Angular Image Upload & Gallery System

Angular Image Upload & Gallery System — সম্পূর্ণ ব্যাখ্যা

তোমার project এ 3টা file আছে। এগুলো একসাথে মিলে কাজ করে। আমি একদম 0 থেকে বুঝাচ্ছি।


🏗️ Big Picture — আগে overall structure বোঝো

┌─────────────────────────────────────────┐
│         Parent Component                │
│   (যেমন: Blog Editor)                   │
│                                         │
│   <app-image-selector                   │
│     (imageSelected)="onImagePick($event)"│
│     (closeModal)="hideModal()">         │
│   </app-image-selector>                 │
└──────────────┬──────────────────────────┘
               │ uses
               ▼
┌─────────────────────────────────────────┐
│      ImageSelectorComponent             │  ← Component (মাথা)
│      (image-selector.component.ts)      │
│                                         │
│  - Gallery দেখায়                        │
│  - Upload handle করে                    │
│  - Event emit করে parent এ              │
└──────────────┬──────────────────────────┘
               │ calls
               ▼
┌─────────────────────────────────────────┐
│         ImageService                    │  ← Service (হাত)
│         (image.service.ts)              │
│                                         │
│  - HTTP GET → সব image আনে             │
│  - HTTP POST → নতুন image upload করে   │
└──────────────┬──────────────────────────┘
               │ talks to
               ▼
┌─────────────────────────────────────────┐
│         Backend API                     │
│   https://localhost:xxxx/api/images     │
└─────────────────────────────────────────┘

Analogy: Component হলো রেস্টুরেন্টের ওয়েটার, Service হলো রান্নাঘর, Backend হলো গোডাউন। ওয়েটার customer এর order নেয়, রান্নাঘরে পাঠায়, রান্নাঘর গোডাউন থেকে মাল এনে রান্না করে।


📦 PART 1 — Interface & Service (image.service.ts)

Step 1.1 — Interface কী?

export interface BlogImage {
  id: string;
  url: string;
  title: string;
}

Interface মানে: Data এর shape/blueprint define করা।

Backend থেকে যখন image data আসে, সেটা এই format এ আসবে:

// Backend থেকে এই format এ data আসে
[
  { "id": "1", "url": "https://...jpg", "title": "My Photo" },
  { "id": "2", "url": "https://...png", "title": "Banner" }
]

Interface দিয়ে TypeScript কে বলছি — “ভাই, BlogImage মানে এই 3টা field থাকবেই”। ভুল field দিলে TypeScript error দেবে। এটা type safety — ভুল data ধরার net।


Step 1.2 — Service এর কাজ কী?

@Injectable({ providedIn: 'root' })
export class ImageService {
  private apiUrl = 'https://localhost:xxxx/api/images';

  constructor(private http: HttpClient) {}

@Injectable({ providedIn: 'root' }) মানে — এই service টা পুরো app এ একবারই তৈরি হবে (Singleton)। যেকোনো component এ inject করলে same instance পাবে।

HttpClient হলো Angular এর built-in tool যেটা দিয়ে HTTP request পাঠানো যায়।


Step 1.3 — getAll() method

getAll(): Observable<BlogImage[]> {
  return this.http.get<BlogImage[]>(this.apiUrl);
}

এটা GET request পাঠায় api/images এ।

Observable কী? ভাবো তুমি একটা pizza order দিলে। দোকান বললো “ready হলে call করবো”। Observable ঠিক তাই — future এ data আসবে, তখন notify করবে

getAll() call করলে → HTTP GET request যায় →
Backend response দেয় → Observable এ data আসে →
.subscribe() এর next: block চলে

Step 1.4 — upload() method

upload(file: File, fileName: string, title: string): Observable<string> {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('fileName', fileName);
  formData.append('title', title);
  return this.http.post<string>(`${this.apiUrl}/upload`, formData);
}

FormData কেন? File পাঠাতে হলে normal JSON দিয়ে হয় না। FormData হলো multipart/form-data format — file binary data পাঠানোর standard way।

Analogy: JSON হলো চিঠি, FormData হলো পার্সেল। File পাঠাতে পার্সেল লাগে।

upload() call →
FormData তে file + name + title pack করা →
POST request → Backend image save করে →
URL return করে (string হিসেবে)

⚙️ PART 2 — Component (image-selector.component.ts)

Step 2.1 — Component এর role

@Component({
  selector: 'app-image-selector',
  standalone: true,
  templateUrl: './image-selector.component.html'
})

standalone: true মানে এই component কে কোনো NgModule এ declare করতে হবে না — নিজেই self-sufficient।


Step 2.2 — Output Events (Parent এর সাথে কথা বলার পথ)

@Output() imageSelected = new EventEmitter<string>();
@Output() closeModal = new EventEmitter<void>();

EventEmitter কী? Component থেকে parent কে signal পাঠানোর mechanism।

Child (ImageSelector) ──emit()──▶ Parent (Blog Editor)
  • imageSelected → যখন কোনো image select হয়, URL টা parent কে পাঠায়
  • closeModal → যখন close করতে হয়, parent কে জানায়

Analogy: তোমার phone এর doorbell app। কেউ আসলে app তোমাকে notify করে — এটাই EventEmitter এর কাজ।


Step 2.3 — Signals (Reactive State)

images = signal<BlogImage[]>([]);
isUploading = signal(false);

Signal কী? Angular 17+ এর নতুন feature। Signal হলো smart variable — এর value change হলে UI automatically update হয়।

// পুরনো way (BehaviorSubject/property binding)
images: BlogImage[] = [];  // change হলে manually detect করতে হতো

// নতুন way (Signal)
images = signal<BlogImage[]>([]);  // change হলে Angular নিজেই UI update করে

Template এ images() এভাবে call করলে current value পাওয়া যায়।


Step 2.4 — ngOnInit() — Gallery Load

ngOnInit() {
  this.imageService.getAll().subscribe({
    next: (res) => this.images.set(res),
    error: (err) => console.error('Load images failed', err)
  });
}

Component তৈরি হওয়ার সাথে সাথে এটা চলে। Flow:

Component তৈরি →
ngOnInit() চলে →
imageService.getAll() call →
HTTP GET request যায় →
Backend response পাঠায় →
next: চলে →
images.set(res) দিয়ে signal update →
UI তে gallery দেখায়

Step 2.5 — Upload Flow

uploadImage() {
  if (!this.selectedFile) return;          // file না থাকলে বের হও
  this.isUploading.set(true);              // button disable + "Uploading..." দেখাও

  this.imageService.upload(this.selectedFile, this.fileName, this.title)
    .subscribe({
      next: (url) => {
        this.isUploading.set(false);       // loading শেষ
        this.refreshImages();              // gallery refresh
        this.imageSelected.emit(url);      // parent কে URL পাঠাও
      },
      error: (err) => {
        this.isUploading.set(false);       // error হলেও loading শেষ
        console.error('Upload failed', err);
      }
    });
}

Complete Upload Flow:

User file select করে →
onFileUploadChange() → selectedFile এ store
User "Upload" click →
uploadImage() চলে →
isUploading = true → button disabled হয় →
service.upload() → HTTP POST →
Backend image save করে URL return করে →
isUploading = false →
refreshImages() → gallery নতুন image দেখায় →
imageSelected.emit(url) → parent এ URL পাঠায়

🖼️ PART 3 — Template (image-selector.component.html)

Step 3.1 — Modal Structure

<div class="modal fade show d-block">    <!-- Bootstrap modal wrapper -->
  <div class="modal-dialog modal-lg">    <!-- Size: large -->
    <div class="modal-content">
      <!-- Header / Body / Footer -->
    </div>
  </div>
</div>

Bootstrap এর modal class দিয়ে popup UI বানানো হয়েছে।


Step 3.2 — Gallery Loop

@for (image of images(); track image.id) {
  <div (click)="selectImage(image.url)" style="cursor: pointer;">
    <img [src]="image.url" style="width: 120px; height: 100px; object-fit: cover;">
    <div>{{image.title}}</div>
  </div>
}
  • images() — signal এর current value পড়া
  • track image.id — Angular কে বলছে প্রতিটা image কে id দিয়ে চেনো (performance এর জন্য)
  • (click)="selectImage(image.url)" — click করলে সেই image এর URL emit হবে

Step 3.3 — Upload Form

<form (ngSubmit)="uploadImage()">
  <input type="file" (change)="onFileUploadChange($event)">
  <input [(ngModel)]="fileName" name="fileName">
  <input [(ngModel)]="title" name="title">
  
  <button [disabled]="isUploading()">
    @if (isUploading()) { Uploading... }
    @else { Upload }
  </button>
</form>
  • (ngSubmit) — form submit হলে uploadImage() চলবে
  • [(ngModel)]two-way binding: input এ type করলে fileName variable আপডেট, variable change করলে input আপডেট
  • [disabled]="isUploading()" — uploading এর সময় button disable
  • @if — Angular 17 এর নতুন control flow syntax

🔄 সব মিলিয়ে Complete Flow

┌─────────────────────────────────────────────────┐
│                  PAGE LOAD                       │
│                                                  │
│  Component তৈরি → ngOnInit()                    │
│       ↓                                          │
│  imageService.getAll() → GET /api/images         │
│       ↓                                          │
│  images signal update → Gallery render           │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│              IMAGE SELECT (existing)             │
│                                                  │
│  Gallery image click → selectImage(url)          │
│       ↓                                          │
│  imageSelected.emit(url) → Parent পায় URL       │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│              IMAGE UPLOAD (new)                  │
│                                                  │
│  File choose → onFileUploadChange()              │
│  Name + Title type → ngModel update              │
│  Upload click → uploadImage()                    │
│       ↓                                          │
│  isUploading = true → "Uploading..." দেখায়      │
│       ↓                                          │
│  imageService.upload() → POST /api/images/upload │
│       ↓                                          │
│  Success → isUploading = false                   │
│          → refreshImages() → Gallery update      │
│          → imageSelected.emit(url)               │
└─────────────────────────────────────────────────┘

🧠 Key Concepts Summary

Conceptকী করেExample
InterfaceData এর shape define করেBlogImage { id, url, title }
ServiceHTTP call handle করেimageService.getAll()
SignalReactive state, UI auto-updateimages = signal([])
ObservableAsync data stream.subscribe({ next, error })
EventEmitterChild → Parent communicationimageSelected.emit(url)
FormDataFile upload এর জন্যformData.append('file', file)
ngModelTwo-way data binding[(ngModel)]="fileName"

এই pattern টা বুঝলে যেকোনো Angular project এ file upload + gallery বানাতে পারবে। শুধু apiUrl আর interface টা নিজের project অনুযায়ী change করতে হবে।

@Output() imageSelected = new EventEmitter<string>();
@Output() closeModal = new EventEmitter<void>();

Explain Modal to Parent(Add/Edit Blog Post)

একেবারে জিরো থেকে বুঝতে হলে আমাদের Component Communication বা “দুইটি কম্পোনেন্টের মধ্যে কথা বলা” বুঝতে হবে।

সহজ একটি বাস্তব উদাহরণ দিয়ে শুরু করি:

ধরো, তুমি একটি দোকানে গিয়েছো। দোকানের মালিক (Parent Component) তোমাকে একটি ক্যালকুলেটর (Child Component) দিল। তুমি ক্যালকুলেটরে কিছু হিসাব করলে (ইমেজ সিলেক্ট করলে)। এখন ক্যালকুলেটর তো আর নিজে নিজে জানে না যে সেই রেজাল্ট দিয়ে মালিক কী করবে। তাই ক্যালকুলেটর একটি আওয়াজ করল বা রেজাল্টটি স্ক্রিনে দেখালো যাতে মালিক সেটা শুনে তার খাতায় লিখে নিতে পারে।

এখানে app-image-selector হলো সেই ক্যালকুলেটর (Child), আর AddBlogPost হলো দোকানের মালিক (Parent)।

১. @Output কেন ব্যবহার করা হয়?

অ্যাঙ্গুলারে ডেটা সাধারণত উপর থেকে নিচে (Parent to Child) সহজেই যায়। কিন্তু নিচ থেকে উপরে (Child to Parent) ডেটা পাঠাতে হলে আমাদের একটি “খবর পাঠানোর মাধ্যম” লাগে। এই মাধ্যমটিই হলো @Output

ImageSelector এর ভেতরে যখন ইউজার কোনো ছবিতে ক্লিক করে, তখন ওই কম্পোনেন্টটি চিৎকার করে বলে— “আমি একটা ইমেজ পেয়েছি!”। এই চিৎকারটাই হলো emit()

২. (imageSelected)="onImageSelected($event)" এর ব্যবচ্ছেদ:

  • imageSelected: এটি হলো চাইল্ড কম্পোনেন্টের তৈরি করা একটি Custom Event। চাইল্ড কম্পোনেন্টের ভেতরে এটি এভাবে লেখা আছে: @Output() imageSelected = new EventEmitter<string>();
  • onImageSelected($event): এটি হলো প্যারেন্ট কম্পোনেন্টের একটি ফাংশন। যখনই চাইল্ড কম্পোনেন্ট কোনো খবর পাঠাবে, প্যারেন্ট এই ফাংশনটি চালু করবে।
  • $event: এটি একটি স্পেশাল কিউওয়ার্ড। চাইল্ড কম্পোনেন্ট চিল্লিয়ে যে ডেটাটা পাঠিয়েছে (এখানে ইমেজের URL), সেই ডেটাটা এই $event এর ভেতরে থাকে।

সহজ কথায়: প্যারেন্ট কম্পোনেন্ট চাইল্ডকে বলছে— “তুমি যখনই imageSelected ইভেন্টটা ঘটাবে, আমি তখন আমার onImageSelected ফাংশনটা চালাবো এবং তোমার পাঠানো ডাটাটা ($event) গ্রহণ করবো।”


৩. (closeModal)="closeImageSelector()" এর ব্যবচ্ছেদ:

এটিও হুবহু একই লজিক।

  • চাইল্ড কম্পোনেন্টের ভেতরে একটি ক্লোজ বাটন আছে। সেখানে ক্লিক করলে সে closeModal.emit() করে।
  • প্যারেন্ট কম্পোনেন্ট তখন তার নিজের closeImageSelector() ফাংশনটি চালায়।
  • এই ফাংশনের কাজ কী? showImageSelector.set(false) করা। অর্থাৎ মোডালটি স্ক্রিন থেকে সরিয়ে ফেলা।

৪. কেন এটি TS-এ @Output হিসেবে ছিল?

কারণ ImageSelector কম্পোনেন্টটি একা কিছু করতে পারে না। সে শুধু ইমেজ দেখাতে পারে এবং সিলেক্ট করতে পারে। কিন্তু সেই ইমেজটা নিয়ে কোথায় বসাতে হবে (ব্লগ পোস্টের ইমেজে নাকি ইউজারের প্রোফাইল পিকচারে), সেটা সে জানে না।

তাই তাকে এমনভাবে বানানো হয়েছে যাতে সে শুধু ডাটাটা “আউটপুট” হিসেবে বাইরে পাঠিয়ে দেয়। এখন যে প্যারেন্ট তাকে ব্যবহার করবে, সে তার নিজের ইচ্ছেমতো ডাটাটা ব্যবহার করবে। একেই বলে Reusability (একই জিনিস বারবার ব্যবহার করা)।

সারসংক্ষেপ:

কোড অংশভূমিকাসহজ মানে
@Outputমাইকখবর বাইরে পাঠানোর যন্ত্র।
.emit(data)চিৎকার করা“এই নাও ডাটা!” বলে চিৎকার দেওয়া।
(event)কান পাতাপ্যারেন্ট কম্পোনেন্ট চাইল্ডের খবরের জন্য কান পেতে আছে।
$eventপার্সেলচাইল্ড যে ডাটাটা পার্সেল করে পাঠিয়েছে।

Recap: Full Flow of a image upload

একদম জিরো থেকে স্টেপ-বাই-স্টেপ বুঝে নাও এই ইমেজ সিলেক্টর মোডালটি কীভাবে কাজ করছে। আমরা দুই ভাগে এটি দেখব।


পার্ট ১: ইমেজ সিলেক্টর (Child) এর ভেতর কী হচ্ছে?

ইউজার যখন কম্পিউটার থেকে একটি ইমেজ সিলেক্ট করে সার্ভারে পাঠানোর আগ পর্যন্ত এই ধাপগুলো ঘটে:

ধাপ ১: ফাইল নির্বাচন (Selection)

ইউজার যখন Choose File বাটনে ক্লিক করে একটি ছবি পছন্দ করে, তখন HTML-এর (change) ইভেন্টটি জেগে ওঠে।

  • কোড: (change)="onFileUploadChange($event)"
  • কাজ: এটি ব্রাউজার থেকে ওই ফাইলটির তথ্য (নাম, সাইজ, টাইপ) নিয়ে TS ফাইলের selectedFile ভেরিয়েবলে জমা রাখে। সাথে সাথে তুমি চালাকি করে ফাইলের আসল নামটা fileName বক্সে বসিয়ে দাও যাতে ইউজারকে কষ্ট করে টাইপ করতে না হয়।

ধাপ ২: ডাটা বাইন্ডিং (Input)

ইউজার যখন File Name এবং Image Title বক্সে কিছু লেখে, তখন [(ngModel)] এর কারণে সেই লেখাগুলো সাথে সাথে TS-এর fileName এবং title ভেরিয়েবলে আপডেট হয়ে যায়।

ধাপ ৩: আপলোড ট্রিগার (The Push)

ইউজার যখন Upload বাটনে ক্লিক করে, তখন uploadImage() ফাংশনটি কল হয়।

  • কাজ: প্রথমে সে isUploading.set(true) করে দেয়। এতে বাটনটি সাথে সাথে “Uploading…” হয়ে যায় এবং ডিজেবল হয়ে যায় (যাতে ইউজার বারবার ক্লিক না করে)।
  • এরপর এটি Service-কে সব ডাটা (File, Name, Title) দিয়ে দেয় সার্ভারে পাঠানোর জন্য।

পার্ট ২: গ্যালারি (Gallery) পার্ট কীভাবে কাজ করছে?

গ্যালারি হলো আগে আপলোড করা ছবির একটি ভাণ্ডার।

  1. লোড হওয়া (Initialization): তুমি ngOnInit ব্যবহার করেছ। এর মানে মোডালটি স্ক্রিনে আসার ১ মিলি-সেকেন্ডের মধ্যে এটি সার্ভার থেকে সব ছবির লিস্ট নিয়ে আসে এবং images সিগন্যালে সেট করে দেয়।
  2. লুপ চালানো (Display): HTML-এ @for লুপ ব্যবহার করে সেই সিগন্যাল থেকে প্রতিটি ইমেজের url নিয়ে <img> ট্যাগে বসানো হয়। এভাবেই ইউজার গ্যালারিতে ছবিগুলো দেখতে পায়।
  3. সিলেক্ট করা (Action): কোনো ছবিতে ক্লিক করলে (click)="selectImage(image.url)" কল হয়। এটি সাথে সাথে ওই ছবির লিঙ্কটা নিয়ে প্যারেন্ট কম্পোনেন্টকে (Add/Edit) পাঠিয়ে দেয়।

পার্ট ৩: Parent (Add/Edit) এবং Child (ImageSelector) এর কথোপকথন

এটিই সবচেয়ে গুরুত্বপূর্ণ। প্যারেন্ট এবং চাইল্ড কীভাবে একে অপরের সাথে কাজ করে তা নিচে দেখো:

১. প্যারেন্টের “কান পাতা” (Parent Listening)

প্যারেন্ট কম্পোনেন্টের HTML-এ তুমি লিখেছ:

HTML

<app-image-selector 
  (imageSelected)="onImageSelected($event)" 
  (closeModal)="closeImageSelector()">
</app-image-selector>
  • (imageSelected): চাইল্ড যখনই কোনো ছবি পায়, সে একটা সিগন্যাল দেয়। প্যারেন্ট তার onImageSelected ফাংশন দিয়ে সেই সিগন্যালটি রিসিভ করে।
  • $event: চাইল্ড যে ইমেজের URL-টি পাঠিয়েছে, সেটি এই $event-এর ভেতরে থাকে।

২. প্যারেন্টের “কাজ করা” (Parent Action)

প্যারেন্ট কম্পোনেন্টের TS-এ যখন onImageSelected(url: string) ফাংশনটি চলে:

TypeScript

onImageSelected(url: string) {
  // ১. মেইন ফর্মের ভেতর ইমেজের লিঙ্কটা বসিয়ে দেয়
  this.addBlogPostForm.patchValue({ featuredImgUrl: url }); 
  
  // ২. কাজ শেষ, তাই মোডালটা বন্ধ করে দেয়
  this.showImageSelector.set(false); 
}

৩. মোডাল বন্ধ করা (Closing)

ইউজার যদি ইমেজ সিলেক্ট না করে শুধু ‘Close’ বাটন বা ‘X’ বাটনে ক্লিক করে:

  • চাইল্ড closeModal.emit() করে।
  • প্যারেন্ট সেটা শুনে তার closeImageSelector() ফাংশন চালায়, যা showImageSelector কে false করে দেয়। ব্যাস, মোডালটি অদৃশ্য হয়ে যায়!

সহজ সারাংশ:

  • Child: একটি স্বাধীন টুলবক্স। সে শুধু ছবি আপলোড করে বা গ্যালারি থেকে ছবি বেছে দেয় এবং সেই লিঙ্কটা বাইরে “ছুঁড়ে” দেয় (emit)।
  • Parent: মেইন কন্ট্রোলার। সে চাইল্ডের ছোঁড়া লিঙ্কটা “ক্যাচ” করে এবং নিজের ফর্মে বসিয়ে দেয়।

এই জিরো-টু-হিরো প্রসেসটি কি এখন তোমার কাছে পরিষ্কার? নাকি কোনো নির্দিষ্ট লাইন নিয়ে কনফিউশন আছে?

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top