← الدرس السابق المستوى 4 - الدرس 7 من 12

🧩 Angular Components المتقدمة: @Input و @Output و Lifecycle Hooks

تقدمك في المستوى الرابع 58%

🧩 المكونات المتقدمة في Angular

في الدرس السابق تعلمنا أساسيات Angular: إنشاء المشروع، المكونات البسيطة، وأنواع الربط. في هذا الدرس، هنغوص أعمق في بناء المكونات المتقدمة وإدارة التواصل بينها. هنتعلم كيفية استخدام Decorators المختلفة مثل @Input و @Output لنقل البيانات بين المكونات، وكيفية استخدام دوال الحياة (Lifecycle Hooks) لتنفيذ كود في أوقات محددة من دورة حياة المكون.

بنهاية هذا الدرس، هتكون قادر على:

💡 نصيحة: فكر في Angular Components زي "صناديق متداخلة". كل صندوق (مكون) بيستقبل بيانات من اللي فوقه (@Input) وبيبعت إشعارات للي فوقه (@Output). ده نفس مفهوم Props في React لكن بطريقة أكثر تنظيماً.

⚡ إنشاء Component باستخدام Angular CLI

Angular CLI مش بس بتسهل إنشاء المشاريع، لكنها بتولّد 4 ملفات جاهزة لكل مكون جديد. ده بيوفر عليك وقت كبير وبيضمن إن المكون متوافق مع معايير Angular.

الخطوة 1: تثبيت Angular CLI (لو مش مثبت)

# تثبيت Angular CLI عالمياً npm install -g @angular/cli # التحقق من الإصدار ng version

الخطوة 2: إنشاء مكون جديد

# إنشاء مكون جديد - يُنشئ مجلد كامل مع 4 ملفات ng generate component user-card # أو الاختصار الأسرع ng g c user-card # النتيجة: src/app/user-card/ ├── user-card.component.ts // منطق المكون (Class + Decorator) ├── user-card.component.html // قالب العرض (Template) ├── user-card.component.css // أنماط خاصة بالمكون └── user-card.component.spec.ts // اختبارات وحدة (Unit Tests)

ملاحظة مهمة: الأمر ng g c بيعمل 3 حاجات تلقائياً:

  1. ينشئ المجلد والملفات الأربعة
  2. يضيف المكون في declarations في app.module.ts
  3. يضيف import للمكون في الملفات اللي محتاجاه
🚀 اختصارات CLI مفيدة:
  • ng g c name - مكون جديد
  • ng g s name - خدمة جديدة (Service)
  • ng g m name - وحدة جديدة (Module)
  • ng g p name - Pipe جديد
  • ng g d name - Directive جديدة

📥 @Input() - استقبال البيانات من المكون الأب

@Input() هو Decorator بيسمح للمكون الابن باستقبال بيانات من المكون الأب. ده مكافئ لـ Props في React، لكن الفرق إن Angular بتستخدم Decorator عشان تعلّم الخاصية إنها "مدخلة" من الخارج.

لما تستخدم @Input()، Angular بتعمل One-way Data Binding من الأب للابن. يعني لو البيانات اتغيرت في الأب، هتتغير في الابن تلقائياً. لكن العكس مش صحيح - الابن مش يقدر يعدل الـ Input مباشرة.

🔍 مثال عملي: بطاقة مستخدم (User Card)

// user-card.component.ts - المكون الابن import { Component, Input } from '@angular/core'; @Component({ selector: 'app-user-card', templateUrl: './user-card.component.html', styleUrls: ['./user-card.component.css'] }) export class UserCardComponent { // ✅ استقبال البيانات من المكون الأب @Input() name: string = ''; @Input() email: string = ''; @Input() age: number = 0; @Input() isActive: boolean = true; // دالة مساعدة لتحديد الحالة getUserStatus(): string { return this.isActive ? '🟢 نشط' : '🔴 غير نشط'; } }
<!-- user-card.component.html - قالب العرض --> <div class="card"> <div class="card-header"> <h3>{{ name }}</h3> <span [ngClass]="{ 'badge-active': isActive, 'badge-inactive': !isActive }"> {{ getUserStatus() }} </span> </div> <div class="card-body"> <p>📧 البريد: {{ email }}</p> <p>🎂 العمر: {{ age }} سنة</p> </div> </div>

🔍 الاستخدام في المكون الأب:

// app.component.ts - المكون الأب export class AppComponent { // بيانات ثابتة user1 = { name: 'أحمد محمد', email: 'ahmed@example.com', age: 28, isActive: true }; // بيانات من API (مثال) users = [ { name: 'فاطمة', email: 'fatima@example.com', age: 25, isActive: true }, { name: 'خالد', email: 'khaled@example.com', age: 32, isActive: false } ]; }
<!-- app.component.html --> <!-- طريقة 1: تمرير قيم ثابتة --> <app-user-card [name]="'أحمد محمد'" [email]="'ahmed@example.com'" [age]="28" [isActive]="true"> </app-user-card> <!-- طريقة 2: تمرير متغيرات من المكون الأب --> <app-user-card [name]="user1.name" [email]="user1.email" [age]="user1.age" [isActive]="user1.isActive"> </app-user-card> <!-- طريقة 3: تكرار مع *ngFor (الأكثر استخداماً) --> <div class="users-grid"> <app-user-card *ngFor="let user of users" [name]="user.name" [email]="user.email" [age]="user.age" [isActive]="user.isActive"> </app-user-card> </div>

⚠️ خطأ شائع: نسيان الأقواس المربعة [ ]

غلط: name="user.name" - ده بيعتبرها نص عادي (String).

صح: [name]="user.name" - ده بيربطها بقيمة المتغير (Property Binding).

📤 @Output() - إرسال البيانات والأحداث للأب

@Output() هو Decorator بيسمح للمكون الابن بإرسال أحداث (Events) للمكون الأب. ده مكافئ لـ Callback Props في React (زي onClick أو onDelete).

Angular بتستخدم EventEmitter عشان تنشئ حدث مخصص. لما يحصل حاجة في الابن (زي نقر زر)، الابن بيستدعي .emit() والأب بيستقبل الحدث ويتعامل معاه.

🔍 مثال: زر مخصص يرسل حدث للأب

// button.component.ts - المكون الابن import { Component, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-custom-button', template: ` <button (click)="onClick()" class="btn-primary"> {{ label }} </button> ` }) export class ButtonComponent { // ✅ إرسال حدث للأب @Output() buttonClicked = new EventEmitter<string>(); label = 'انقر هنا'; onClick() { this.buttonClicked.emit('تم النقر على الزر في ' + new Date().toLocaleTimeString()); } }
// app.component.ts - المكون الأب export class AppComponent { lastClickMessage = ''; onButtonClick(message: string) { this.lastClickMessage = message; console.log('الابن قال:', message); } }
<!-- app.component.html --> <app-custom-button (buttonClicked)="onButtonClick($event)"> </app-custom-button> <p *ngIf="lastClickMessage"> آخر رسالة: {{ lastClickMessage }} </p>

🔍 مثال متقدم: حذف مستخدم من القائمة

// user-card.component.ts (محدّث مع Output) import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-user-card', templateUrl: './user-card.component.html' }) export class UserCardComponent { @Input() name: string = ''; @Input() email: string = ''; @Input() userId: number = 0; // ✅ إرسال حدث حذف للأب @Output() deleteUser = new EventEmitter<number>(); onDelete() { this.deleteUser.emit(this.userId); } }
<!-- user-card.component.html --> <div class="card"> <h3>{{ name }}</h3> <p>{{ email }}</p> <button (click)="onDelete()" class="btn-danger"> 🗑️ حذف </button> </div>
// app.component.ts - المكون الأب export class AppComponent { users = [ { id: 1, name: 'أحمد', email: 'ahmed@test.com' }, { id: 2, name: 'فاطمة', email: 'fatima@test.com' } ]; handleDelete(userId: number) { this.users = this.users.filter(user => user.id !== userId); console.log('تم حذف المستخدم رقم:', userId); } }
<!-- app.component.html --> <app-user-card *ngFor="let user of users" [name]="user.name" [email]="user.email" [userId]="user.id" (deleteUser)="handleDelete($event)"> </app-user-card>
🎯 القاعدة الذهبية:
  • @Input = البيانات بتنزل من فوق (أب → ابن)
  • @Output = الأحداث بتطلع لفوق (ابن → أب)
ده بيسمّوه Unidirectional Data Flow (تدفق بيانات في اتجاه واحد)، وهو نفس المبدأ في React.

🔄 Lifecycle Hooks - دوال حياة المكون

كل مكون في Angular بيمر بـ دورة حياة محددة: الإنشاء → التهيئة → التحديثات → التدمير. Angular بتوفّر Interfaces تقدر تطبقها عشان "تتنصت" على مراحل معينة من الدورة.

🔍 ترتيب Lifecycle Hooks:

// my-component.component.ts import { Component, OnInit, OnDestroy, OnChanges, DoCheck, AfterViewInit, AfterContentInit, SimpleChanges } from '@angular/core'; @Component({ selector: 'app-my-component', template: '<p>مرحباً {{ name }}</p>' }) export class MyComponent implements OnInit, OnDestroy, OnChanges, DoCheck, AfterViewInit, AfterContentInit { @Input() name: string = ''; // 1️⃣ ngOnChanges - يعمل أولاً وفي كل تغيير لـ Inputs ngOnChanges(changes: SimpleChanges) { console.log('📊 ngOnChanges:', changes); // مفيد لـ: تتبع تغييرات Props، تنفيذ منطق عند تغيير بيانات } // 2️⃣ ngOnInit - يعمل مرة واحدة بعد الإنشاء ngOnInit() { console.log('🚀 ngOnInit: المكون تم إنشاؤه'); // مفيد لـ: جلب البيانات من API، الاشتراك في الخدمات // ⚠️ لازم تكون كل الـ Inputs جاهزة هنا } // 3️⃣ ngDoCheck - يعمل مع كل تغيير يدوي ngDoCheck() { console.log('🔍 ngDoCheck: فحص يدوي'); // مفيد لـ: منطق مخصص للكشف عن التغييرات } // 4️⃣ ngAfterContentInit - بعد محتوى الـ <ng-content> يتحمل ngAfterContentInit() { console.log('📦 ngAfterContentInit'); } // 5️⃣ ngAfterViewInit - بعد القالب (Template) يترسم كاملاً ngAfterViewInit() { console.log('🎨 ngAfterViewInit: القالب جاهز'); // مفيد لـ: التعامل مع DOM مباشرة، رسم Charts } // 6️⃣ ngOnDestroy - قبل حذف المكون نهائياً ngOnDestroy() { console.log('💀 ngOnDestroy: المكون سيتم حذفه'); // مفيد لـ: إلغاء الاشتراكات، تنظيف المؤقتات (Timers) // ⚠️ مهم جداً عشان تتجنب Memory Leaks } }

📋 جدول Lifecycle Hooks:

الـ Hook متى يشتغل؟ الاستخدام الشائع
ngOnChanges قبل ngOnInit + كل تغيير في Inputs تتبع تغييرات Props، منطق عند تغيير بيانات
ngOnInit مرة واحدة بعد الإنشاء جلب بيانات API، اشتراك في Services
ngDoCheck مع كل تغيير (يدوي) كشف تغييرات مخصصة
ngAfterContentInit بعد محتوى <ng-content> تفاعل مع محتوى مُدرج
ngAfterViewInit بعد رسم القالب كاملاً تفاعل مع DOM، Charts، Libraries خارجية
ngOnDestroy قبل حذف المكون إلغاء الاشتراكات، تنظيف الموارد

🔍 مثال عملي: جلب بيانات مع تنظيف

import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { UserService } from './user.service'; @Component({ selector: 'app-user-list', template: ` <div *ngIf="loading">⏳ جاري التحميل...</div> <div *ngIf="error">❌ {{ error }}</div> <ul> <li *ngFor="let user of users">{{ user.name }}</li> </ul> ` }) export class UserListComponent implements OnInit, OnDestroy { users: any[] = []; loading = true; error = ''; // ✅ Subject للتنظيف الآمن private destroy$ = new Subject<void>(); constructor(private userService: UserService) { } ngOnInit() { this.userService.getUsers() .pipe( takeUntil(this.destroy$) // ✅ إلغاء الاشتراك تلقائياً ) .subscribe({ next: (data) => { this.users = data; this.loading = false; }, error: (err) => { this.error = 'فشل في جلب البيانات'; this.loading = false; } }); } ngOnDestroy() { // ✅ تنظيف الاشتراك عند حذف المكون this.destroy$.next(); this.destroy$.complete(); } }

⚠️ خطأ قاتل: نسيان إلغاء الاشتراكات

لو استخدمت .subscribe() من غير takeUntil أو unsubscribe()، الـ Observable هيفضل شغال حتى بعد ما المكون يتحذف. ده بيسبب Memory Leak وبيأثر على أداء التطبيق.

الحل: استخدم takeUntil مع Subject زي المثال اللي فوق، أو استخدم async pipe في Template.

💡 نصائح ومعايير مهمة في Angular

📚 دروس ذات صلة

❓ أسئلة شائعة عن Angular Components

ما الفرق بين Constructor و ngOnInit؟

Constructor بيشتغل لما Angular تنشئ instance من الClass. استخدمه بس للـ Dependency Injection (تمرير Services). ngOnInit بيشتغل بعد ما Angular تهيئ الـ Inputs. استخدمه للمنطق الفعلي زي جلب البيانات من API. السبب: في Constructor، الـ Inputs لسه مش جاهزة.

هل يمكن تمرير Object كامل بدل Properties منفصلة في @Input؟

نعم، ومستحسن كمان! بدل @Input() name; @Input() email;، استخدم @Input() user: User;. ده بيقلل عدد الـ Inputs وبيسهل الصيانة. بس تأكد إنك بتستخدم ChangeDetectionStrategy.OnPush لو الـ Object كبير.

ما الفرق بين EventEmitter و Subject في Angular؟

EventEmitter مخصص للـ @Output في المكونات. Subject جزء من RxJS ويستخدم في Services وإدارة الحالة. EventEmitter أبسط لكن Subject أكثر قوة ومرونة. في Angular 16+، EventEmitter أصبح wrapper حول Subject.

إزاي أتجنب Memory Leaks في Angular؟

3 طرق: (1) استخدم takeUntil مع Subject في ngOnDestroy، (2) استخدم async pipe في Template (بيُلغي الاشتراك تلقائياً)، (3) استخدم .unsubscribe() يدوياً في ngOnDestroy. الطريقة الأولى هي الأكثر شيوعاً.

← الدرس السابق