connect with me on LinkedIn for more technical updates and content.
This Repo is inspired by the following sources:
- angular.dev
- TekTutorialsHub-Logo
- Logo ConcretePage.com
- List of 300 Angular Interview Questions and answers
connect with me on LinkedIn for more technical updates and content.
connect with me on LinkedIn for more technical updates and content.
connect with me on LinkedIn for more technical updates and content.
connect with me on LinkedIn for more technical updates and content.
connect with me on LinkedIn for more technical updates and content.
No. | Questions |
---|---|
1 | What is Angular Http Interceptor |
connect with me on LinkedIn for more technical updates and content.
No. | Questions |
---|---|
1 | Why Handle Errors? |
2 | What is HttpErrorResponse? |
3 | How Can Catching Errors in HTTP Request? |
function greeter(person: string) {
return "Hello, " + person;
}
let user = "mohamed";
document.body.innerHTML = greeter(user);
في المثال ده، الـ greeter function مش بتقبل غير المتغيرات اللي نوعها string بس كـ argument، وده مثال على قوة TypeScript في تحديد نوع المتغيرات.
The main building blocks of an Angular application are shown in the diagram below:-
ال(Components) في Angular زي الطوب اللي بتبني بيه التطبيق بتاعك، كل Component مسؤول عن جزء معين من الصفحة اللي بتظهر للمستخدم. يعني هو اللي بيقول الـ HTML يتعرض إزاي ويشتغل إزاي. زي مثلاً لو عندك صفحة تسجيل دخول، الComponent هو اللي هيتحكم في الشكل والأزرار وكل حاجة بتظهر في الجزء ده.
في Angular هي عبارة عن مجموعة من Components Angular الأساسية زي الـ Components، والـ Directives، والـ Services وغيرها. التطبيق بيتقسم لقطع منطقية، وكل قطعة بتتسمى "Module" وبتقوم بمهمة واحدة محددة.
ال(Templates) في Angular زي الرسمة اللي بتقول Component يعرض إيه بالظبط. يعني هي اللي بتحدد الشكل اللي المستخدم هيشوفه، زي الأزرار، النصوص، والصور. Template ده بيبقى مليان كود HTML، بس ممكن كمان يكون فيه حاجات ديناميكية بتتغير حسب اللي بيحصل في التطبيق. يعني باختصار، Template هو اللي بيرسم شكل الواجهة اللي هتشوفها قدامك.
دي زي حاجات بتعملها مرة واحدة وتقدر تستخدمها في أي حتة في التطبيق بتاعك. يعني لو عندك شغلانة معينة وعايز تعملها في كذا مكان، مش هتعيد الكود كل مرة، بتعملها كـ "Service" وتستخدمها في أي حتة.
دي زي شوية معلومات إضافية بتضيفها على الكلاس عشان تخليه يشتغل بشكل معين في Angular. يعني زي ما تكون بتدي توجيهات زيادة للكلاس ده، عشان Angular يفهمه بطريقة أحسن.
import { Directive, ElementRef } from "@angular/core";
@Directive({ selector: "[myHighlight]" })
export class HighlightDirective {
constructor(el: ElementRef) {
el.nativeElement.style.backgroundColor = "yellow";
}
}
<p myHighlight>Highlight me!</p>
الـ Components دي بتشتغل مع بعض وبتتجمع عشان تبني التطبيق كله، وعشان كده بنقول إنها بتكون شجرة من الـ Components، يعني كل Component ممكن يكون جواه Components تانية أصغر. زي ما يكون عندك صفحة فيها أجزاء مختلفة وكل جزء فيه تفاصيله الخاصة.
الComponent هو نوع من الـ Directives، بس الفرق الأساسي هو إن الـ Component لازم يكون ليه template، يعني لازم يعرض حاجة للمستخدم زي نصوص، صور، أزرار... إلخ.
في المقابل، الـ Directive العادي مش لازم يكون ليه template، وممكن يستخدم بس عشان يضيف أو يعدل سلوك عنصر HTML موجود. يعني بيغير في العنصر من غير ما يعرض محتوى جديد.
الComponent بيكون ليه template وبيعرض حاجة للمستخدم. كل تطبيق Angular بيتكون من مجموعة Components بتشتغل مع بعض زي شجرة. الفرق الأساسي بين Component و Directive هو إن الأول بيعرض واجهة (UI) والثاني بس بيغير أو يضيف سلوك للعناصر اللي موجودة.
يعني إنك تكتب كود الـ HTML مباشرة جوا الـ Component باستخدام الخاصية template. ده مناسب لو عندك HTML بسيط.
يعني إنك تخزن الـ template في ملف HTML منفصل وتربطه بالـ Component باستخدام templateUrl. ده بيكون مناسب لما يكون عندك HTML كبير أو معقد.
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<div>
<h1>{{ title }}</h1>
<div>Learn Angular</div>
</div>
`,
})
export class AppComponent {
title: string = "Hello World";
}
import { Component } from "@angular/core";
@Component({
selector: "my-app",
templateUrl: "app/app.component.html",
})
export class AppComponent {
title: string = "Hello World";
}
خلينا ناخد مثال على الملف app.module.ts، اللي هو الموديول الرئيسي في التطبيق، وبيتم الإعلان عنه باستخدام @NgModule decorator
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
@NgModule({
imports: [BrowserModule],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: [],
})
export class AppModule {}
دي بتستخدم عشان تستورد موديولات تانية محتاجها في التطبيق. في المثال ده، بنستورد BrowserModule اللي بيكون مطلوب لأي تطبيق ويب في Angular.
دي بتستخدم عشان تعلن عن الـ Components اللي موجودة في الموديول ده. هنا بنعلن عن AppComponent.
دي بتقول لـ Angular أي Component هو اللي هيبدأ تشغيل التطبيق. في الحالة دي، AppComponent هو اللي بيبدأ تشغيل التطبيق.
بتستخدم عشان تحدد الحاجات اللي تقدر تعمل لها حقن (Injectable objects) في أجزاء مختلفة من التطبيق. يعني لو عندك (Services) معينة وعايز كل أجزاء التطبيق تقدر تستعملها، بتحطها في المكان ده. مثلاً، لو عندك Service بتجيب بيانات من سيرفر، بتقدر تحطها في الـ providers عشان أي Component في التطبيق يقدر يستخدمها من غير ما تضطر تعيد الكود في كل مكان.
الـ Modules بشكل عام بتساعد في تقسيم الوظائف بشكل منطقي في التطبيق وبتسهل تنظيم الكود وإعادة استخدامه.
بيتنفذ لما يحصل تغيير في قيمة أي خاصية مربوطة بالبيانات (data bound property). يعني كل ما تتغير بيانات الـ Component بيتنفذ الكود ده.
بيتنفذ أول ما يتم تهيئة (initialize) الـ Component أو الـ Directive بعد ما Angular يعرض الخصائص اللي مربوطة بالبيانات لأول مرة.
بيتنفذ عشان يكتشف أي تغييرات Angular مش قادر يكتشفها لوحده، وبيخليك تعمل أي حاجات إضافية لو فيه تغييرات معينة محتاج تتعامل معاها يدوي.
بيتنفذ بعد ما Angular يعمل (project) للمحتوى الخارجي جوا الـ Component.
بيتنفذ بعد ما Angular يتأكد من المحتوى اللي تم إسقاطه جوا الـ Component، يعني بيتشيك على المحتوى اللي جوا الـ Component بعد ما يتم عرضه.
بيتنفذ بعد ما Angular يهيئ (initialize) الـ Views الخاصة بالـ Component وأي Child Components (الأطفال اللي جواه).
بيتنفذ بعد ما Angular يتأكد من الـ Views الخاصة بالـ Component وأي Child Components.
ده بيكون المرحلة الأخيرة، بيتنفذ قبل ما Angular يدمر الـ Component أو الـ Directive. هنا بتعمل تنظيف (cleanup) لأي موارد أو اشتراكات (subscriptions) مستخدمة في الـ Component.
✨ باختصار، كل Lifecycle hook بيخليك تتحكم في سلوك الـ Component في مراحل معينة من حياته، زي لما يتغير، يتعرض، أو يتدمر.
ده بيستخدم لما عايز تعرض قيمة متغير من الـ Component جوه الـ HTML. بتستخدم الأقواس المزدوجة {{ }} زي ما بنقول "خذ القيمة دي من الكود بتاعك وحطها هنا في للview".
<li>Name: {{ user.name }}</li>
<li>Address: {{ user.address }}</li>
هنا، بتربط قيمة معينة من الـ Component بخاصية (property) من خصائص عنصر HTML. بتستخدم الأقواس المربعة [ ] حوالين الخاصية اللي عايز تتحكم فيها.
<input type="email" [value]="user.email">
ده لما عايز تربط حدث معين (زي click، أو keyup) بدالة أو وظيفة في الـ Component بتاعك. بتستخدم الأقواس العادية ( ) حوالين اسم الحدث.
<button (click)="logout()"></button>
هنا بتربط البيانات بين الـ Component والـ DOM في الاتجاهين. يعني لو المستخدم غيّر حاجة في الواجهة، القيمة بتتحدث في الـ Component، ولو حصل العكس، القيمة اللي في الـ Component بتتغير في الواجهة. بتستخدم أقواس مربعة وعادية [( )] حوالين الـ ngModel.
<input type="email" [(ngModel)]="user.email">
-الInterpolation و Property Binding بيخلوا البيانات تمشي من الـ Component للـ DOM. -الEvent Binding بيخلي البيانات تمشي من الـ DOM للـ Component. -الTwo-way binding بيخلي البيانات تمشي في الاتجاهين، بين الـ Component والـ DOM في نفس الوقت.
بتستخدم لتزيين الكلاس كله، زي لما بنستخدم Component@ أو NgModule@. دي بتحول الكلاس إلى Component أو Module.
import { NgModule, Component } from "@angular/core";
@Component({
selector: "my-component",
template: "<div>Class decorator</div>",
})
export class MyComponent {
constructor() {
console.log("Hey I am a component!");
}
}
@NgModule({
imports: [],
declarations: [],
})
export class MyModule {
constructor() {
console.log("Hey I am a module!");
}
}
هنا بيحول الكلاس MyComponent إلى Component بواجهة HTML مرتبطة بيه.
بيحول الكلاس MyModule إلى Module عشان يقدر يشتغل في التطبيق.
بتستخدم لتزيين الخصائص جوا الكلاس. أشهر الأمثلة هي Input@ و Output@ اللي بيتحكموا في تبادل البيانات بين الـ Components.
import { Component, Input } from "@angular/core";
@Component({
selector: "my-component",
template: "<div>Property decorator</div>",
})
export class MyComponent {
@Input()
title: string;
}
بتستخدم لتزيين (methods) جوا الكلاس، زي HostListener@ اللي بيتفاعل مع أحداث معينة زي الـ click أو hover.
import { Component, HostListener } from "@angular/core";
@Component({
selector: "my-component",
template: "<div>Method decorator</div>",
})
export class MyComponent {
@HostListener("click", ["$event"])
onHostClick(event: Event) {}
}
بتستخدم لتزيين Parameters اللي جوا الـ Constructor. زي Inject@ و Optional@ اللي بيتحكموا في كيفية حقن القيم أو الخدمات جوا الكلاس.
import { Component, Inject } from "@angular/core";
import { MyService } from "./my-service";
@Component({
selector: "my-component",
template: "<div>Parameter decorator</div>",
})
export class MyComponent {
constructor(@Inject(MyService) myService) {
console.log(myService); // بيتم حقن MyService
}
}
Angular CLI is a command line interface to scaffold and build Angular apps using nodejs style (commonJs) modules. You need to install using the npm command,
npm install @angular/cli@latest
i. Creating New Project:
ng new
ii. Generating Components, Directives & Services:
ng generate/g
The different types of commands would be:
- ng generate class my-new-class: add a class to your application
- ng generate component my-new-component: add a component to your application
- ng generate directive my-new-directive: add a directive to your application
- ng generate enum my-new-enum: add an enum to your application
- ng generate module my-new-module: add a module to your application
- ng generate pipe my-new-pipe: add a pipe to your application
- ng generate service my-new-service: add a service to your application
iii. Running the Project:
ng serve
الـ constructor هو زي "function" اللي بتشتغل أول ما تعمل نسخة جديدة من (class) في TypeScript. يعني مثلاً لما تيجي تعمل Component جديد، أول حاجة بتحصل هي إن الـ constructor بيشتغل.
وظيفته الأساسية إنه يجهز المتغيرات بتاعت الكلاس ويظبط حاجة اسمها Dependency Injection، ودي اللي بتخلي Angular يقدر يحط الـ services اللي محتاجها الكلاس بسهولة.
المهم إنك تستخدم الـ constructor عشان تجهز الحاجات الأساسية بتاعت الكلاس بس، يعني متعملش فيه أي "شغل" تقيل زي استدعاء دوال أو التعامل مع بيانات أو حاجات معقدة. الأفضل إنك تسيب الحاجات دي للـ ngOnInit لأن ده بيكون وقت مناسب أكتر عشان تبدأ الشغل الفعلي.
باختصار، خلي الـ constructor لتهيئة الحاجات الأساسية، من غير ما تعمل فيه حاجات كتير.
هو واحد من الـ lifecycle hooks اللي Angular بتشغله بعد ما تكون خلصت إنشاء الـ Component وعرضته قدام المستخدم. يعني ببساطة، بيشتغل بعد الـ constructor.
عادة بنستخدم الـ ngOnInit لما نكون عايزين نجهز أو نعمل تهيئة لحاجات معينة زي البيانات أو لو عايزين نجيب بيانات من API، أو حتى لو في حاجات مربوطة (bindings) محتاجين نشتغل عليها.
الـ ngOnInit بيكون أنسب مكان تبتدي فيه الشغل التقيل، زي استدعاء بيانات أو التعامل مع الحاجات اللي جاية من بره الـ Component، لأن في الوقت ده بيكون Angular ظبط كل الـ bindings واتأكد إن البيانات اللي جايالك من بره (زي المتغيرات اللي بتجي من الـ parent component) جاهزة.
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
@Injectable({
providedIn: "root",
})
export class RepoService {
constructor(private http: Http) {}
fetchAll() {
return this.http.get("https://api.github.com/repositories");
}
}
import { Component, OnInit } from "@angular/core";
import { RepoService } from "./repo.service";
@Component({
selector: "app-repo-list",
template: `
<ul>
<li *ngFor="let repo of repos">{{ repo.name }}</li>
</ul>
`,
})
export class RepoListComponent implements OnInit {
repos: any[] = [];
constructor(private repoService: RepoService) {}
ngOnInit() {
this.repoService.fetchAll().subscribe((data) => {
this.repos = data;
});
}
}
بدل ما الكلاس يبقى مسؤول عن إنشاء كل حاجة بنفسه ، Angular بتشوف هو محتاج إيه، وتقوم تروح تجيبها وتحطها له جاهزة، عن طريق حاجة اسمها الـ Injector.
تنظيم الكود: بيساعد إن الكلاس مايبقاش مضطر ينشئ الحاجات اللي محتاجها بنفسه، وده بيفصل بين منطق الكود والحاجات اللي بيحتاجها.
إعادة الاستخدام: لو عندك service بتعمل حاجة معينة، تقدر تستخدمها في أكتر من Component بسهولة.
تسهيل الاختبارات: لما تيجي تختبر الكود، تقدر تستبدل الـ service الأصلية بحاجة وهمية بسهولة وتعمل اختباراتك من غير مشاكل.
تخيل إن عندك Observable بيرجع الوقت الحالي كل ثانيتين، وانت عايز تعرضه في الـ HTML بتاعك. باستخدام الـ Async Pipe، هتقدر تعمل ده من غير أي تعقيدات.
@Component({
selector: "async-observable-pipe",
template: `<div>
<code>observable|async</code>: Time: {{ time$ | async }}
</div>`,
})
export class AsyncObservablePipeComponent {
time$: Observable<string>;
constructor() {
this.time$ = new Observable((observer) => {
setInterval(() => {
observer.next(new Date().toString());
}, 2000);
});
}
}
الـ Async Pipe بيقوم بالاشتراك في الـ Observable أو الـ Promise وبيجيب أحدث قيمة منه ويعرضها. كل ما الـ Observable يبعث قيمة جديدة، الـ Pipe بيحدث القيمة في الـ View تلقائي.
<li *ngFor="let user of users">
{{ user }}
</li>
خلينا نوضح بمثال عشان الصورة تكون أوضح. لو عندك component معمول في Angular وعايز تعرض تاريخ ميلاد معين (birthday) بس في شكل يكون سهل للفهم أو "human-friendly"، هنا ممكن تستخدم حاجة اسمها "date pipe". الـpipe دي هتخلي التاريخ يظهر بطريقة متنسيقة زي ما المستخدم ممكن يتوقع.
import { Component } from "@angular/core";
@Component({
selector: "app-birthday",
template: `<p>Birthday is {{ birthday | date }}</p>`,
})
export class BirthdayComponent {
birthday = new Date(1987, 6, 18); // June 18, 1987
}
اللي بيحصل هو إن التاريخ اللي مخزن جوا الـbirthday هيظهر للمستخدم في شكل تاريخ مفهوم بسهولة، حسب الفورمات اللي Angular بيستخدمه افتراضيًا لعرض التواريخ.
Birthday is Jun 18, 1987
خلينا نوضح الموضوع بمثال بسيط زي اللي انت جبته عن عيد الميلاد. في الكود اللي انت كتبته، بنستخدم الـ date pipe عشان نعرض تاريخ معين لكن بالصيغة اللي احنا عايزنها (يوم/شهر/سنة).
import { Component } from "@angular/core";
@Component({
selector: "app-birthday",
template: `<p>Birthday is {{ birthday | date : "dd/MM/yyyy" }}</p>`, // 18/06/1987
})
export class BirthdayComponent {
birthday = new Date(1987, 6, 18);
}
لو الـ pipe بيقبل أكتر من باراميتر، هتفصلهم بنفس الطريقة عن طريق colons.
الفكرة ببساطة انك بتبدأ من القيمة الأولانية وبتعديها على أول pipe، وبعد ما الـ pipe الأول يخلص، النتيجة بتاعت الـ pipe الأول بتروح للـ pipe اللي بعده، وهكذا لغاية ما يخلصوا كل الـ pipes اللي انت حاططهم.
في المثال بتاعك، انت عندك خاصية birthday اللي هي تاريخ الميلاد:
birthday = new Date(1987, 6, 18);
اللي بيحصل هنا: الdate pipe: بيحول قيمة التاريخ ويعرضها بشكل معين. انت استخدمت الفورمات 'fullDate'، فالتاريخ هيظهر كامل زي "Thursday, June 18, 1987".
الuppercase pipe: بياخد النتيجة اللي طلعت من الـ date pipe وبيحولها لحروف كبيرة، فالناتج النهائي هيكون: "THURSDAY, JUNE 18, 1987".
<p>Birthday is {{ birthday | date:'fullDate' | uppercase }} </p>
أول حاجة لازم تعملها لما تيجي تكتب "custom pipe" هي إنك تستخدم الـ @Pipe ديكوريتور اللي بتجيبها من الـ core Angular.
@Pipe({name: 'myCustomPipe'})
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({ name: "multiply" })
export class MultiplyPipe implements PipeTransform {
transform(value: number, multiplier: number): number {
return value * multiplier;
}
}
{{ 5 | multiply: 2 }} // 10
يتنفذ بس لما يكون في تغيير في القيمة أو في الـ parameters اللي اتبعتت للـ pipe. يعني لو عندك قيمة primitive زي String أو Number أو Boolean، ولو اتغيرت القيمة دي، أو لو كان فيه تغيير في الـ object reference زي Date أو Array أو Function أو Object. ده بيخلي الـ pure pipe أكتر كفاءة لأنها مش بتتنفذ كتير، وبتشتغل بس لما يكون في حاجة اتغيرت فعلاً.
يتنفذ في كل دورة تغيير (change detection cycle) بغض النظر إذا كانت القيمة أو الـ parameters اتغيرت ولا لأ. يعني ممكن الـ impure pipe يتنفذ كتير جداً، زي كل حركة مفتاح (keystroke) أو حركة ماوس (mouse-move). ده ممكن يؤدي إلى أداء أبطأ لو استخدمته في أماكن كتير في التطبيق.
طيب، هو موجود فين؟ تقدر تضيفه في التطبيق بتاعك عن طريق تعمل import لل HttpClientModule من الباكدج اللي اسمها angular/common/http@ بالطريقة دي:
import { HttpClientModule } from "@angular/common/http";
تقدر تختبر الكود بتاعك بسهولة لما تستخدمه في الـ HTTP requests. تقدر تحدد أنواع البيانات اللي طالعين من وإلى الـ backend، وده بيخليك متأكد إنك شغال على البيانات الصح. بيعمل intercept للrequest and response: يعني ممكن تتحكم في الطلبات اللي طالعة من التطبيق بتاعك أو الاستجابات اللي راجعة قبل ما توصل. ده بيساعد في حاجات زي إضافة headers أو التعامل مع الـ tokens. بيدعم Observable APIs: لو متعود على الـ Observables في Angular، هتلاقي الـ HttpClient بيشتغل معاهم حلو جداً، وده بيخليك تستفيد من حاجات زي الـ RxJS.
✨ مثال على كده، ممكن يكون عندك component مش عارف هيتعرض فين بالضبط، زي في Dialog، Popup، أو حتى جزء من الصفحة حسب تفاعل المستخدم أو رد من API. فبتستخدم الـ Dynamic Components عشان تضيف الـ component دي بشكل ديناميكي بناءً على اللي بيحصل في التطبيق وقت التشغيل.
ده بيكون مفيد جدًا لو عاوز تعمل تصميم مرن أو تعرض components معينة بناءً على ظروف أو اختيارات معينة للمستخدم.
دي عبارة عن directives بس معاها template. يعني بتتعامل كأنها حتة UI كاملة، زي ما تكون واجهة المستخدم اللي انت بتعرضها. الكمبوننتس دي بتحتوي على html و logic بتاعها. يعني تقدر تعتبرها مزيج ما بين شكل الصفحة والسلوك اللي فيها.
دي بتشتغل على الـ DOM. بتغير في الـ layout بتاع الصفحة، يعني تقدر تضيف أو تشيل عناصر من الصفحة نفسها. مثال على كده عندنا الـ *ngIf أو الـ *ngFor اللي بتخلينا نتحكم في إذا كان العنصر يتعرض أو يتخفى أو يتكرر بناءً على شروط معينة.
النوع ده بيغير في شكل العنصر أو في الـ behavior بتاعه، زي الـ ngClass أو الـ ngStyle. دي بتتحكم في الخصائص أو الـ styles بتاعت العنصر اللي موجود في الـ DOM من غير ما تشيله أو تضيف عناصر جديدة.
يعني إيه؟ يعني لو إنت مثلاً فاتح صفحة "Home" وعايز تروح لصفحة "About"، الـ router ده هو اللي بيعمل التنقل ده. كل ما تتنقل من صفحة لصفحة، بيحصل شوية أحداث.
١. NavigationStart: ده أول ما تبدأ تتحرك من صفحة للتانية، يعني كده بنقول إنك "بدأت التنقل".
٢. NavigationEnd: لما التنقل يخلص وتوصل للصفحة اللي رايح لها، الـ router بيقولك "التنقل خلص بنجاح".
٣. NavigationCancel: لو حصل حاجة وانت في النص، مثلاً أنت لغيت أو فيه مشكلة، يبقى كده التنقل "اتلغى".
٤. NavigationError: لو حصلت مشكلة وانت بتتنقل، زي مثلاً الصفحة مش موجودة أو السيرفر وقع، هنا بنقول "فيه خطأ".
٥. RoutesRecognized: هنا بقى الـ router بيقولك "أيوه أنا عرفت الصفحة اللي انت رايح لها ومستعد أروح لها".
✨الفكرة إنك ممكن تستفيد من الأحداث دي، زي إنك مثلاً تشغل loader (الحاجة اللي بتفضل تلف وانت مستني الصفحة تفتح) أول لما يبدأ التنقل، وبعد ما يخلص تخفيه. أو ممكن تعمل check لو فيه أخطاء في النص عشان تبلغ المستخدم إن في مشكلة.
يعني باختصار، الـ Router Events دي بتديك سيطرة على كل خطوة بتحصل وانت بتتنقل بين صفحات التطبيق.
const appRoutes: Routes = [
{ path: 'todo/:id', component: TodoDetailComponent },
{
path: 'todos',
component: TodosListComponent,
data: { title: 'Todos List' }
},
{ path: '',
redirectTo: '/todos',
pathMatch: 'full'
},
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true } // <-- debugging purposes only
)
// other imports here
],
...
})
export class AppModule { }
{ path: '**', component: PageNotFoundComponent }
دي معناها إن أي رابط ما اتعرفش في باقي المسارات هيتم التقاطه من المسار ده. الـ ** هو رمز الـ wildcard اللي بيمسك أي URL ما ينطبقش عليه أي من المسارات اللي قبله.
الPageNotFoundComponent: هنا بتحدد الـ component اللي هيتعرض لما المسار يتحقق. في الحالة دي، صفحة الخطأ أو الصفحة اللي بتقول إن الرابط مش موجود. ✨الخلاصة: الـ Wildcard route بيضمن إن التطبيق مش هيكراش بسبب روابط غير معروفة وبيسمحلك تعرض رسالة أو صفحة مخصصة بدل من حدوث خطأ.
أداء أسرع: بدل ما المستخدم يقعد مستني لما الصفحة تتحمل بالكامل، السيرفر بيرسل له HTML جاهز أول ما يفتح التطبيق، فبيحس إن الصفحة ظهرت بسرعة.
تحسين الـ SEO: عشان محركات البحث زي جوجل تفهم محتويات الصفحات، لازم تلاقي HTML كامل أول ما تزور الصفحة. الـ SPA (Single Page Applications) زي Angular ممكن يكون عندها مشكلة في النقطة دي لأنها بتحتاج الجافاسكريبت يتنفذ الأول عشان يطلع المحتوى. لكن مع Angular Universal، الصفحة بتطلع بـ HTML جاهز، فمحركات البحث تقدر تقرأها وتحسن ترتيب الموقع.
تجربة أحسن على الشبكات البطيئة: المستخدمين اللي عندهم إنترنت بطيء هيتبسطوا إنهم يشوفوا الصفحة بسرعة من غير ما يقعدوا مستنيين تحميل كل حاجة.
🔴 لو فكرت فيها، الـ Input@ بيسمح للـ Parent إنه يمرر بيانات للـ Child Component، بس لو حبينا نمرر حاجة زي HTML elements أو محتوى فيه CSS معين، الـ Input@ مش حينفع. وده هنا بييجي دور ng-content.
لو عندك Button Component بسيط، هيعرض زراير، وعايز مثلاً تخلي النص اللي جواه "Click Me"، تقدر تعمل كده بإنك تضيف Click Me جوة الكومبوننت.
لكن لو عايز تخلي الـ Parent هو اللي يقرر يكتب إيه جوة الزرار أو يضيف HTML مختلف، بنستخدم ng-content.
في المثال ده، بدل ما تحط "Click Me" جوة Click Me، هنحط مكانه. وده معناه إن المحتوى اللي بين في الـ Parent Component حيتعرض جوة الزرار.
@Component({
selector: "app-fancybtn",
template: `<button><ng-content></ng-content></button>`,
})
export class FancyBtnComponent {}
<app-fancybtn>Click Me</app-fancybtn>
<app-fancybtn><b>Submit</b></app-fancybtn>
لو عندك كومبوننت زي Card Component وفيه أجزاء زي header وcontent وfooter، تقدر تدي لكل جزء ng-content خاص بيه باستخدام select attribute. بالشكل ده تقدر تمرر محتويات مختلفة للـ Parent والـ Child هيعرضهم في الأماكن الصح.
@Component({
selector: "app-card",
template: `
<div class="card">
<div class="header"><ng-content select="header"></ng-content></div>
<div class="content"><ng-content select="content"></ng-content></div>
<div class="footer"><ng-content select="footer"></ng-content></div>
</div>
`,
})
export class CardComponent {}
<app-card>
<header><h1>Angular</h1></header>
<content>One framework. Mobile & desktop.</content>
<footer><b>Super-powered by Google</b></footer>
</app-card>
export class CustomerDetailComponent implements OnInit {
@Input() customer: Customer;
}
<app-customer-detail [customer]="selectedCustomer"></app-customer-detail>
لما تيجي تتعامل مع Output@، ده معناه إنك بتبعت أحداث للـ parent component. يعني مثلا لو الـ child component عايز يقول للـ parent component إن العميل اتعدل، هنعمل كالتالي:
@Output() customerChange: EventEmitter<Customer> = new EventEmitter<Customer>();
update() {
this.customerChange.emit(this.customer);
}
🔴 في الـ parent component، نقدر نسمع الحدث ده باستخدام كود زي ده
<app-customer-detail [customer]="selectedCustomer" (customerChange)="update($event)"></app-customer-detail>
Input@ بتسمح لك بإنك تحدد اسم اختياري، وده بيبقى مفيد لما تحب تسمي الخاصية حاجة في الكود واسم تاني للـ binding في الـ HTML.
@Input('customerData') customer: Customer;
تقدر تعمل setter للخاصية اللي عليها Input@ عشان تضيف أي منطق إضافي أو تتحكم في التغيير قبل ما يتم تعيين القيمة.
private _customerData: Customer;
@Input()
set customer(customer: Customer) {
this._customerData = customer;
console.log("Updated customer data:", this._customerData);
}
get customer(): Customer {
return this._customerData;
}
الAngular بيوفر ngOnChanges كـ lifecycle hook، وده بيسمح لنا نراقب أي تغيير في الخصائص اللي عليها Input@ في أي وقت.
import { OnChanges, SimpleChanges } from "@angular/core";
export class CustomerDetailComponent implements OnChanges {
@Input() customer: Customer;
ngOnChanges(changes: SimpleChanges) {
if (changes["customer"]) {
console.log("Customer changed:", changes["customer"].currentValue);
}
}
}
الEventEmitters في Angular هي Observable زي RxJS Subjects، وده بيسمح باستخدام مشغلين RxJS عشان تتلاعب بالأحداث، مثلا تقدر تعمل debounceTime، filter، وغيره على الأحداث اللي بتنبعث.
لبيانات من نوع Object وArray بتنتقل حسب Reference يعني أي تغيير فيها بيعدل النسخة الأصلية. عشان كده لما تبعت بيانات من الـ Parent للـ Child، لو عدلتها في الـ Child، هتتغير تلقائيًا في الـ Parent. البيانات من نوع Primitive زي number و string بتتنقل حسب القيمة، فالتغيير على القيمة مش بيأثر على النسخة الأصلية في الـ Parent.
نقدر نعرّفها باستخدام علامة # متبوعة باسم المتغير.
عنصر HTML
<input type="text" #firstName>
<app-customer #customerList="customer"></app-customer>
في المثال ده، بنعرف متغيرات firstName# و#lastName
<h1>Welcome</h1>
<input type="text" #firstName placeholder="First Name" (keyup)="0">
<input type="text" #lastName placeholder="Last Name" (keyup)="0">
<b>Welcome {{ firstName.value }} {{ lastName.value }}</b>
2- Change Detection بيحدث (view) مباشرة ويعرض القيمة الجديدة اللي كتبتها في الinput
لو شيلنا (keyup) من الكود، ممكن ما يتم تحديث جملة الترحيب بالسرعة اللي بنتوقعها لأن Angular يعتمد على الكشف عن التغييرات فقط عند وقوع حدث غير متزامن أو عند أي حدث متعلق بالكشف عن التغييرات.
الميزة الرئيسية للـ ng-container إنه بيخلينا نضيف حاجات معينة زي شروط أو تكرارات (زي *ngIf أو *ngFor) من غير ما نضطر نضيف عنصر إضافي في DOM، واللي ممكن يكون ليه تأثير سلبي على الأداء، أو حتى يكون مزعج لو بتعدل في الـ CSS.
<h1> ng-Container</h1>
<p>Hello world!</p>
<ng-container>
المحتوى بتاع الكونتينر.
</ng-container>
<h1> ng-Container</h1>
<p>Hello world!</p>
المحتوى بتاع الكونتينر.
تخيل إن عندك قائمة فيها مجموعة من العناصر وعايز تعرض بس العناصر النشطة (اللي Active)، فهتستخدم ngFor عشان تعمل لوب على العناصر، وngIf عشان تتحقق إذا كان العنصر نشط ولا لأ.
لو استخدمت عنصر زي span مثلا عشان تحط الـ *ngFor هتلاقي إنه بيضيف عنصر span جديد في DOM لكل عنصر، وده ممكن يخلي الكود أطول وممكن يأثر على الـ CSS. لكن لو استخدمت ng-container، هتتجنب الحكاية دي
بدون ng-container
<ul>
<span *ngFor="let item of items;">
<li *ngIf="item.active">
{{item.name}}
</li>
</span>
</ul>
<ul>
<ng-container *ngFor="let item of items;">
<li *ngIf="item.active">
{{item.name}}
</li>
</ng-container>
</ul>
بدون ng-container
<div *ngIf="items1">
<div *ngFor="let item of items1;">
{{item.name}}
</div>
</div>
<ng-container *ngIf="items1">
<div *ngFor="let item of items1;">
{{item.name}}
</div>
</ng-container>
<h2>Defining a Template using ng-Template</h2>
<ng-template>
<p> Say Hello</p>
</ng-template>
ال ngTemplateOutlet: دي Directive بتخليك تعرض الـ template في مكان معين.
الأول بنعمل الـ template ونحط عليه اسم علشان نقدر نرجعله:
<ng-template #sayHelloTemplate>
<p>Say Hello/p>
</ng-template>
<ng-container *ngTemplateOutlet="sayHelloTemplate">
This text is not displayed
</ng-container>
Say Hello
لو محتاج تتحكم في عرض الـ template من جوه TS؟ بتاع الـ Component، ممكن تستخدم حاجة اسمها TemplateRef و ViewContainerRef.
الTemplateRef: ده عبارة عن object بيحتوي على الـ template نفسه (اللي موجود جوه ng-template). بنستخدمه عشان نمسك الـ template ونتحكم فيه من الكود.
الViewContainerRef: ده عبارة عن مكان في الـ DOM (الصفحة) تقدر تعرض فيه الـ template في أي وقت. هو بيحدد المكان اللي الـ template هيظهر فيه، وده بيبقى مفيد لما تكون عاوز تضيف أو تحذف أجزاء من الـ DOM بشكل ديناميكي.
@ViewChild('sayHelloTemplate', { read: TemplateRef }) sayHelloTemplate: TemplateRef<any>;
this.vref.createEmbeddedView(this.sayHelloTemplate);
import {
Component,
ViewChild,
TemplateRef,
ViewContainerRef,
AfterViewInit,
} from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent implements AfterViewInit {
@ViewChild("sayHelloTemplate", { read: TemplateRef })
sayHelloTemplate: TemplateRef<any>;
constructor(private vref: ViewContainerRef) {}
ngAfterViewInit() {
this.vref.createEmbeddedView(this.sayHelloTemplate);
}
}
علشان نتاكد إن الـ template بقى جاهز واتحمل في الصفحة، لأن لو جربنا نستخدمه مباشرة في ngOnInit ممكن ميكنش اتحمل بالكامل.
لما تستخدم ngIf، الAngular بيحولها تلقائيًا لـ ng-template (behind the scenes). يعني بدل ما تكتب الكود بالشكل المعتاد
<div *ngIf="selected">
<p>You are selected</p>
</div>
<ng-template [ngIf]="selected">
<div>
<p>You are selected</p>
</div>
</ng-template>
الAngular لما بيشوف *ngIf بيترجمها تلقائيًا لـ ng-template بحيث لما الشرط يكون true، يعرض اللي جواه، ولو الشرط false، مش هيعرض أي حاجة. فبكده ng-template بيدي مرونة أكبر للتحكم في أماكن العرض وشروطه.
في حالات ممكن تحتاج تتحكم في طريقة العرض بشكل أوسع من مجرد *ngIf. مثلاً، لو عاوز تعمل أجزاء تانية تظهر لو الشرط كان false، زي استخدام الـ then و else.
لو عندك سيناريو فيه محتوى عاوز تعرضه لما الشرط يكون true، ومحتوى تاني لما الشرط يكون false، تقدر تعملها كده
<div *ngIf="selected; then thenBlock else elseBlock"></div>
<ng-template #thenBlock>
<p> You are selected</p>
</ng-template>
<ng-template #elseBlock>
<p> You are selected </p>
</ng-template>
تحديد Template: بنبدأ بكتابة Template باستخدام ng-template، وده بيكون مجرد تصميم مش بيظهر لوحده. لازم نحدد Reference للـ Template ده زي #template1.
<ng-template #template1>
<p> Template </p>
</ng-template>
<ng-container *ngTemplateOutlet="template1"></ng-container>
<ng-template let-value="value" #messageTemplate>
<p>القيمة اللي وصلت من الـ Parent هي {{value}}</p>
</ng-template>
<ng-container [ngTemplateOutlet]="messageTemplate" [ngTemplateOutletContext]="{value:'1000'}"></ng-container>
في Angular، استخدام implicit بيسمحلك تمرر قيمة افتراضية لأي متغير محلي (local variable) في ng-template من غير ما تعينها مباشرة للمتغير ده. ببساطة، لو عينت قيمة للـ implicit في ngTemplateOutletContext، المتغيرات اللي ملهاش قيمة صريحة هتاخد القيمة اللي عينتها لـ $implicit.
<ng-template let-name let-message="messageText" #welcomeTemplate>
<p>hello {{name}}، {{message}}</p>
</ng-template>
<ng-container
[ngTemplateOutlet]="welcomeTemplate"
[ngTemplateOutletContext]="{$implicit: 'mohamed', messageText: 'welcome'}">
</ng-container>
<ng-container
[ngTemplateOutlet]="welcomeTemplate"
[ngTemplateOutletContext]="{$implicit: 'hassan', messageText: 'welcome'}">
</ng-container>
let-message="messageText": هنا بنعرّف متغير محلي تاني اسمه message داخل نفس الـ Template، والمتغير ده هياخد قيمته من messageText اللي بنمرره في ngTemplateOutletContext.
فكرتها إنها بتسهّل متابعة أي تغييرات بتحصل على البيانات اللي بنعتمد عليها. يعني، لما مثلاً تبقى عندك واجهة بتظهر (Counter) بيزيد، ممكن تعمل الـ"counter" ده كـ"Signal". لما قيمة العداد تتغير، أي مكان بيستخدم القيمة دي هيعرف فوراً إن فيه تغيير حصل.
النوع ده تقدر تغيره بنفسك. يعني تقدر تزود القيمة بتاعته، تنقصها، أو تعدلها بأي شكل.
const count = signal(0);
تقدر تستخدم ()set لو عايز تدي قيمة جديدة مباشرة للـ"signal"
count.set(3); // كده القيمة بقت 3
تقدر كمان تستخدم ()update علشان تحسب قيمة جديدة بناءً على القيمة الحالية. زي إنك تزود القيمة بواحد:
count.update((value) => value + 1); // كده القيمة بتزيد 1
النوع ده بيكون للقراءة بس، مش هتقدر تعدل فيه مباشرة. وده بيبقى مفيد في الحالات اللي بتعتمد على حسابات من "signals" تانية، زي لما يبقى عندك مثلاً "double counter" اللي بيعتمد على قيمة "counter" تاني، فبيتحسب تلقائي.
✨ فالـ"Signals" دي بتبسط الموضوع لما يبقى عندك تطبيق معقد، لأن Angular بيتابع أي تحديثات بتحصل في "signals" تلقائياً ويرندر بس الأماكن اللي فعلاً اتغيرت، مش كل الصفحة.
أما الـ"Computed Signals" فهي نوع تاني من "signals" لكن بتبقى للقراءة فقط (read-only)، لأنها بترتبط بقيم signals أخرى وبتحسب قيمة جديدة بناءً عليها. علشان تعمل "computed signal"، بتستخدم دالة ()computed وبتكتب طريقة الحساب بناءً على signals تانية.
const count = signal(0);
const doubleCount = computed(() => count() * 2);
الـ"computed signals" بتحسب القيمة بس لما تحتاج تقراها لأول مرة. القيمة بعد كده بتبقى محفوظة (cached)، يعني لما تقراها تاني، Angular مش هيحسبها من الأول، إلا لو في حاجة اتغيرت.
بتتابع بس الsignals اللي تم قراءتها أثناء الحساب. فلو مثلاً عندنا شرط بيحدد إذا كانت القراءة من count أو لأ، الـ"computed signal" هيتابع count بس في حالة تحقق الشرط، وإلا مش هيتابعها.
الـ"computed signals" مش بتقبل التعديل المباشر باستخدام .()set أو .()update لأنها للقراءة بس، وده بيساعد في تأكيد إنه يتم تحديثها تلقائيًا بناءً على signals اللي بتعتمد عليها.
لما بنعمل متغير عادي، لو غيرنا قيمته مش بيأثر في الواجهة مباشرة، وعلشان يحصل ده محتاجين نعمل "تحديث" يدوي. إنما مع الـSignal، لما بتغير القيمة بشكل صحيح، التغيير ده بينعكس تلقائي على الواجهة اللي بتعتمد على القيمة دي.
import { signal } from "@angular/core";
const numberSignal = signal(10); // هنا بنخزن قيمة 10 كعدد
🔴 لو حاولنا نغير القيمة كده:
❌
numberSignal = 20;
بنستخدم ()set عشان نحدث القيمة:
numberSignal.set(20); // كده بنغير القيمة بطريقة صح
إزاي نعملها:
const userSignal = signal({ name: "Ali", age: 25 });
userSignal().name = "Ahmed";
userSignal.set({ ...userSignal(), name: "Ahmed" });
إزاي نعملها:
const numbersArraySignal = signal([1, 2, 3]);
numbersArraySignal().push(4);
numbersArraySignal.set([...numbersArraySignal(), 4]); // بنعيد تعيين الأراي كلها
numbersArraySignal.update((numbers) => [...numbers, 4]);
إزاي نعملها:
const usersArraySignal = signal([
{ name: "Ali", age: 25 },
{ name: "Mona", age: 30 },
]);
//❌ ده غلط
usersArraySignal()[0].name = "Hassan"; // هنا بنغير الأراي مباشرة وده غلط
usersArraySignal.update((users) =>
users.map((user) =>
user.name === "Ali" ? { ...user, name: "Hassan" } : user
)
);
الـ"Signals" بتقدم طريقة أبسط وأسرع للتعامل مع التغيرات في البيانات من غير ما تضطر تستخدم Observables وطرق معقدة. مثلاً لو عندك قيمة معينة عايزها تتغير وتؤثر على جزء معين في UI ممكن تحطها كـ"Signal". فلما القيمة دي تتغير، كل عنصر أو جزء بيعتمد عليها هيعرف بشكل تلقائي، وده من غير ما ترجع تشغل "change detection" لكل التطبيق.
الـ"signals" ليها تأثير كبير على عملية change detection في Angular، وده لأنها بتساعد في تحسين الأداء وتبسيط عملية تحديث البيانات بشكل مباشر.
لما بنستخدم الـ"signals"، التحديث بيكون على مستوى كل جزء (أو "Component") بشكل مستقل، بمعنى إن أي جزء بيستخدم قيمة معينة من "signal" هيتم تحديثه بس لو حصل تغيير في القيمة دي، من غير ما يضطر Angular يشغل عملية change detection لكل التطبيق. فده بيوفر كفاءة أكبر في التطبيقات الكبيرة، ويقلل من كمية الأكواد المطلوبة للتحديث.
لو بتستخدم استراتيجية "OnPush" للتحديثات في Angular، فالـ"signals" بتتكامل معاها بشكل ممتاز، لأنها بتسمح بتحديث القيم اللي بتتأثر بشكل مباشر من غير ما يتم تحديث كل شيء. لما يكون عندك جزء معين بيعتمد على "signal"، و"OnPush" مفعل، الواجهة هتتحدث تلقائيًا لما تتغير قيمة الـ"signal" من غير تدخل أو إعادة حسابات غير ضرورية.
مع الـ"signals"، Angular مش بيحتاج يشغل كل الـ change detection cycles، لأن الـ"signal" بيبعت إشعار بإن التغيير حصل بس للأجزاء اللي بتعتمد على القيمة دي. فبكده بنقلل من الضغط على الذاكرة والبروسيسور، وده بيساعد في تحسين أداء التطبيق عمومًا.
الـ"signals" بتتيح طريقة بسيطة للتعامل مع القيم اللي بتتغير بشكل متكرر، زي مثلاً counter أو حالة معينة. باستخدام signals، ممكن تتحكم في عملية التحديث من غير ما تحتاج تكتب أكواد إضافية لمتابعة التغييرات، وده بيسهل على المطورين التعامل مع الـUI بشكل ديناميكي وفعال.
فايدة الـ"signals" الأساسية في الـchange detection هي إنها بتخلي التحديثات أكتر تركيزًا وأقل حملًا على النظام، وده بيحسن من استجابة التطبيق بشكل كبير وبيوفر وقت وجهد كبير على المطورين، خاصةً في التطبيقات اللي فيها بيانات كتير بتتحدث باستمرار.
الـ Observables تعتبر أداة قوية في Angular بتخلينا نتحكم في البيانات المتغيرة على مدار الوقت، زي لما بيجيلك تحديثات لبيانات التطبيق من API أو WebSocket. يعني لو عندك حاجة بتتغير بانتظام (زي رقم بيزيد كل ثانية)، تقدر تستخدم Observable علشان تتابع التغييرات دي.
فكرة الـ Observable هي إنها بتطلع القيم الجديدة للبيانات وتبعت إشارات للتحديثات دي للتطبيق، وده بيحصل على مدار الوقت، مش مرة واحدة وخلاص. باستخدام الـ “async pipe” ممكن تخلي القيم تظهر في الـ UI مباشرةً. بس العيب إن استخدام الـ Observables أحياناً بيكون معقد، وخصوصًا لما بتجمع أكتر من مصدر للبيانات أو لما تستخدم عمليات معقدة كتير من RxJS، اللي ممكن تخلي الكود صعب يتفهم أو يتتبع، وممكن كمان يعمل مشاكل في الأداء والذاكرة لو مش متظبط صح.
الـ Signals بقت جزء من Angular من أول الإصدار 16، وبتقدم طريقة جديدة وأبسط في التعامل مع البيانات المتغيرة. الفكرة ببساطة إنك بدل ما تستخدم Observable وتتعامل مع التعقيد بتاع RxJS، تقدر تستخدم Signal لتحديث البيانات. الميزة هنا إنها بتسمحلك تتابع التغيرات بطريقة أبسط وأكتر تحكم، وده بيفيد في حاجات كتير خاصة في التطبيقات الكبيرة اللي فيها تفاصيل كتير عايزة تحديث بانتظام من غير ما تعمل عبء كبير على الأداء.
الـ observables بتكون مناسبة جدًا في حالة البيانات اللي بتيجي على شكل تدفق مستمر زي الداتا اللي جايه من API، إشعارات الـ real-time، أو العمليات اللي بتم بشكل غير متزامن. يعني لو عندك service بتجيب بيانات كل شوية أو بتتغير كل شوية زي إشعارات أو حاجة بتتحدث بانتظام، فالأفضل هنا إنك تستخدم observable.
لازم تشترك subscribe في الـ observable عشان تاخد البيانات، ولما تخلص بتحتاج تعمل unsubscribe عشان تقفل الاتصال وتوفر موارد. كمان في الـ observables تقدر تعمل حاجات زي التصفية، الترتيب، والتعديل باستخدام الـ pipe والـ operators، ودي بتساعدك تعمل معالجة للداتا اللي جاية بشكل مرن وسهل.
الـ signals في Angular بتكون أفضل في الجزء الخاص بالـ UI، يعني الجزء اللي بيتعامل مع UI أو components بشكل مباشر. لو عندك بيانات عايز تعرضها وتتغير بشكل ديناميكي مع كل تعديل، الـ signals هتكون مناسبة.
مش بتحتاج تعمل subscribe أو unsubscribe زي الـ observable؛ هنا بتستخدم set عشان تغير القيمة أو update عشان تحدث القيمة بناءً على الحالة الحالية.
كمان تقدر تستخدم حاجة اسمها computed signals اللي بتسمح لك تعمل إشارات جديدة بتعتمد على إشارات موجودة فعلاً، ودي مفيدة لما يكون عندك أكتر من عنصر بيعتمد على داتا واحدة. استخدام الـ signals هيكون مناسب لو التصميم بتاع الـ template بيعتمد على تغيير لحظي، عشان تتأكد إن components بتتحدث مع كل تعديل في الداتا بدون لخبطة أو مشاكل.
الفكرة هي إن الـEffect بيسجل قيمة الـSignal اللي بيتتبعها، ولما يحصل تغيير في قيمة الـSignal دي، الـEffect ده بيشتغل تاني ويعمل العمليات المطلوبة.
يعني مثلا لو عندك عداد وعايز تسجل القيمة كل مرة تتغير فيها، ممكن تستخدم الـEffect كالتالي
effect(() => {
console.log(`The current count is ${count()}`);
});
مش مطلوب إنك تستخدم الـEffects دايمًا، لكن فيه شوية حالات معينة ممكن تحتاج تستخدمها فيها:
مثلا لو محتاج تسجل أي تغير بيحصل في الداتا اللي بتظهر للمستخدم، عشان التحليل أو التتبع.
مزامنة البيانات مع localStorage: عشان البيانات تحفظ محليًا وتتحدث تلقائيًا.
في Angular، التعامل مع Injection Context أمر أساسي لإنشاء الـEffects بطريقة صحيحة، إذ تحتاج هذه البيئة (Injection Context) لتشغيل ()effect داخل سياق تقدر تستدعي فيه دوال inject، مثل الكومبوننت، (directives)، أو (services). فيما يلي طرق مختلفة لتنفيذ الـEffects ضمن Injection Context:
وبذلك يظل الـEffect يعمل فقط أثناء حاجة الكومبوننت له، ويتوقف تلقائيًا عندما يتم تدمير الكومبوننت.
تسجيل Effect داخل كود البناء (constructor) أبسط طريقة هي تسجيل الـEffect داخل الـconstructor مباشرة في الكومبوننت أو الخدمة. مثلًا
باختصار، استخدام Injection Context يسهل إنشاء وإدارة الكائنات والخدمات بطريقة تجعل التطبيق مرنًا، مستقرًا، وقابلاً للصيانة
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
constructor() {
// Register a new effect.
effect(() => {
console.log(`The count is: ${this.count()}`);
});
}
}
الطريقة دي بتساعد لو عايز تتابع أو توضح الهدف من الـEffect بشكل أوضح، وكمان بتخلي الكود منظم أكتر.
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
private loggingEffect = effect(() => {
console.log(`The count is: ${this.count()}`);
});
}
في بعض الحالات، قد تحتاج إلى إنشاء Effect خارج الـconstructor، ويمكنك هنا تمرير Injector إلى effect كخيار (Option)
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
constructor(private injector: Injector) {}
initializeLogging(): void {
effect(() => {
console.log(`The count is: ${this.count()}`);
}, {injector: this.injector});
}
}
الـEffects في Angular بيتم تدميرها تلقائيًا لما ينتهي المكان اللي اتسجلت فيه (زي الكومبوننت أو service). يعني لو عندك Effect معمول في كومبوننت، هيتدمر بشكل تلقائي لما الكومبوننت نفسه يتدمر. نفس الكلام بينطبق على التوجيهات (directives) و (services).
في نفس الوقت، كل Effect بيرجع حاجة اسمها EffectRef، ودي بتديك القدرة على تدمير الـEffect يدويًا لو احتجت كده. عن طريق استدعاء ()destroy على الـEffectRef، تقدر تنهي الـEffect وقت ما تحب.
@Component({...})
export class ExampleComponent {
private count = signal(0);
private effectRef = effect(() => {
console.log(`The count is: ${this.count()}`);
}, { manualCleanup: true });
ngOnDestroy() {
this.effectRef.destroy();
}
}
الElementRef هو Object بيتيح لنا الوصول المباشر للعنصر في الـDOM من داخل الكومبوننت أو الديركتيف.
غالبًا، في أغلب حالات الـAngular، بنعتمد على (Bindings) , (Directives) لتغيير الـDOM بشكل غير مباشر، لكن في حالات معينة، بنحتاج نوصل للعنصر نفسه عشان نعمل عمليات خاصة عليه، زي التفاعل مع مكتبات JavaScript خارجية أو إجراء تعديلات معينة على العنصر مباشرة.
أحيانًا نحتاج نعدل حاجة محددة في العنصر مباشرة، مش كل حاجة بتكون ممكنة بالـAngular bindings. على سبيل المثال:
إضافة أو تعديل بعض الخصائص (CSS Styles) بشكل ديناميكي.
التعامل مع مكتبات JavaScript خارجية محتاجة الـDOM Element مباشرة.
تنفيذ تعديلات أو تنقلات مباشرة على العنصر مش ممكنة بسهولة بالـAngular.
أول حاجة، عشان تقدر تستخدم ElementRef، لازم تنشئ حاجة اسمها Template Reference Variable في التيمبلت.
ده بيكون متغير بيشير للعنصر اللي عاوز توصله، وبتكتبه بالشكل ده في الـHTML بتاعك:
<div #hello>Hello Angular</div>
. بيدينا زي اسم داخلي نقدر نوصل بيه للعنصر ده من كود الكومبوننت.
عشان نستخدم hello جوه كود الكومبوننت، بنستعمل ViewChild@، وده بيسمح لـAngular إنه يححقن لنا refrence للعنصر في الكومبوننت بشكل مباشر.
import { Component, ElementRef, ViewChild } from "@angular/core";
@Component({
selector: "app-root",
template: `<div #hello>Hello Angular</div>`,
})
export class AppComponent {
@ViewChild("hello", { static: false }) divHello: ElementRef;
ngAfterViewInit() {
console.log(this.divHello.nativeElement.innerText); // هيطبع 'Hello Angular'
}
}
هنا، ViewChild@ بيعمل ربط بين المتغير divHello والـElementRef للعنصر اللي عليه الـTemplate Reference Variable hello.
بداخل ElementRef، عندنا خاصية nativeElement اللي بترجع العنصر نفسه من الـDOM، وبالتالي تقدر تتعامل مع العنصر مباشرة زي ما بتعمل في JavaScript العادي.
أحيانًا بيكون في ديركتيفات على نفس العنصر زي ngModel. لو عاوز توصل للعنصر نفسه كـElementRef بدل ما توصل للديركتيف ngModel، هنا بييجي دور الـread token. ده بيساعدنا نحدد نوع المرجع اللي محتاجينه (سواء ElementRef أو ديركتيف زي NgModel).
مثلاً عندنا input عليه ngModel كالتالي:
<input #nameInput [(ngModel)]="name">
import { Component, ElementRef, ViewChild } from "@angular/core";
import { NgModel } from "@angular/forms";
@Component({
selector: "app-root",
template: `<input #nameInput [(ngModel)]="name" />`,
})
export class AppComponent {
name: string;
// هنا بنطلب الـ ElementRef للعنصر input
@ViewChild("nameInput", { static: false, read: ElementRef })
elRef: ElementRef;
// هنا بنطلب ngModel المرتبط بالعنصر
@ViewChild("nameInput", { static: false, read: NgModel }) inRef: NgModel;
ngAfterViewInit() {
console.log(this.elRef.nativeElement); // بيرجع العنصر `<input>`
console.log(this.inRef.model); // بيرجع القيمة المرتبطة بـ ngModel
}
}
استخدام ElementRef في ديركتيف مخصص بيتيح لك تتعامل مع host element للديركتيف بشكل مباشر، زي ما بيظهر في مثال ttClass اللي عملنا فيه ديركتيف بيضيف كلاس معين للعنصر.
في الكود اللي كتبناه، ttClass هو ديركتيف مخصص بيستخدم ElementRef للوصول للعنصر host element وإضافة كلاس معين عليه، زي كالتالي:
import { Directive, ElementRef, Input, OnInit } from "@angular/core";
@Directive({
selector: "[ttClass]",
})
export class ttClassDirective implements OnInit {
@Input() ttClass: string;
constructor(private el: ElementRef) {}
ngOnInit() {
this.el.nativeElement.classList.add(this.ttClass);
}
}
لما تستخدم الديركتيف ttClass، هتكتب زي كده في الـHTML:
<div ttClass="highlight">Hello World!</div>
الElementRef بيسمح لك تتعامل مع العناصر مباشرةً، لكن Angular بتحذر من الاعتماد عليه كتير لأنه بيخلي التطبيق مرتبط جدًا بطريقة عرض العناصر، وده ممكن يسبب مشاكل في بعض الحالات، زي لما تتعامل مع Web Workers أو Server-side rendering. Renderer2 يعتبر بديل آمن لأنه بيسمح لك تتعامل مع العناصر بطريقة بتشتغل في بيئات مختلفة بدون الاعتماد المباشر على DOM.
استخدام ElementRef بطريقة غير صحيحة ممكن يعرض التطبيق لمشاكل أمنية زي هجمات XSS (Cross-Site Scripting).
الRenderer2 هو API من Angular بيتيح لك التعامل مع الـDOM بشكل آمن ويتوافق مع بيئات مختلفة.
الElementRef مفيد في بعض الحالات لكنه يحتاج حذر لأنه مرتبط مباشرةً بالـDOM.
الRenderer2 هو الخيار الأفضل لو محتاج تضمن أمان أكتر في التعامل مع العناصر.
الRenderer2 في Angular هو API بيتيح لك تتعامل مع عناصر الـ DOM بطريقة آمنة ومتوافقة مع بيئات مختلفة، وده بيكون أفضل من التعامل المباشر مع الـ DOM باستخدام ElementRef.
الRenderer2 بيستخدم في حالات زي إضافة أو إزالة كلاس، تغيير ستايلات CSS، التعامل مع الأحداث، وإضافة أو حذف عناصر في الـ DOM.
لو استخدمت ElementRef للتعامل مع الـ DOM مباشرة، هيشتغل في المتصفح (Browser) بس. لكن لو عاوز تشغل التطبيق بتاعك في بيئات تانية زي:
الWeb Workers (لتشغيل الكود بعيدًا عن الواجهة الأساسية).
الServer-Side Rendering (لتحميل التطبيق من السيرفر زي Angular Universal).
تطبيقات الموبايل وسطح المكتب.
✅ الـ Renderer2 بيخلي الكود متوافق ويشتغل في كل البيئات دي، لأنه بيعمل كـ "طبقة وسطية" بين كودك وعناصر الـ DOM.
- تغيير الخصائص (Properties) والستايلات (Styles) بدل ما تستخدم ElementRef.nativeElement.style، تقدر تستخدم Renderer2 لإضافة أو إزالة ستايلات على العنصر. زي كده:
@ViewChild('myDiv') myDiv: ElementRef;
constructor(private renderer: Renderer2) {}
addStyle() {
this.renderer.setStyle(this.myDiv.nativeElement, 'color', 'blue'); // يغير اللون للأزرق
}
removeStyle() {
this.renderer.removeStyle(this.myDiv.nativeElement, 'color'); // يشيل اللون
}
ممكن تضيف أو تشيل كلاس للعنصر زي addClass وremoveClass
addClass() {
this.renderer.addClass(this.myDiv.nativeElement, 'highlight');
}
removeClass() {
this.renderer.removeClass(this.myDiv.nativeElement, 'highlight');
}
ممكن كمان تستخدم Renderer2.listen عشان تتعامل مع الأحداث بطريقة آمنة ومتوافقة مع Angular
ngAfterViewInit() {
this.clickListener = this.renderer.listen(this.myDiv.nativeElement, 'click', () => {
console.log('Div clicked!');
});
}
ngOnDestroy() {
this.clickListener(); // تلغي الاشتراك في الحدث عشان ما يبقاش فيه تسريب للذاكرة
}
تقدر تنشئ عناصر جديدة وتضيفها للـ DOM باستخدام Renderer2
createElement() {
const newDiv = this.renderer.createElement('div');
const text = this.renderer.createText('Hello Angular');
this.renderer.appendChild(newDiv, text); // تضيف النص للعنصر
this.renderer.appendChild(this.myDiv.nativeElement, newDiv); // تضيف العنصر للـ DOM
}
ال@ViewChild بتستخدمها لو عايز تجيب عنصر معين من عناصر الـ DOM (زي زرار أو كومبوننت تاني) عشان تتعامل معاه من الكود، وبتديك أول عنصر يطابق الشرط اللي حطيته.
ال@ViewChildren بتديك كل العناصر اللي بتطابق الشرط اللي حطيته، وبتطلعهم في QueryList تقدر تلف عليها وتستخدم كل عنصر منها.
@ViewChild(ChildComponent, { static: true }) child: ChildComponent;
@ViewChild(ChildComponent, { static: true })
هنا ChildComponent هو نوع أو كلاس العنصر اللي عايزين نجيبه (في الحالة دي، بنجيب عنصر معين اللي هو الكومبوننت ChildComponent).
🔴static: true و static: false الخيار static بيحدد امتى Angular تجيب العنصر وتديك مرجع ليه. فيه حالتين هنا:
لو حطينا static: true، ده معناه إن Angular هتجيب المرجع للعنصر قبل ما تعمل أول تغيير (Change Detection). بمعنى تاني، Angular هتجيب العنصر من الـ DOM في بداية تحميل الصفحة أو الكومبوننت، وده مفيد لو العنصر دايمًا بيبقى موجود في التيمبلت بتاعنا.
لو حطينا static: false، ده معناه إن Angular هتستنى لحد ما تكمل أول Change Detection وتحدد العناصر اللي هتظهر حسب شروط معينة زي *ngIf أو *ngSwitch.
يعني لو العنصر بيظهر بناءً على شرط معين أو بيتم تحميله ديناميكيًا، لازم تستخدم static: false، عشان Angular تستنى لحد ما الشرط يتحقق أو العنصر يظهر في التيمبلت.
لو عندنا عنصر بيتحكم في ظهوره شرط معين، زي *ngIf، لازم نستخدم static: false عشان نضمن إن Angular هتنتظر لحد ما يتحقق الشرط ده ويظهر العنصر في التيمبلت.
<div *ngIf="showChildComponent">
<child-component></child-component>
</div>
@ViewChild(ChildComponent, { static: false }) child: ChildComponent;
عندنا كومبوننت صغير اسمه ChildComponent فيه عداد، وفيه زراير لزيادته أو نقصانه:
import { Component } from "@angular/core";
@Component({
selector: "child-component",
template: `<h2>Child Component</h2>
<p>Current count: {{ count }}</p>`,
})
export class ChildComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
import { Component, ViewChild } from "@angular/core";
import { ChildComponent } from "./child.component";
@Component({
selector: "app-root",
template: `
<h1>Parent Component</h1>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<child-component></child-component>
`,
})
export class AppComponent {
@ViewChild(ChildComponent, { static: true }) child: ChildComponent;
increment() {
this.child.increment();
}
decrement() {
this.child.decrement();
}
}
<child-component #childRef></child-component>
@ViewChild('childRef', { static: true }) child: ChildComponent;
لو عايز تجيب عنصر HTML زي p أو div، ممكن تستخدم ViewChild@ مع ElementRef.
<p #para>Some text</p>
import { Component, ElementRef, ViewChild, AfterViewInit } from "@angular/core";
@Component({
selector: "htmlelement",
template: `<p #para>Some text</p>`,
})
export class HTMLElementComponent implements AfterViewInit {
@ViewChild("para", { static: false }) para: ElementRef;
ngAfterViewInit() {
console.log(this.para.nativeElement.innerHTML);
this.para.nativeElement.innerHTML = "new text";
}
}
لو عندك أكتر من عنصر زي ChildComponent في نفس التيمبلت، ViewChild@ هترجعلك أول واحد بس. لكن لو عايز كل العناصر، استخدم ViewChildren@
في المثال ده، عندنا أكتر من input:
<input name="firstName" [(ngModel)]="firstName">
<input name="middleName" [(ngModel)]="middleName">
<input name="lastName" [(ngModel)]="lastName">
import { Component, ViewChildren, QueryList, NgModel } from "@angular/core";
@Component({
selector: "app-viewchildren-example",
template: `
<input name="firstName" [(ngModel)]="firstName" />
<input name="middleName" [(ngModel)]="middleName" />
<input name="lastName" [(ngModel)]="lastName" />
<button (click)="show()">Show</button>
`,
})
export class ViewChildrenExampleComponent {
@ViewChildren(NgModel) modelRefList: QueryList<NgModel>;
show() {
this.modelRefList.forEach((element) => {
console.log(element);
});
}
}
لما نستخدم ViewChild@ او ViewChildren@ في Angular، بنحتاج نستنى لحد ما العناصر في الـ DOM تكون اتعملها تحميل بالكامل قبل ما نقدر نستخدمها في الكود.
عشان كده، الـlifecycle hook المناسبة للتعامل مع ViewChild@ هي ngAfterViewInit مش ngOnInit.
الContentChild@ و ContentChildren@
هما (Decorators) في Angular بنستخدمهم عشان نوصل لحاجة اسمها "Projected Content"، اللي هو المحتوى اللي بيجي للكومبوننت من كومبوننت الأب (Parent Component).
الProjected Content يعني محتوى جاي من الكومبوننت الأب عشان يتعرض في الكومبوننت الابن، وبنحدده عادةً باستخدام عنصر ng-content
اللي بنضيفه في الكومبوننت الابن عشان يحجز مكان لعرض المحتوى اللي جاي من بره.
🔴 الViewChild@ و ViewChildren@ بيقدروا يجيبوا أي عنصر موجود جوه الكومبوننت نفسه، سواء كان عنصر HTML، أو كومبوننت تاني، أو ديركتيف.
🔴 الContentChild@ و ContentChildren@ بقى بيستخدموا للوصول للمحتوى اللي جاي من الكومبوننت الأب عبر ng-content
، يعني بيقدروا يجيبوا أي حاجة جاية من بره مش موجودة بشكل مباشر جوه الكومبوننت.
- مثال بسيط على ContentChild
خلينا نقول عندنا كومبوننت (CardComponent) بيستخدم عشان ياخد محتوى من بره ويعرضه جواه.
مثلا الكارت فيه 3 أماكن:
header
,content
,وfooter
import { Component } from "@angular/core";
@Component({
selector: "card",
template: `
<div class="card">
<ng-content select="header"></ng-content>
<ng-content select="content"></ng-content>
<ng-content select="footer"></ng-content>
</div>
`,
styles: [
`
.card {
min-width: 280px;
margin: 5px;
float: left;
}
`,
],
})
export class CardComponent {}
الكود ده هيحجز 3 أماكن في الكارت: واحد لـ header، والتاني لـ content، والتالت لـ footer
- استخدام ContentChild للوصول للعنصر المرسل من الكومبوننت الأب دلوقتي لو عندنا في الكومبوننت الأب كذا كارت وعايزين نضيف لكل كارت محتوى مختلف، نكتب الكود بالشكل ده:
<card>
<header><h1 #header>Angular</h1></header>
<content>One framework. Mobile & desktop.</content>
<footer><b>Super-powered by Google</b></footer>
</card>
في المثال ده، إحنا ضفنا header, content, وfooter لكارت واحد، وحددنا <h1 #header>
اللي موجود في header.
دلوقتي عايزين نوصل للـ h1 اللي في header جوه CardComponent باستخدام ContentChild@
import {
Component,
ContentChild,
ElementRef,
AfterContentInit,
Renderer2,
} from "@angular/core";
@Component({
selector: "card",
template: `
<div class="card">
<ng-content select="header"></ng-content>
<ng-content select="content"></ng-content>
<ng-content select="footer"></ng-content>
</div>
`,
})
export class CardComponent implements AfterContentInit {
@ContentChild("header") cardContentHeader: ElementRef;
constructor(private renderer: Renderer2) {}
ngAfterContentInit() {
this.renderer.setStyle(
this.cardContentHeader.nativeElement,
"font-size",
"20px"
);
}
}
الContentChild('header')@
بيجيب أول عنصر اسمه header موجود في الـ Projected Content.
الngAfterContentInit هو الـ lifecycle hook المناسب عشان نضمن إن Angular حملت المحتوى الخارجي.
لو عندنا كذا عنصر بنفس الاسم (مثلا كذا عنصر header)، ممكن نستخدم ContentChildren@ عشان نجيبهم كلهم في شكل قائمة (QueryList) ونقدر نلف عليهم ونتعامل مع كل عنصر فيهم.
@ContentChildren('header') headers: QueryList<ElementRef>;
ال@ContentChild بيجيب أول عنصر بس يطابق الاسم.
ال@ContentChildren بيجيب كل العناصر اللي ليها نفس الاسم في شكل قائمة (QueryList)، وتقدر تستخدم forEach عشان تتعامل مع كل عنصر لوحده.
# | ViewChild و ViewChildren | ContentChild و ContentChildren |
---|---|---|
الاستخدام | بيدور على العناصر جوه الكومبوننت نفسه | بيدور على المحتوى اللي جاي من بره |
التوقيت | بيشتغل بعد تحميل التيمبلت | بيشتغل بعد تحميل المحتوى الخارجي (AfterContentInit) |
الـ Decorators في Angular هي عبارة عن وظيفة أو دالة بتدي معلومات إضافية عن الكلاس (class) أو (method) أو (property) أو (parameter).
بتدي معلومات لـ Angular عشان تفهم الكود وتعرف إزاي تتعامل معاه. يعني، الديكوريتور بيزود الكلاس بمعلومات تخليه (Component)، أو (Directive)، أو (Service)... وهكذا.
الAngular بتستخدم الـ Decorators عشان تضيف Metadata (معلومات إضافية) للكود اللي بنكتبه.
المعلومات دي بتساعد Angular تفهم إزاي تتعامل مع الكلاس أو الميثودات اللي جواه.
مثلا، لو عندك كلاس وعايز تخليه Component، بنستخدم ديكوريتور اسمه Component@، ولو عايز تعمل Service بنستخدم Injectable@ وهكذا.
الAngular عندها كذا نوع من الـ Decorators، وهنقسمهم لأربع أنواع رئيسية:
ديكوريتورز بتشتغل على الكلاسات، زي Component@ وDirective@ وInjectable@، ودي بتخلي Angular تتعامل مع الكلاس ده بطريقة معينة.
بنستخدمه عشان نقول لـ Angular إن الكلاس ده عبارة عن Component، وده بيخلي Angular تتعامل مع الكلاس ده ك component ممكن يتعرض في التيمبلت.
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
})
export class AppComponent {}
ديكوريتور بيستخدم عشان نقول لـ Angular إن الكلاس ده محتاج يتعامل كـ Service. ده بيخلي Angular تعرف توفر الكلاس ده لما نحتاجه في component تاني عن طريق الـ Dependency Injection.
@Injectable({
providedIn: "root",
})
export class MyService {}
export class ChildComponent {
@Input() childProperty: string;
}
@HostListener('click') onClick() {
console.log(' cliked');
}
constructor(@Inject(SomeToken) private myValue) {
}
قبل ما نشرح الـ Hooks اللي بنستخدمهم، محتاجين نفهم حاجة مهمة، وهي الفرق بين Content و View:
الContent: ده المحتوى اللي بيوصل للكومبوننت كـ Projected Content، يعني محتوى بيجيله من الكومبوننت الأب عن طريق <ng-content>
.
الView: دي التيمبلت (Template) أو UI بتاع الكومبوننت نفسه، اللي بنكتبه جواه.
فيه أربع Hooks مهمين في Angular بيساعدونا نتحكم في التوقيت اللي بنشتغل فيه مع المحتوى اللي جاي من بره (اللي هو الـ Content) والعرض بتاع الكومبوننت (اللي هو الـ View).
ده Hook بيشتغل أول ما يتعمل تحميل كامل للمحتوى (Content) اللي جاي من الأب.
الAngular كمان بيحدّث الخصائص اللي بنزودها بديكوريتور ContentChild@ أو ContentChildren@ قبل ما تنادي Hook دي.
بتشتغل مرة واحدة بس.
ده Hook بيشتغل كل مرة يحصل Change Detection في التطبيق.
يعني إيه؟ يعني كل مرة Angular بتفحص إذا فيه تغيير حصل، بتشغل الـ Hook ده وتتأكد من أي تغييرات حصلت في المحتوى.
بـالمختصر، ده بيشوف إذا حصل أي تعديل على المحتوى وبيشتغل كل مرة Angular تعمل عملية فحص للتغييرات (حتى لو بسيط زي الضغط على زرار).
ده Hook بيشتغل بعد ما ينتهي تحميل الـ View الخاص بالكومبوننت وأي Child Components جواه.
الAngular بيحدّث الخصائص اللي بنزودها بديكوريتور ViewChild@ أو ViewChildren@ قبل ما تنادي Hook دي.
بتشتغل مرة واحدة بس.
ده Hook بيشتغل كل مرة يحصل Change Detection ويتأكد من أي تغييرات في (View).
يعني بعد ما Angular تخلص فحص التغييرات، هتشغل الـ Hook ده عشان تشوف إذا اتعدل ولا لأ.
بـالمختصر، ده زيه زي AfterContentChecked لكن بيركز على العرض بدل المحتوى، وبيشتغل في كل عملية فحص تغييرات.
في Angular، الـ Checked Hooks زي ngAfterViewChecked بتشتغل بعد كل دورة فحص تغييرات (Change Detection Cycle). يعني، كل مرة Angular تشوف إذا فيه حاجة اتغيرت في الكومبوننت أو في الـ View، بيتم استدعاء الـ Hook دي.
المشكلة في تعديل الـ Bindings في الـ Checked Hooks لما تعمل تعديل على Binding (زي تعديل قيمة متغيرة بتظهر في التيمبلت) في ngAfterViewChecked، ده بيعمل مشكلة لإن Angular هتضطر تدخل في دورة فحص جديدة عشان تتأكد من التغييرات، وهكذا تفضل في حلقة مفرغة (Infinite Loop) من دورات الفحص.
الخطأ ده معناه إن فيه قيمة اتغيرت بعد ما Angular انتهت من الفحص، وده بيعمل تعارض في الـ Change Detection.
لو عايز تقرأ قيمة viewChild.message وتعرضها في التيمبلت من غير مشاكل، اعمل التعديل في Hook زي ngAfterViewInit أو في أماكن تانية مش بتتكرر زي ngOnInit.
الInit Hooks (AfterContentInit وAfterViewInit): بتشتغل مرة واحدة بس بعد ما ينتهي تحميل المحتوى أو العرض لأول مرة.
الChecked Hooks (AfterContentChecked وAfterViewChecked): بتشتغل في كل دورة Change Detection.
🔴 لما بقول إن الـ Hook "بتشتغل مرة واحدة بس"، أقصد إن الـ Hook دي بتشتغل أول ما يحصل تحميل للكومبوننت بس ومش بتكرر تاني.
الفكرة إن الـ Hooks دي متخصصة إنك تستخدمها لإعدادات مبدئية. مثلا لو عايز تعمل حاجة في الكومبوننت بعد ما يخلص تحميله مباشرةً، بس مش محتاج تعملها كل مرة يحصل فيها تغيير في التطبيق. عشان كده Angular بتشغل الـ Hooks دي مرة واحدة بس بعد أول تحميل للكومبوننت أو المحتوى.
في المقابل، فيه Hooks زي AfterContentChecked و AfterViewChecked. دول بيشتغلوا كل مرة يحصل فيها Change Detection في التطبيق.
الـ View Encapsulation في Angular بيساعدنا نتحكم في الاستايلات الخاصة بكل كومبوننت بحيث تكون معزولة وماتأثرش على باقي الكومبوننتات في التطبيق.
يعني لو عندك استايلات معينة في كومبوننت واحد، مش عايزينها تأثر على استايلات الكومبوننتات التانية.
يعني إيه ViewEncapsulation.None؟
لو استخدمت ViewEncapsulation.None، يبقى كده مفيش عزل. الاستايلات اللي بتعملها في الكومبوننت ده هتكون (Global)، يعني ممكن تأثر على كل العناصر في التطبيق.
import { Component, ViewEncapsulation } from "@angular/core";
@Component({
selector: "app-none",
template: `<p>I am not encapsulated and in blue (ViewEncapsulation.None)</p>`,
styles: ["p { color:blue}"],
encapsulation: ViewEncapsulation.None,
})
export class ViewNoneComponent {}
لو أضفنا الكومبوننت ده في التطبيق، الاستايل color: blue هيأثر على كل <p>
في التطبيق، مش بس جوا الكومبوننت ده.
يعني إيه ViewEncapsulation.Emulated؟
دي تعتبر الاستراتيجية الافتراضية في . Emulated Encapsulation يعني الAngular هتضيف حاجة زي أكواد HTML Attributes فريدة للكومبوننت والاستايلات الخاصة به، بحيث تضمن إن الاستايلات دي ماتأثرش على باقي الكومبوننتات.
يعني Angular بتضيف حاجات زي _ngcontent لكل عنصر جوا الكومبوننت عشان تربط الاستايلات الخاصة بيه بيه بس، بحيث تضمن إن الاستايلات ماتخرجش لباقي التطبيق.
import { Component, ViewEncapsulation } from "@angular/core";
@Component({
selector: "app-emulated",
template: `<p>Using Emulator</p>`,
styles: ["p { color:red}"],
encapsulation: ViewEncapsulation.Emulated,
})
export class ViewEmulatedComponent {}
لما تفتح (Developer Tools)، هتلاقي Angular ضافت Attribute فريد زي _ngcontent-c2
على عنصر <p>
والاستايلات الخاصة به. ده بيخلي الاستايلات دي تنطبق بس على العناصر اللي جوا الكومبوننت ده، وماتأثرش على باقي التطبيق.
يعني إيه ViewEncapsulation.ShadowDOM؟
الـ Shadow DOM هو جزء من Web Components وبيوفر عزل حقيقي للاستايلات.
لما تستخدمه، الاستايلات الخاصة بالكومبوننت ده مش هتخرج بره الكومبوننت، ومش هتأثر على أي عناصر تانية بره الكومبوننت.
ملاحظة: مش كل المتصفحات بتدعم الـ Shadow DOM، فالأفضل تشغل التطبيق على متصفح زي جوجل كروم.
import { Component, ViewEncapsulation } from "@angular/core";
@Component({
selector: "app-shadowdom",
template: `<p>
I am encapsulated inside a Shadow DOM ViewEncapsulation.ShadowDom
</p>`,
styles: ["p { color:brown}"],
encapsulation: ViewEncapsulation.ShadowDom,
})
export class ViewShadowdomComponent {}
الAngular بترندر الكومبوننت ده جوا عنصر اسمه shadow-root#.
العنصر ده بيعزل الاستايلات الخاصة بالكومبوننت عن باقي التطبيق، وده بيحقق عزل حقيقي.
مزايا: عزل حقيقي للاستايلات، وبكده تضمن إن الاستايلات ماتأثرش على أي حاجة بره الكومبوننت.
عيوب: مش كل المتصفحات بتدعمه، وبعض الاستايلات المشتركة بين الكومبوننتات ممكن ماتشتغلش زي ما تتوقع.
الViewEncapsulation.None: استخدامه لما تكون عايز الاستايلات تأثر على التطبيق كله.
الViewEncapsulation.Emulated: الاختيار الافتراضي، بيعزل الاستايلات باستخدام ng-content.
الViewEncapsulation.ShadowDOM: بيعزل الاستايلات بشكل كامل لكن ممكن مايشتغلش على كل المتصفحات.
الhost هو CSS Selector بنستخدمه في Angular عشان نستهدف العنصر الأساسي بتاع الكومبوننت نفسه.
لما بتستخدمه، هو بيخليك تضيف استايلات على العنصر الرئيسي للكومبوننت من غير ما تتعرض للعناصر اللي جواه أو من غير ما تضطر تضيف كلاس معين عليه.
كل كومبوننت بيبقى ليه استايلات خاصة بيه، ولو عايز تضيف استايل للعنصر الأساسي بتاع الكومبوننت من غير ما تأثر على باقي الكومبوننتات أو محتوى الصفحة، هنا :host بيبقى مفيد.
خلينا نقول عندك كومبوننت اسمه app-card وعايز تخلي العنصر الأساسي يظهر بحدود (border) ولون خلفية (background color) محددين.
@Component({
selector: "app-card",
template: `
<div class="content">
<p>this is a content</p>
</div>
`,
styles: [
`
:host {
display: block;
border: 2px solid #333;
background-color: #f9f9f9;
padding: 16px;
border-radius: 8px;
}
`,
],
})
export class CardComponent {}
الـ :host هنا بيخلي الاستايلات تطبق على العنصر الأساسي بتاع app-card نفسه.
مش محتاج تضيف كلاس أو تعمل حاجة مخصوص للعنصر الرئيسي، لأن :host بيفهم لوحده إن ده هو العنصر الحاوي للكومبوننت، وبيطبق عليه الاستايلات مباشرة.
الhost-context هو CSS Selector بيستخدم في Angular عشان تتحكم في الاستايلات بناءً على السياق الخارجي للكومبوننت. يعني بيشوف الكومبوننت موجود فين أو جواه إيه، ويطبق الاستايلات بناءً على ده.
تخيل :host-context زي شرط بيقول: "لو الكومبوننت موجود في سياق معين أو محاط بعنصر معين، يبقى طبق الاستايل ده".
افترض إن عندك كومبوننت في Angular فيه عنصر <input>
، وعايز هذا الكومبوننت يظهر بشكل مختلف بناءً على المكان اللي بيتواجد فيه. يعني مثلًا:
لو الكومبوننت ده موجود جوا Dropdown، عايز عرض الـ <input>
يكون 50%.
لو الكومبوننت ده موجود جوا Table، عايز عرض الـ <input>
يكون 100%.
لو عندنا كومبوننت MyInputComponent اللي فيه <input>
، عايزين نخليه يغير عرضه بناءً على المكان اللي هو فيه.
الكومبوننت MyInputComponent هيظهر في أماكن مختلفة، زي my-dropdown و my-table.
لو الكومبوننت ده جوه <my-dropdown>
، العرض يكون 50%.
لو الكومبوننت ده جوه <my-table>
، العرض يكون 100%.
:host-context(my-dropdown) input {
width: 50%; /* العرض لما يكون جوا dropdown */
}
:host-context(my-table) input {
width: 100%; /* العرض لما يكون جوا table */
}
ال:host-context(my-dropdown) input: ده بيقول إنه لو الكومبوننت MyInputComponent موجود جوا عنصر <my-dropdown>
، يخلي عرض <input>
في الكومبوننت ده 50%.
ال:host-context(my-table) input: ده بيقول إنه لو الكومبوننت MyInputComponent موجود جوا <my-table>
، يخلي عرض <input>
في الكومبوننت ده 100%.
دلوقتي لو حطيت MyInputComponent جوا أو ، هيتم تطبيق الاستايل اللي يناسب السياق بشكل تلقائي.
<my-dropdown>
<my-input></my-input> <!-- العرض 50% هنا عشان موجود في dropdown -->
</my-dropdown>
<my-table>
<my-input></my-input> <!-- العرض 100% هنا عشان موجود في table -->
</my-table>
بدل ما تكتب كود إضافي في كل مكان أو تضيف كلاس جديد للتحكم في الاستايل، host-context بيسهل عليك التحكم في استايلات الكومبوننت بناءً على السياق الخارجي اللي الكومبوننت موجود فيه، من غير ما تعدل على الكومبوننت نفسه كل مرة.
السؤال هنا، إزاي بيعمل كده بعد أحداث زي ضغطة زر، اللي ممكن تحصل في أي حتة في الصفحة؟
اللي بيحصل هو إن أنجولار بيبص على كل متغير مستخدم في الـHTML (الـTemplate)، زي لما تكتب {{ user.name }}
أو تحط حاجة زي [value]="counter"
، وبيقارن القيمة الحالية للمتغير ده بالقيمة اللي كانت موجودة قبل كده.
1- أنجولار بيمر على كل متغير جوه الـTemplate عشان يشوف إذا حصل فيه تغيير ولا لأ.
2- بيقارن القيمة الجديدة بالقيمة القديمة. لو لقى إنهم مختلفين، بيقول "أيوة، حصل تغيير"، وبيخلي متغير داخلي اسمه isChanged يبقى true.
3- أنجولار بيستخدم دالة اسمها looseNotIdentical، ودي بتقارن بين القيم بطريقة مشابهة للمقارنة ===، لكنها بتتجنب مشاكل معينة زي NaN (عشان لو عندك NaN، جافاسكريبت بتتعامل معاه بشكل مختلف شوية).
4- لو isChanged بقت true، أنجولار بيروح يعمل تحديث للجزء ده في الواجهة عشان يظهر التغييرات.
الصورة بتوضح شجرة الكومبوننتات (Component Tree) في Angular، ولكل كومبوننت فيه (Change Detector) خاص بيه بيتعمل أثناء عملية تشغيل التطبيق لأول مرة
والchange detector بيعمل نفس الخطوات السابقة
الZone.js هو مكتبة في Angular بتعمل حاجة زي "مراقبة" للعمليات غير المتزامنة، زي لما تعمل استدعاء API أو تعدادات زمنية (timers) زي setTimeout أو setInterval.
المكتبة دي بتسمح لـ Angular إنه "يتصنت" على العمليات دي، وده بيساعده إنه يقدر يكتشف التغييرات اللي بتحصل في البيانات بتاعت التطبيق لما أي من العمليات دي تتنفذ.
الـ Zone بتعدي بمراحل معينة علشان Angular يقدر يتحكم في متى يشغل آلية اكتشاف التغييرات:
1- المرحلة المستقرة (Stable): التطبيق بيكون شغال عادي من غير أي عمليات غير متزامنة شغالة، يعني التطبيق هادي.
2- المرحلة غير المستقرة (Unstable): لو حصلت عملية غير متزامنة (زي طلب HTTP أو حدث كليك على زر)، الـ Zone بتتحول لغير مستقرة، وبتبدأ تراقب العمليات دي لحد ما تخلص.
3- المرحلة المستقرة تاني: بعد ما تخلص العمليات دي، الـ Zone بترجع مستقره، ووقتها Angular بيبدأ عملية change detection علشان يحدث واجهة المستخدم لو في أي تغيير حصل.
اللي بيحصل إن Angular عامل منطقة خاصة بيه اسمها NgZone، ودي بتمثل المكان اللي Angular بيقدر يشغل فيه change detection على العمليات غير المتزامنة اللي بتحصل فيه. يعني مثلاً:
لما يحصل حدث في المتصفح زي ضغطة زر (click) أو كتابة حرف (keyup). لما يستعمل setTimeout أو setInterval. لما تعمل طلب HTTP عن طريق XMLHttpRequest. أي حاجة بتحصل في NgZone دي هتشغل change detection وتخلي Angular يحدث UI لو حصل أي تغيير في البيانات.
Angular provides two strategies to run change detections
بشكل افتراضي، Angular بيستخدم ChangeDetectionStrategy.Default، واللي معناها إن كل مرة يحصل حدث ممكن يؤثر على البيانات، زي حدث من المستخدم، أو مؤقت (timer)، أو طلب HTTP (XHR)، أو promise وغيره، Angular بيمر على كل الكومبوننتات في شجرة الكومبوننتات من فوق لتحت.
العملية دي بيسموها الفحص المتكرر (dirty checking)، وده لأنه Angular بيتأكد من كل كومبوننت من غير ما يفترض إن فيه علاقات معينة أو اعتماد على عناصر تانية جواه.
الطريقة دي محافظة أكتر، لأنها بتفحص كل الكومبوننتات وتفترض إنها ممكن تكون اتغيرت، لكن ده ممكن يأثر سلبًا على أداء التطبيق، خصوصًا في التطبيقات الكبيرة اللي فيها عدد كبير من الكومبوننتات. فحص كل الكومبوننتات كل مرة بيكون بطيء وممكن يقلل سرعة التطبيق مع زيادة الحجم والتعقيد.
علشان نستخدم استراتيجية ChangeDetectionStrategy.OnPush، ممكن نضيف خاصية changeDetection في الديكوريتور بتاع الكومبوننت بالشكل ده:
@Component({
selector: 'hero-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
export class HeroCard {
...
}
استراتيجية OnPush بتديك ميزة كبيرة في تحسين الأداء، لأنها بتقلل الفحوصات اللي Angular بيعملها. لما نستخدم OnPush، Angular مش هيفحص الكومبوننت ده ولا أي كومبوننت فرعي جواه إلا في حالات معينة، زي:
- the input reference has changed
- the component or one of its children triggers an event handler
- change detection is triggered manually
- an observable linked to the template via the async pipe emits a new value
لما تستخدم استراتيجية OnPush في Angular، فآلية changeDetection بتشتغل بس لما يتغير refrence.
ال(Primitive types): زي الأرقام (numbers)، النصوص (strings)، القيم المنطقية (booleans)، null، وundefined. لما تعدل قيمتها، بيعتبر ده تغيير، وده معناه إن changeDetection هيشتغل تلقائيًا.
ال(Objects) و (Arrays): هنا الموضوع مختلف. لو عدلت خاصية جوا object أو عنصر جوا array الrefrence بتاعهم مبيختلفش، يعني Angular مش هيشغل changeDetection لأن refrence ثابت.
علشان االchangeDetection يشتغل، لازم تبعت object جديد أو array جديدة بreference جديد.
تخيل عندك كومبوننت بياخد بيانات كـ Input@ بالطريقة دي
@Component({
selector: "example-component",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`,
})
export class ExampleComponent {
@Input() data: any;
}
🔴 السيناريو الأول:
لو البيانات كانت رقم (مثلاً data = 5) وعدلتها إلى data = 6، الAngular هيلاحظ التغيير لأن Primitive types بيتغير بالقيمة، وchangeDetection هيشتغل.
🔴 السيناريو الثاني
تعديل object أو array
this.data = { name: "John" };
this.data.name = "Doe";
لو عايز الـ Change Detector يشتغل، لازم تعمل Object أو Array جديد بrefrence جديد، كده Angular هيقدر يشغل الـ Change Detector ويلاحظ التغيير.
this.data = { ...this.data, name: "Doe" };
خد بالك ان في بعض الحالات زي setTimeout وPromise واشتراكات RxJS، التغييرات بتحصل بشكل "خارج" عن نطاق الكشف التقليدي. وبالتالي، الـOnPush مش بيكتشف التغييرات دي تلقائيًا.
لما بنستخدم setTimeout أو setInterval في الـcomponent، الكود بينفذ بعد وقت معين، لكن ده بيحصل "خارج" نطاق Angular change detection.
يعني Angular مش عارف إن فيه تغيير حصل ومش هيعرف يظهره مباشرة على الشاشة لأن OnPush مش بيشوف التغييرات دي.
استخدام ChangeDetectorRef.markForCheck
اللي بيقول لـAngular يشوف التغييرات حتى لو جت من حاجات زي setTimeout أو Promise
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from "@angular/core";
@Component({
selector: "app-hero-card-on-push",
template: `
<div>
<p>Age: {{ age }}</p>
<button (click)="changeAge()">Change Age with setTimeout</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeroCardOnPushComponent {
age = 25;
constructor(private cdr: ChangeDetectorRef) {}
changeAge() {
setTimeout(() => {
this.age = 30; // غيرنا العمر هنا
this.cdr.markForCheck(); // بنستخدم markForCheck عشان نقول لـAngular يشوف التغيير
}, 1000);
}
}
بعد ما نغير age، بنستخدم this.cdr.markForCheck
عشان نطلب من Angular إنه يعمل كشف عن التغييرات ويحدث الشاشة. بدون markForCheck
، التغيير في age مش هيظهر لأننا بنستخدم OnPush.
الmarkForCheck بيقول لـAngular إن فيه تغيير حصل وبيحتاج يتشاف حتى لو كان جاي من setTimeout أو Promise أو حاجة تانية خارج نطاق الكشف التقليدي للتغييرات.
في Angular، فيه ٣ طرق أساسية ممكن تستخدمها عشان تشغل الكشف عن التغييرات يدويًا، وده مفيد لما تكون محتاج تحديث الـcomponent أو التطبيق في حالات خاصة، زي ما بيحصل في setTimeout أو Promise، اللي ما بيشغلوش الكشف عن التغييرات بشكل تلقائي
إيه اللي بيعمله؟ الdetectChanges بتشغل الكشف عن التغييرات للـcomponent الحالي وأي مكونات فرعية ليه.
فين ممكن تستخدمه؟ لما عايز تحدث الـcomponent الحالي فقط بعد ما يحصل تغيير معين من غير ما تعمل تحديث لكل التطبيق.
مثال عملي:
لو عندك component فيه قيمة age بتتغير بعد فترة باستخدام setTimeout، وعايز تشغل الكشف عن التغييرات يدويًا عشان تظهر القيمة الجديدة
import { ChangeDetectorRef } from '@angular/core';
constructor(private cdr: ChangeDetectorRef) {}
changeAge() {
setTimeout(() => {
this.age = 30;
this.cdr.detectChanges();
}, 1000);
}
إيه اللي بيعمله؟
الApplicationRef.tick بتشغل الكشف عن التغييرات على مستوى التطبيق بالكامل، يعني بتشيك على كل الـcomponents، وده بيكون تأثيره أوسع من detectChanges.
فين ممكن تستخدمه؟ لو حصل تغيير في جزء معين من التطبيق وعايز تشغل الكشف عن التغييرات في كل components بس نادرًا بنحتاجها لأن تأثيرها على الأداء كبير
إيه اللي بيعمله؟
الmarkForCheck ما بيشغّلش الكشف عن التغييرات مباشرةً، لكنه بيوصّل للـOnPush components إنهم يتفحصوا في أقرب دورة كشف تغييرات.
فين ممكن تستخدمه؟ لو فيه تغييرات بتحصل خارج نطاق الكشف عن التغييرات، وعايز تأكد إن component هيعمل فحص ليها في الدورة الجاية.
الmarkForCheck: لما تحتاج تضمن تحديث الـcomponent في الدورة القادمة بدون ما تشغّل الكشف عن التغييرات في الحال.
الAsync Pipe هو Pipe جاهز في Angular بيقوم بالاشتراك في الـObservable أو Promise تلقائيًا، وبيرجع آخر قيمة تم إصدارها منه.
ده بيخليك تستفيد من التحديثات اللي بتحصل في البيانات بدون ما تحتاج تتعامل مع الاشتراكات subscribe بنفسك، وده بيسهل عليك كتابة كود أنظف وأكثر ترتيبًا
الAsync Pipe بيستخدم markForCheck عشان يضمن إن التحديثات تظهر حتى لو الـcomponent بيستخدم استراتيجية OnPush.
في كل مرة الـObservable أو الـPromise يرجع قيمة جديدة، Async Pipe بيستخدم markForCheck عشان يخلي Angular يشوف التغييرات دي ويحدث الـcomponent
التوافق مع الOnPush: متوافق تلقائيًا مع استراتيجية OnPush، مما يجعله مناسب جدًا لو انتقلنا لاستخدام OnPush في التطبيق.
إدارة الذاكرة: Async Pipe بيلغي الاشتراك تلقائيًا في الـObservable أو الـPromise لما يتم تدمير الـcomponent، وبالتالي بيقلل من مخاطر التسريبات (Memory Leaks).
بساطة الكود: Async Pipe بيقلل من الحاجة لكتابة كود معقد لعملية الاشتراك والإلغاء، مما يجعل الكود أنظف وأقل عرضة للأخطاء.
import { Component } from "@angular/core";
import { Observable, of } from "rxjs";
@Component({
selector: "app-user",
templateUrl: "./user.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserComponent {
user$: Observable<{ name: string }> = of({ name: "John Doe" });
}
<div *ngIf="user$ | async as user">
<p>{{ user.name }}</p>
</div>
الAngular هو إطار عمل بيشتغل بطريقة الـ component-driven، يعني كل حاجة في التطبيق مبنية على مكونات (components). وزي أي إطار عمل تاني، فكرته الأساسية إنه يعرض الداتا للمستخدم ويعمل تحديث للواجهة (view) لما الداتا تتغير.
فكرة كشف التغييرات (change detection) ببساطة هي عملية متابعة التغييرات اللي بتحصل في الداتا، ولما يحصل تغيير، يبتدي يعرضه للمستخدم في الواجهة. يعني لو مثلا عندك اسم بيظهر على الشاشة والمستخدم غيره، Angular بيعمل تحديث تلقائي للعرض عشان يظهر الاسم الجديد.
زمان، Angular كانت بتستخدم حاجة اسمها Zone.js عشان تتتبع التغييرات دي. Zone.js كان مسؤول عن متابعة كل الأحداث اللي بتحصل في التطبيق، زيcilke و (server responses)، ولما يحصل تغيير، بيبدأ عملية كشف التغييرات عشان يحدّث الواجهة.
دلوقتي، في توجه جديد اسمه زونليس (zoneless)، وده يعني إننا بنحاول نبعد عن استخدام Zone.js عشان نخلي الأداء أسرع ونقلل التعقيدات. الفكرة هنا إنك تتابع التغييرات بشكل محلي في كل component لوحده، وده اللي بيسموه الكشف المحلي للتغييرات (local change detection). يعني كل component يتابع الداتا بتاعته بس، من غير ما يدخل Zone.js في كل عملية.
وفي مفهوم جديد ظهر اسمه (signals)، وده عبارة عن طريقة ذكية عشان تعرف Angular إن في حاجة اتغيرت، من غير ما تتابع كل التفاصيل الصغيرة. يعني بدل ما التطبيق كله يبقى بيراقب الداتا طول الوقت، signals هتدي إشارة لما يحصل تغيير، وده يخلي الأداء أحسن والتحميل أسرع.
A UserCard component that shows user data in the template
مع الوقت، وإحنا بنبني components أكتر في التطبيق، بنبدأ نركبهم مع بعض ونعمل شجرة من (component tree) زي اللي موضحة في الرسم اللي فوق.
إزاي بتعرف إن في داتا اتغيرت؟ وإزاي بتعرف إنها محتاجة تشغل كشف التغييرات (change detection)؟
خلينا نبدأ بمثال بسيط. عندنا (component) فيه خاصية اسمها name
ودالة اسمها changeName
. لما ندوس على الزرار، بنادي دالة changeName
اللي بتغير قيمة name
@Component({
template: `
<button (click)="changeName()">Change name</button>
<p>{{ name }}</p>
`,
})
export class AppComponent {
name = "John";
changeName() {
this.name = "Jane";
}
}
لما بندوس على الزرار، changeName بتشتغل وبتغير القيمة بتاعت name من "John" لـ "Jane". لأن Angular بتحاوط كل حاجة بآلية كشف التغييرات، فإحنا نقدر نفترض بأمان إنه بعد تغيير الاسم، Angular هتشغل كود عشان تحدث العرض وتخلي كل حاجة متزامنة (in sync).
مثال للكود اللي Angular ممكن تشغله وراء الكواليس:
تخيل إن Angular شغلت كود زي ده ورا الكواليس:
component.changeName();
angular.runChangeDetection();
ده شغال تمام في الحالة البسيطة دي، عشان كل التغييرات بتحصل في نفس اللحظة (synchronously)، وده بيخلي Angular تشغل كشف التغييرات بدون مشاكل.
لكن في أغلب الوقت، لما بنغير الداتا، الموضوع مش بيحصل بشكل متزامن. غالباً بنبعت طلب HTTP، أو بنعتمد على مؤقتات (timers)، أو بنستنى أحداث تانية تحصل قبل ما نحدث الداتا. وده هنا اللي بيسبب المشاكل!
لما يكون التغيير مرتبط بأحداث غير متزامنة (async)، Angular مش هتقدر تعرف لوحدها إن في حاجة اتغيرت. عشان كده بتعتمد على Zone.js (لو مش شغالين بزونليس)، اللي بيراقب الأحداث زي طلبات HTTP أو مؤقتات، وبيخبر Angular لما يحصل تغيير في الداتا، عشان تشغل كشف التغييرات وتحدث العرض.
لكن استخدام الأحداث غير المتزامنة مع Angular بيخلي الأمور معقدة أكتر وبيحتاج انتباه، عشان نتأكد إن Angular دايماً في حالة تحديث (in sync) مع التغييرات.
في المثال ده، إحنا عاوزين نغير قيمة name
بعد ثانية واحدة، وهنعمل ده باستخدام setTimeout
.
@Component({
template: `
<button (click)="changeName()">Change name</button>
<p>{{ name }}</p>
`,
})
export class AppComponent {
name = "John";
changeName() {
setTimeout(() => {
this.name = "Jane";
}, 1000);
}
}
لما ندوس على الزرار، changeName
بتتنادي وsetTimeout بيشتغل. setTimeout
هتستنى ثانية، وبعدها هتنفذ اللي جواها عشان تغير قيمة name
لـ "Jane".
خلينا نضيف نفس الكود اللي تخيلنا إنه شغال ورا الكواليس في Angular:
component.changeName();
angular.runChangeDetection();
بسبب ترتيب الكود في stack، الـ callback بتاع setTimeout مش هيتنفذ غير بعد ما Angular تكون شغلت runChangeDetection. فبالتالي، Angular هتشغل كشف التغييرات، لكن name لسه ما اتغيرتش، وده هيخلي (view) ما يتحدثش، عشان كده مش هنشوف اسم "Jane" على الشاشة بعد ما نضغط الزرار. وده بيسبب مشكلة في التطبيق
لو فاكرين، Angular بتستخدم Zone.js، اللي بيراقب كل الأحداث غير المتزامنة زي setTimeout. فلما setTimeout ينفذ بعد ثانية ويغير name، Zone.js هيلاحظ التغيير ويخطر Angular إنها تشغل runChangeDetection مرة تانية. وده هيخلي (view) يتحدث ويظهر القيمة الجديدة "Jane".
الZone.js موجودة من أيام Angular 2.0، وهي مكتبة بتعمل حاجة اسمها "monkey patching" لمجموعة من واجهات المتصفح (browser APIs) وتسمح لنا ندخل في دورة حياة (lifecycle) الأحداث اللي بتحصل في المتصفح. طيب إيه معنى الكلام ده؟ ببساطة، ده معناه إننا نقدر نشغل كود معين قبل أو بعد أحداث معينة بتحصل في المتصفح.
خلينا نشوف مثال بسيط:
setTimeout(() => {
console.log("Hello world");
}, 1000);
هنا بتيجي Zone.js بدورها. بتسمح لنا نعمل "zone" (ودي حاجة Angular نفسها بتستخدمها) ونتدخل في دورة حياة الـ setTimeout.
مثال مع Zone.js هنعمل زون جديدة ونضيف عمليات قبل وبعد تنفيذ setTimeout
const zone = Zone.current.fork({
onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => {
console.log("Before setTimeout"); // الكود ده هيشتغل قبل `setTimeout`
delegate.invokeTask(target, task, applyThis, applyArgs); // هنا بيشغل `setTimeout`
console.log("After setTimeout"); // الكود ده هيشتغل بعد `setTimeout`
},
});
عشان نشغل setTimeout
داخل الزون، محتاجين نستخدم zone.run
زي كده:
zone.run(() => {
setTimeout(() => {
console.log("Hello world");
}, 1000);
});
الناتج لما نشغل الكود ده، هنشوف التالي في الـ console
Before setTimeout
Hello world
After setTimeout
اللي Zone.js بتعمله هو إنها بتعدل في الطريقة اللي المتصفح بيشغل بيها الـ APIs زي setTimeout وsetInterval وغيرها. ده بيسمح ل Angular إنها "ترصد" التغييرات اللي بتحصل في الـ components وتحدث UI بشكل متزامن مع الأحداث، وده بيضمن إن كل حاجة في التطبيق شغالة بتناغم وكفاءة.
الZone.js و Angular: إزاي Angular بتستخدم NgZone في التطبيقات الAngular بتشغل Zone.js تلقائيًا في كل تطبيق، وبتعمل حاجة اسمها NgZone، وده نوع من الـ zones المخصصة اللي بتراقب التطبيق. NgZone فيها حاجة اسمها onMicrotaskEmpty، ودي عبارة عن Observable، يعني زي عداد بيتابع الأحداث اللي بتحصل في التطبيق. الـ Observable ده بيبعث قيمة لما مفيش أي مهام صغيرة (microtasks) فاضلة في الطابور. وده اللي Angular بتستخدمه عشان تعرف إن كل الكود غير المتزامن (asynchronous code) خلص، وتقدر تشغل كشف التغييرات (change detection) بأمان.
NgZone wraps every angular application today
في Angular، لما يحصل تغيير جوه (component)، بتقوم بتمييز component كـ "متسخ" (dirty)، وده معناه إن component محتاج يتحدث عشان يظهر التغييرات الجديدة. فكرة "تمييز component كـ dirty" بتساعد Angular إنها تحدد إيه اللي محتاج يتحدث بدل ما تحدث كل حاجة في التطبيق.
-
الأحداث (Events زي click، mouseover، إلخ) كل مرة بندوس على زرار أو بنعمل حدث معين في (template) عنده مستمع (listener)، Angular بيلف الدالة المستجيبة (callback function) بدالة تانية اسمها wrapListenerIn_markDirtyAndPreventDefault. زي ما الاسم بيوضح 😅، الدالة دي بتعلّم Component كـ dirty.
-
تغييرات (Changed Inputs) أثناء تشغيل كشف التغييرات، Angular بتشيك إذا كان فيه تغيير حصل في قيمة (input value) الخاصة بcomponent. لو Inputs اتغير، Angular بتميّز component كـ dirty. وده بيخلي component يعرف إنه محتاج يتحدث.
-
الـ Output Emissions عشان تستمع للـ outputs في Angular، بنسجل حدث في template. وزي ما شفنا قبل كده، الـ callback بتتلف بدالة تانية، ولما يتم استدعاء الحدث، component بيتعلّم كـ dirty.
هي المسؤولة عن تمييز (view) الحالي وكل الأجداد (ancestors) كـ dirty.
دي بتعمل تمييز للview الحالي، وكمان لكل Components اللي فوقه (الأجداد) كـ dirty. وده معناه إن لما يحصل تغيير، Angular بتعرف إنها مش بس محتاجة تحدث Component ده، لكن كمان كل الأجزاء اللي بتعتمد عليه.
Dirty marking component and its ancestor up to the root
لما ندوس على الزر، Angular بتستدعي callback اللي هي هنا changeName
. وبما إن دي ملفوفة داخل دالة wrapListenerIn_markDirtyAndPreventDefault
، فده بيخلي Angular تميّز الـ component كـ dirty.
زي ما قولنا قبل كده، Angular بتستخدم Zone.js وبتحاوط التطبيق كله بيه.
Dirty marking component and its ancestor up to the root
التمييز كـ Dirty: لما wrapListenerIn_markDirtyAndPreventDefault
تشتغل، بتقوم بتمييز الـ component وكل الأجداد كـ dirty، وبكده Angular بتعرف إن في حاجة اتغيرت في الـ component أو الـ components اللي فوقه.
تنبيه Zone.js: بعد تمييز الـ components، wrapListenerIn_markDirtyAndPreventDefault
بتشغل Zone.js.
الonMicrotaskEmpty: بما إن Angular بتراقب الـ observable onMicrotaskEmpty، وأي حدث (زي click) بيسجل listener مع Zone.js، فالـ zone هيعرف إن الـ listener خلص شغله ويقدر يبعث قيمة لـ onMicrotaskEmpty.
إطلاق الـ onMicrotaskEmpty: لما onMicrotaskEmpty يشتغل، ده بيقول لـ Angular إن كل المهام الصغيرة (microtasks) خلصت، وبالتالي الوقت مناسب لتشغيل change detection.
أول ما Angular تشغل change detection، بتبدأ تراجع كل الـ components من فوق لتحت. يعني بتمر على كل الـ components (سواء كانت marked كـ dirty أو لا) وبتشيك على كل الـ bindings. لو الـ binding اتغير، بيتم تحديث الـ view.
لكن ليه Angular بتشيك على كل الـ components 🤔؟ ليه ما تراجعش الـ components اللي marked كـ dirty بس؟ 🤔
الإجابة هنا ليها علاقة بـ استراتيجية change detection
في Angular، فيه استراتيجية اسمها OnPush لـ change detection. لما نستخدم الاستراتيجية دي، Angular هتشغل change detection فقط على الـ component اللي متعلم كـ dirty.
@Component({
// ...
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCard {}
دلوقتي خلينا نشوف الرسوم التوضيحية عشان نفهم أكتر إزاي change detection بيشتغل لما نستخدم استراتيجية OnPush.
بعض الـ components هتكون معلمة كـ OnPush (وأي children ليها هيبقوا تلقائيًا كمان OnPush).
خلينا نعمل نفس الخطوة اللي فاتت، ندوس على زرار في الـ component ونغير قيمة name
.
- مرحلة Dirty Marking
- إشعار Zone.js بالـ Event بعدها، الـ event listener هيشعر Zone.js بالحدث.
- انتظار انتهاء الكود غير المتزامن لما كل الكود غير المتزامن يخلص، onMicrotaskEmpty هتشتغل.
- تشغيل tick في Angular بعد كده، Angular هتشغل tick وتبدأ تمر على كل الـ components من فوق لتحت وتشيك عليهم.
لو الـ component حالته كانت:
لوOnPush + Non-Dirty -> تتجاهله لوOnPush + Dirty -> تشيك الـ bindings -> تحدث الـ bindings -> تشيك الـ children
الفائدة من OnPush باستخدام OnPush، Angular تقدر تتخطى أجزاء من الشجرة اللي مفيهاش أي تغييرات، وده بيقلل من تكلفة التحديث ويحسن أداء التطبيق.
في الAngular، observables بقت الأداة الأساسية لإدارة البيانات وتحديث الحالة (state changes). ولأن Angular بتدعم observables، هي بتوفر async pipe اللي بيعمل اشتراك (subscription) في الـ observable ويرجع آخر قيمة.
عشان Angular تعرف إن القيمة اتغيرت، async pipe
بيستخدم markForCheck
اللي جاية من كلاس ChangeDetectorRef
.
الكود التالي بيوضح الأساس اللي async pipe بيشتغل عليه:
@Pipe()
export class AsyncPipe implements OnDestroy, PipeTransform {
constructor(ref: ChangeDetectorRef) {}
transform<T>(obj: Observable<T>): T | null {}
private _updateLatestValue(async: any, value: Object): void {
// code removed for brevity
this._ref!.markForCheck();
}
}
الasync pipe بيتابع الـ observable، وأول ما يحصل تغيير في القيمة، بيشغل markForCheck. الطريقة دي هي اللي بتعلّم الـ component كـ dirty، وده بيخلّي Angular تعرف إنه محتاج تحديث.
باستخدام async pipe مع الobservables، Angular بتقدر تتأكد من إن الـ component بيتحدث تلقائيًا لما البيانات تتغير، بدون الحاجة لاستخدام .subscribe بشكل مباشر في الكود. وبما إن markForCheck بيشتغل مع OnPush، Angular بتعرف إنها محتاجة تشيك فقط على الـ components اللي اتعلمت كـ dirty، وده بيخلي التحديثات أكتر كفاءة وأقل تكلفة.
في حالة إن البيانات اتغيرت بدون أي event مباشر (زي click أو mouseover)، غالبًا بيكون فيه حاجة شغالة في الخلفية زي setTimeout أو setInterval أو استدعاء HTTP بيحفز Zone.js.
@Component({
selector: "todos",
standalone: true,
imports: [AsyncPipe, JsonPipe],
template: `{{ todos$ | async | json }}`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodosComponent {
private http = inject(HttpClient);
private ngZone = inject(NgZone);
todos$ = of([] as any[]);
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
// هنا البيانات هتتحدث، لكن مفيش حاجة هتشغّل Zone.js
this.todos$ = this.getTodos();
});
});
}
getTodos() {
return this.http
.get<any>("https://jsonplaceholder.typicode.com/todos/1")
.pipe(shareReplay(1));
}
}
في ngOnInit
، استخدمنا ngZone.runOutsideAngular
، وده API بيسمح لنا نشغّل حاجات خارج نطاق Angular zone.
استخدمنا setTimeout (عشان نتخطى أول عملية تكون شغالة وكمان عشان Angular بيشغّل change detection على الأقل مرة واحدة بشكل تلقائي)، وفي داخل setTimeout، أسندنا قيمة جديدة للـ observable (todos$).
بما إن setTimeout شغّال خارج نطاق Angular zone، كمان استدعاء الـ API هيشتغل خارج الـ zone عشان الكود كله جوه runOutsideAngular. وبكده، مفيش حاجة بتنبّه Zone.js إن فيه حاجة اتغيرت.
لما تشغّل الكود ده في التطبيق، هتلاقي إن اللي بيظهر في المتصفح هو “[]” بس.
ده وضع غير مثالي 😄، وبيفتح لنا تساؤل تاني: ازاي Angular تقدر تتعامل مع التغييرات اللي بتحصل خارج نطاق Zone.js، وخصوصًا مع استراتيجية OnPush؟
في الحالة دي، نقدر نستخدم ChangeDetectorRef
وندعي markForCheck
بعد التحديث عشان نجبر Angular على تشغيل change detection على الـ component ده بالتحديد، وده هيضمن إن التحديثات بتظهر في العرض حتى لو التغييرات حصلت خارج Zone.js.
ممكن نعمل كده باستخدام detectChanges في كلاس ChangeDetectorRef، لكن ليها مشاكلها. لأن detectChanges بيشغل change detection بشكل متزامن، وده ممكن يسبب مشاكل في الأداء. لأن كل حاجة هتتنفذ في نفس مهمة المتصفح (browser task)، وده ممكن يسبب "performance issues" في الـ main thread ويخلي الأداء متقطع (jank).
تخيل مثلاً إنك بتشيك على التغييرات في قائمة فيها 100 عنصر كل ثانية أو ثانيتين، ده هيكون عبء كبير على المتصفح
الmarkForCheck: لما بنستخدم markForCheck، إحنا بس بنقول لـ Angular إن فيه component متعلم كـ dirty، لكن مفيش أي تحديث بيحصل في اللحظة دي. حتى لو دعينا markForCheck 1000 مرة، مش هيبقى فيه مشكلة، لأن التحديث الفعلي هيحصل في دورة الـ change detection التالية، وده بيوفر أداء أفضل.
الdetectChanges: لما نستخدم detectChanges، Angular بتعمل الشغل الحقيقي فورًا، زي إنها تراجع الـ bindings وتحدث العرض (view) لو في حاجة محتاجة تتحدث. وده ممكن يسبب ضغط على الأداء، وخصوصًا لو عندنا مكونات كتيرة أو تحديثات متكررة.
الmarkForCheck بيسمح لـ Angular إنها تشتغل بكفاءة أعلى عن طريق تأجيل التحديثات لحد ما تكون جاهزة لتشغيل change detection بشكل مجمع (coalesced run) بدل ما يكون فيه تحديثات متزامنة (sync runs) لكل component منفصل. وده بيخلي أداء التطبيق سلس أكتر ويقلل الضغط على الـ main thread في المتصفح.
الـ Signals في Angular جابت تحسينات كبيرة في تجربة المطورين. نقدر بسهولة نخلق ونعمل اشتقاق للـ state وكمان نشغل side effects لما الـ state يتغير باستخدام الـ effects. مش محتاجين نعمل اشتراك ولا إلغاء اشتراك فيها، ومش محتاجين نقلق من مشاكل الـ memory leaks 🧯.
const name = signal("John");
const upperCaseName = computed(() => name().toUpperCase());
effect(() => {
console.log(name() + " " + upperCaseName());
});
setTimeout(() => {
name("Jane");
}, 1000);
// Output:
// John JOHN
// Jane JANE
@Component({
template: `
<button (click)="name.set('Jane')">Change name</button>
<p>{{ name() }}</p>
`
})
export class AppComponent {
name = signal('John');
}
في الإصدار الجديد من Angular v17، حصل تطوير في موضوع الـchange detection أو "كشف التغيرات" في الـtemplates، اللي بيخليها تستجيب لأي تحديثات في البيانات تلقائيًا بطريقة أسهل وأسرع.
زمان كنا بنستخدم حاجة اسمها async pipe عشان نقدر نستجيب لتحديثات البيانات في الـtemplate. يعني لو في قيمة بتتغير، الـasync pipe كان بيخلي Angular يعرف إن في تحديث ويعمل markForCheck للـtemplate عشان يرندر التغييرات دي. بس دلوقتي مع الإصدار الجديد، Angular بقى بيفهم إن الإشارات (signals) اللي بنستدعيها في الـtemplate مش مجرد استدعاءات دوال، لأ بقى بيشوفها كحاجات تستاهل المتابعة. وبالتالي، لو استخدمنا signal، Angular أوتوماتيك هيسجل حاجة اسمها effect (أو consumer) عشان يرائب الـsignal دي ويعمل markForCheck كل ما يحصل أي تغيير في قيمتها.
مش محتاجين الـasync pipe: دلوقتي بقى سهل تستدعي الـsignal في الـtemplate عادي من غير ما تضطر تستخدم async pipe.
أداء أسرع: الـsignals دلوقتي بتوفر أداء أفضل لإن الـchange detection بيحصل في الأماكن اللي محتاجينها بس.
لما signal ياخد قيمة جديدة في الـ template بتاعة الـ component، اللي بيحصل إنه بيقول لـ reactive consumer (اللي هو الكود اللي بيتابع التغيرات) إنه فيه تغيير محتاج يتشاف. لكن هنا التركيز على حاجة معينة: اللي بيتم تعليمه كأنه "dirty" أو محتاج تحديث هو reactive consumer اللي متعلق بالـ component view مش component view نفسها. يعني مش الكومبوننت ككل اللي بيتعلم كأنه عايز يتحدث، زي اللي بيحصل في الحالة العادية لو كنت بتستخدم OnPush.
ببساطة، reactive consumer هو كود أو جزء من البرنامج وظيفته إنه "يراقب" signals أو بيانات معينة عشان يشوف لو حصل عليها تغيير، ويقرر إزاي يتصرف بناءً على التغيير ده. يعني هو "المستهلك" للتحديثات اللي بتحصل على البيانات، وبيشتغل بطريقة reactive (تفاعلية).
تخيل عندك تطبيق فيه قائمة مهام، وكل ما المستخدم يضيف مهمة جديدة أو يحذف مهمة، عايز القائمة تتحدث تلقائيًا. هنا، الـ reactive consumer بيكون الجزء من الكود اللي "منتظر" يشوف لو فيه تغيير في قائمة المهام عشان يظهرها أو يخفيها من الشاشة. لما يحصل تغيير (زي إضافة مهمة جديدة)، الـ reactive consumer يعرف إنه فيه حاجة جديدة وبيتعلم إنه "dirty" أو محتاج يتحدث عشان يعرض التغيير.
الفكرة هنا إنك مش عايز التطبيق يعيد بناء كل الكومبوننت أو الـ UI من الأول في كل مرة يحصل فيها تغيير بسيط. بدل كده، بنستفيد من إن عندنا reactive consumers، بحيث يقدروا يتابعوا البيانات المطلوبة ويحدثوا بس الأجزاء المتأثرة. ده بيساعدنا نحسن الأداء، لأننا مش بنعيد بناء الكل، بس بنركز على اللي اتغير.
الComponent view هي الشكل أو (View) اللي بيظهر للمستخدم من component معين في Angular. كل component بيكون ليه "view" خاص بيه، وده اللي بيعرض واجهة المستخدم (UI) اللي مرتبطة بالكومبوننت ده.
الـ Component View هو الجزء اللي Angular بيعمل عليه "فحص للتغييرات" (change detection)، بحيث لما يحصل تغيير على البيانات اللي مرتبطة بالـ view، بيتم تحديثها على الشاشة للمستخدم بدون الحاجة لتحديث الكومبوننت بالكامل. الفكرة دي بتساعد في تحسين أداء التطبيق لأنك مش مضطر تعمل إعادة بناء لكل الكومبوننتات مرة واحدة، لكن بس الجزء اللي فعلاً حصل فيه تغيير.
قبل إصدار Angular 17، لما الـ reactive consumer كان بيتعلم إنه "dirty"، ده كمان كان بيخلي الـ component view نفسها تتعلم كأنها محتاجة تحديث. وده بيخلي الطريقة شبيهة باللي بتعمله AsyncPipe: بيشوف لو فيه تحديث، ويعمل التحديث على مستوى الكومبوننت ككل.
بالتالي، لو عايز تطبق الاستراتيجية دي وتقلل الكشف على التغيرات (عشان تتحسن الكفاءة)، كنت هتستخدم OnPush على كل الـ components، بحيث الكشف على التغيرات يتم على مسار واحد بس، ومش بيشمل كل مكونات التطبيق مرة واحدة.
مع إصدار Angular 17 والأحدث، الوضع اتغير شوية. لما الـ reactive consumer يتعلم كأنه "dirty"، ده مش بيأثر على component view نفسها. بدل من كده، فيه دالة جديدة اسمها markAncestorsForTraversal بتشتغل. الدالة دي بتعمل حاجة شبيهة، لكنها بتمشي من الـ component لغاية فوق عند كل الـ components الجدود (من الـ component الحالي لغاية الكومبوننت الرئيسية اللي فوق الكل).
لكن الفرق هنا إن الدالة دي مش بتعلمهم إنهم كلهم محتاجين تحديث، لا! هي بتعلم بس الجدود إن عندهم مكون فرعي (child component) محتاج تحديث. وبتديهم علامة اسمها HasChildViewsToRefresh كإشارة إنه فيه تغيير جاي من تحت، لكن مش بتعمل تحديث فعلي غير لما يوصل عند الـ component اللي فيه التغيير الفعلي.
التقنية الجديدة للتأكد من التغيير (change detection) اتحدثت. دلوقتي لما العملية تبدأ والشجرة في الحالة دي، بيعدي على الـcomponents A وE من غير ما يعمل لهم change detection، وده لأنهم OnPush لكن مش dirty. بفضل الـHasChildViewsToRefresh flag، Angular بيكمل يزور النودز اللي معمول لها علامة بالـflag ده ويدور على الـcomponent اللي محتاج change detection (في مثالنا هنا هو اللي فيه reactive consumer ومتعلّم عليه إنه dirty). لما يوصل للـcomponent F، يلاقي إن الـreactive consumer بتاعه dirty، فـcomponent ده بس اللي بيحصل له change detection – وده component الوحيد!
حلو، صح؟ بدل ما يعمل change detection للمسار الكامل بتاع الـcomponents، دلوقتي بقى بيعمله component واحد بس. ده مثال مبسط لشجرة components، لكن المكاسب في الأداء في التطبيقات الفعلية أكبر بكتير.
النهج الجديد في الـchange detection، اللي بيخلي component واحد بس يحصل له change detection بفضل الـsignals، بيطلق عليه "شبه محلي" أو “global-local/glocal” change detection.
🔴 تحذيرات لكن في شوية تحذيرات لازم تاخد بالك منها. خلينا نبص على مثال، لما ضغط المستخدم على الزرار وتسبب في تغيير إشارة signal:
المشكلة بتحصل لو التغيير جاي من حاجة معينة، زي لما المستخدم يدوس على زرار، وده بيعمل تغيير في البيانات. في الحالة دي، Angular بيطبّق القواعد القديمة وبيعتبر إن الوحدة نفسها وكل اللي فوقها في الشجرة dirty، وده بيرجعنا للطريقة التقليدية اللي بتفحص الوحدات كلها، حتى لو بس وحدة واحدة محتاجة تحديث.
لو التغيير في البيانات جاي من حاجة زي setTimeout أو Observable، ساعتها Angular هيشتغل بالنظام الجديد ويفحص الوحدات اللي محتاجة فقط. لكن لو التغيير سببه حاجة زي ضغط زرار، النظام القديم بيشتغل وبيعمل فحص لكل الوحدات اللي فوق الوحدة دي.
لو في وحدات مش بتستخدم نظام OnPush، فحتى مع النظام الجديد، الوحدات دي هيتعمل لها فحص كل مرة، لأنها مش متعلّم عليها بالنظام الجديد.
لو عندنا شجرة فيها وحدات زي A، B، C، وD، وكلهم مش شغالين بـOnPush، والوحدة F فيها تغيير بسيط أو تحديث، اللي هيحصل هو إن مش بس الوحدة F اللي هيتعمل لها change detection، لكن كمان الوحدات اللي في الشجرة ABCD هيتعمل لها فحص كامل في نفس الوقت. السبب إن الوحدات اللي مش بتستخدم OnPush بتشتغل على النظام الافتراضي اللي بيفحصها كل مرة يحصل فيها أي تغيير في التطبيق، حتى لو مش محتاجة.
ده بيأثر على كفاءة التطبيق، لأنه بيزود عدد الوحدات اللي بيتم فحصها مع كل تغيير، حتى لو التغيير حصل في جزء بعيد عنها. وبالتالي، وجود وحدات مش بتستخدم OnPush بيلغي جزء من الفائدة اللي بنحصل عليها من نظام الـsemi-local change detection، لأنه بيخلينا نفحص وحدات أكتر من اللي محتاجة فعلاً.
ليه نشيل Zone.js أصلاً؟ خلينا نفكر الأول ليه ممكن نحب نتخلص من Zone.js. فيه شوية أسباب:
تقليل حجم الـ bundle في البداية: بص كده على الـ outputs اللي قدامك (واحد Zone-full وواحد Zoneless). زي ما انت شايف، Zone.js حجمه حوالي 30kB خام، وبيقل لـ 10kB لما يتضغط. ده حجم مش قليل، خصوصاً إنه بيدخل ضمن الحاجات اللي لازم تتحمل قبل ما التطبيق يبدأ يشتغل.
تجنب الـ change detection اللي ملهاش لازمة: Zone.js بيساعد في إن Angular يعمل change detection من خلال إنه بيبلغه لما أي عملية تخلص. لكن هو في الحقيقة مش بيعرف إذا كانت العمليات دي غيرت أي داتا ولا لأ. عشان كده، الفريم وورك بيبالغ شوية وبيعمل run "للاحتياط".
عايز أوضح حاجة – Zone.js كان أداة رهيبة (أو على الأقل كان كده لما بدأ) وساهم كتير في نجاح Angular من الأول. كان بيخلي التعامل مع الفريم وورك سهل حتى للمطورين المبتدئين – يركزوا بس على شغلهم، وكل حاجة تشتغل بطرق سحرية.
لكن، زي ما شوفنا، السحر ده له تكلفة. متأكد إن كتير من التطبيقات شغالة كويس جداً مع Zone.js. بس في حالات التطبيقات المعقدة اللي محتاجة أداء أعلى، أكيد البحث عن بدائل خطوة منطقية.
الـ "Zoneless Scheduler" الجديد اللي تم تقديمه في الإصدار 17.1 بيغير طريقة عمل الـ "Zone.js" events وبيستنى لحد ما يجي له إشعار صريح من أجزاء تانية في الفريمورك عن أي تغييرات. التغيير ده مهم لأنه بدل ما يشغل عملية الـ "change detection" لما "أي عملية تحصل وخلاص"، النظام بقى يشغله "لما يوصل له إشعار أن فيه بيانات اتغيرت".
عشان يحقق ده، الجدول الجديد بيعرض طريقة خاصة اسمها "notify" وبتتدعي في الحالات دي:
لما "signal" اللي بيتقرا في الـ "template" يوصل له قيمة جديدة (بالتحديد، لما "markAncestorsForTraversal" تتدعي).
لما الـ "component" يتعلم أنه بقى "dirty" عن طريق "markViewDirty". ده ممكن يحصل بسبب وصول قيمة جديدة من "AsyncPipe"، أو "template-bound event"، أو عن طريق استدعاء "ComponentRef.setInput"، أو استدعاء "ChangeDetectorRef.
markForCheck" بشكل مباشر، أو حاجات تانية.
لما "afterRender hook" يتسجل، أو لما "view" يتضاف تاني للـ "change detection tree" أو يتشال من الـ "DOM". في الحالات دي، "notify" بتتدعي لكنها بس بتنفذ الـ "hooks" من غير ما تعمل "refresh" للـ "view".
ممكن تسأل لو "change detection" ممكن يشتغل كتير لو "signals" كتيرة اتغيرت في نفس الوقت أو لو حصلت "events" بسرعة ورا بعض. لكن التنفيذ بتاع الـ "scheduler" معمول عشان يتعامل مع ده بكفاءة. بيجمع الإشعارات على مدار فترة قصيرة وبيعمل "schedule" لتشغيل "change detection" مرة واحدة بدل ما يشغله كذا مرة. السلوك ده مبني على سباق بين "setTimeout" و "requestAnimationFrame"، بس مش هندخل في التفاصيل دي هنا. المهم إن "notify" دي بتتجمع، عشان تضمن أحسن أداء.
في أنجولار، بقت في طريقة جديدة لكتابة الcomponents والdirectives والpipes، والطريقة دي اسمها Signal Components، وهي أبسط وأقوى من الطريقة التقليدية.
الSignal Components هي بديل كامل للdecorators زي Input
و Output
. كمان بتوفر طريقة جديدة لعمل two-way binding (التواصل رايح جاي). والمفروض إن دي تبقى الطريقة المفضلة اللي تكتب بيها الcomponents الجديدة في أنجولار من دلوقتي، وبالمناسبة هي جاهزة للاستخدام حالياً.
الميزة الأساسية في Signal Components إنها مش محتاجة decorators، وكمان مفيش أغلب lifecycle hooks اللي متعودين عليها، لأنها متكاملة بشكل عميق مع الإشارات (signals).
لو عايز تستفيد من Signal Components، لازم تحدث لنسخة Angular 17.3 أو أعلى.
في الدليل ده، حاشرح إزاي تكتب components بطريقة Signal، وإزاي تستخدم الأدوات الأساسية زي input و output و model عشان تبني Signal Components في أنجولار.
الـinput() function بقت بديل للـInput() decorator، وده بيعتبر طريقة جديدة في Angular للتعامل مع الـinputs.
لكن مهم تعرف إن Input() decorator لسه شغال وهيفضل مدعوم لفترة جاية.
لما نستخدم input، القيمة اللي بنحطها للـinput property بتتحول لحاجة اسمها Signal. بمعنى إن الـSignal ده بيحتفظ دايمًا بأحدث قيمة متاحة للـinput اللي جاية من الجزء الأب أو الـparent.
مثال عملي: هنشوف إزاي نعمل input property اسمها book في BookComponent
import { Component, input } from "@angular/core";
@Component({...})
class BookComponent {
book = input<Book>()
}
هنا استخدمنا input عشان نعمل input field اسمها book.
الفرق هنا إن input بيرجع قيمة نوعها InputSignal<Book>
. وده معناه إن book مش مجرد object عادي زي الطريقة القديمة باستخدام @Input، لكنها بقت حاجة بتحافظ دايمًا على آخر قيمة جايه من الجزء الأب.
لكن بالرغم من استخدام الطريقة الجديدة، من ناحية الجزء الأب اللي بيبعت البيانات للـBookComponent، مفيش أي تغيير. تقدر تبعت بيانات بالطريقة المعتادة:
<book [book]="angularCoreDeepDiveBook" />
angularCoreDeepDiveBook = {
title: "Angular Core Deep Dive",
synopsis: "A deep dive into Angular core concepts",
};
من ناحية الـ Parent Component، كل حاجة بتفضل زي ما هي.
لكن بالنسبة للـ Component نفسه، إزاي نقدر نجيب قيمة الـ input؟
علشان نقرا القيمة، محتاجين نستدعي الـ Signal بتاع الـ book، زي أي Signal تاني في Angular:
book();
السطر ده لما نستدعيه، هيجيب لنا أحدث قيمة موجودة في book signal، اللي فيها قيمة angularBook object.
خلي بالك إن Signals دايمًا بيكون ليها قيمة، فـ ()book هترجع لنا يا إما undefined لو مفيش حاجة، أو قيمة book object.
دلوقتي هنعدل الـ Component بتاعنا علشان نعرض title وsynopsis بتوع الكتاب في template بتاعه:
import { Component, input } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div> `,
})
class BookComponent {
book = input<Book>();
}
لاحظ إننا في template استدعينا الـ Signal بتاع ()book، وبعد كده دخلنا على الـ title وsynopsis properties.
استخدمنا ?. (الـ Optional Chaining) علشان لو الـ book ممكن يبقى undefined.
الكود ده شغال، لكن ممكن يبقى متعب شوية.
طب إيه اللي هيحصل لو إحنا متأكدين وضامنين إن قيمة book عمرها ما هتكون undefined؟
هنا بقى يظهر لنا الفرق بين نوعين من Signal Inputs اللي ممكن نستخدمهم في Angular:
بشكل افتراضي، inputs اللي بنعملها باستخدام ()input بتعتبر optional.
ده معناه إننا مش مضطرين نحدد قيمة للـ input لما نستخدم component ده من جوه parent component.
المثال اللي استخدمناه فوق مع BookComponent كان optional input.
وده معناه حاجتين مهمين:
أول حاجة، ممكن نستخدم BookComponent من غير ما نحدد قيمة للـ book input:
<book />
تاني حاجة، في الحالة دي، الـ book signal هيبقى قيمته undefined.
لو مش عايزين تبقى القيمة الافتراضية هي undefined، ممكن كمان نحدد قيمة مبدئية للـ optional input كالتالي:
const age = input<number>(0);
كده، القيمة المبدئية للـ age input signal هتبقى 0 بدل undefined.
في بعض الحالات، بنحتاج إن input properties تكون required بدل ما تبقى optional. وده بيكون مفيد لما نكون عايزين نضمن إن قيمة معينة هتكون موجودة دايمًا.
إزاي نعمل ده؟ ببساطة، بنستخدم input.required بالشكل ده:
import { Component, input } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book().title }}</b>
<div>{{ book().synopsis }}</div>
</div> `,
styles: ``,
})
class BookComponent {
book = input.required<Book>();
}
فيه شوية حاجات مهمة لما بنستخدم input.required
🔴 مش ممكن نحدد قيمة مبدئية للـ input signal. القيمة الأولية هتبقى هي القيمة اللي بنمررها للـ input من الـ parent component.
🔴 ماينفعش نهمل الـ book property في الـ parent component
<book />
لو عملنا كده، هيظهر خطأ في الـ compilation لأن الـ book input field مش optional دلوقتي!
علشان نحل المشكلة دي، لازم نمرر قيمة للـ book property لما نستخدم BookComponent:
<book [book]="angularBook" />
غالبًا، بنحب نخلي اسم input property هو نفسه اسم input signal.
لكن أحيانًا ممكن نحتاج ندي الـ input property اسم مختلف، وده نادرًا ما بيحصل، لكنه مفيد في بعض الحالات.
لو واجهت موقف زي ده، دي الطريقة اللي تقدر تعمل بيها input alias سواء للـ optional أو required inputs:
لعمل alias لـ optional input:
book = input<Book>(null, {
alias: "bookInput",
});
book = input.required<Book>({
alias: "bookInput",
});
إزاي نستخدم الـ input alias في parent component: لما نيجي نستخدم الـ alias في parent component، بيكون بالشكل ده:
<book [bookInput]="angularBook" />
لو حاولت تستخدم اسم الـ input property الأصلي بدل alias، زي كده:
<book [book]="angularBook" />
في بعض الحالات النادرة، ممكن نحتاج نعدل قيمة الـ input قبل ما تتخزن في input signal.
بنعمل كده عن طريق استخدام input transform، اللي بيخلينا نغير البيانات اللي جاية من parent component قبل ما تتخزن.
دي الطريقة اللي نقدر نحدد بيها input transforms سواء للـ optional أو required inputs:
للـ optional input:
book = input(null, {
transform: (value: Book | null) => {
if (!value) return null;
value.title += " :TRANSFORMED";
return value;
},
});
للـ required input:
book = input.required({
transform: (value: Book | null) => {
if (!value) return null;
value.title += " :TRANSFORMED";
return value;
},
});
قيمة خاصية transform لازم تكون pure function، يعني دالة بتشتغل بدون side effects (تأثيرات جانبية).
في الدالة دي بنكتب منطق تحويل القيمة اللي محتاجينه، ومهم جدًا إننا نرجع قيمة من الدالة علشان الـ transform يشتغل صح.
بما إن input في الأساس هو مجرد signal، نقدر نعمل بيه كل اللي بنعمله مع أي signal تاني، وده بيشمل حساب derived signal منه.
إزاي نقدر نعمل derived signal من input signal باستخدام computed:
import { Component, input, computed } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
<div>{{ bookLength() }}</div>
</div> `,
styles: ``,
})
class BookComponent {
book = input.required<Book>();
bookLength = computed(() => this.book().title.length);
}
في الكود ده، bookLength هو derived signal ناتج من book signal.
بمعنى، في أي وقت تتغير فيه قيمة book input signal، الـ bookLength signal هيتم حسابه من جديد.
كمان ممكن نستخدم effect علشان نراقب أي تغييرات بتحصل في book signal لو حابين.
خلوا في بالكم، input signal هو مجرد read-only signal، يعني مفيش حاجة خاصة بيه، تقدروا تعملوا عليه كل العمليات اللي ممكن تعملوها مع أي signal تاني.
استخدام signal inputs بدلًا من Input() decorator بيدينا فائدة مخفية وهي تبسيط عملية متابعة التغييرات في الـ input بدون الحاجة لاستخدام OnChanges lifecycle hook.
خلينا نوضح ده بمثال:
الطريقة التقليدية باستخدام Input@ و OnChanges: في الطريقة القديمة، لو عايزين نتابع تغييرات الـ input بنستخدم OnChanges بالشكل ده:
import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book?.title }}</b>
<div>{{ book?.synopsis }}</div>
</div> `,
})
class BookComponent implements OnChanges {
@Input() book: Book;
ngOnChanges(changes: SimpleChanges) {
if (changes["book"]) {
console.log("Book changed: ", changes.book.currentValue);
}
}
}
الطريقة الجديدة باستخدام signals وeffect
دلوقتي، مع نظام signal-based الجديد، مابقيناش محتاجين OnChanges lifecycle hook. بدل كده، بنستخدم effect لمتابعة أي تغيير يحصل في input signal بشكل مباشر:
import { Component, input, effect } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div> `,
styles: ``,
})
class BookComponent {
book = input.required<Book>();
constructor() {
effect(() => {
console.log("Book changed: ", this.book());
});
}
}
بدون الحاجة لأي lifecycle hook خاص، مجرد استدعاء effect بيكون كافي علشان يتابع أي تغييرات تحصل في input signal.
الـ output() API هو بديل مباشر للـ Output() decorator التقليدي في Angular.
رغم إن Output مش هيتم إيقافه، لكن استخدام output بيخلي كتابة الكود أكثر تناسقًا، خاصة لو كنا بنستخدم input، وبيقدم طريقة أكثر type-safe وأفضل تكاملًا مع RxJs مقارنة بالطريقة القديمة اللي بتستخدم EventEmitter.
إزاي نستخدم output لتحديد component output في Angular:
import { Component, output } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
<button (click)="onDelete()">Delete Book</button>
</div>`,
})
class BookComponent {
deleteBook = output<Book>();
onDelete() {
this.deleteBook.emit({
title: "Angular Deep Dive",
synopsis: "A deep dive into Angular core concepts",
});
}
}
من وجهة نظر parent component
بنفس طريقة event handling المعتادة، الـ parent component ممكن يستمع لحدث deleteBook بالشكل ده:
<book (deleteBook)="deleteBookEvent($event)" />
deleteBookEvent(book: Book) {
console.log(book);
}
زي ما قدرنا نحدد alias للـ signal inputs، كمان ممكن نحدد alias للـ output بالطريقة دي
deleteBook = output<Book>({
alias: "deleteBookOutput",
});
<book (deleteBookOutput)="deleteBookEvent($event)" />
زي ما وضحنا، الـ ()output مش مبني على signals، لكنه أكتر type-safe من الـ Output@ التقليدي، وبيوفر تكامل أفضل مع RxJs.
واحدة من الميزات القوية هي إننا نقدر بسهولة نعمل output signal بيصدر القيم من Observable.
علشان نعمل ده، بنستخدم outputFromObservable، ودي الطريقة:
import { Component } from "@angular/core";
import { outputFromObservable } from "@angular/core/rxjs-interop";
import { of } from "rxjs";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div>`,
})
class BookComponent {
deleteBook = outputFromObservable<Book>(
of({
title: "Angular Core Deep Dive",
synopsis: "A deep dive into the core features of Angular.",
})
);
}
🚀 الميزة هنا هي إننا نقدر نربط بين Observables وoutputs في Angular بشكل مباشر، وده بيسهل الشغل مع streams وتكامل RxJs، خصوصًا لو بنعتمد على reactive programming.
زي ما قدرنا نحول Observable لـ output باستخدام outputFromObservable، ممكن كمان نحول output إلى Observable باستخدام outputToObservable.
دي الطريقة:
import { Component } from "@angular/core";
import { output, outputToObservable } from "@angular/core/rxjs-interop";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
<button (click)="onDelete()">Delete Book</button>
</div>`,
})
class BookComponent {
deleteBook = output<Book>();
deleteBookObservable$ = outputToObservable(this.deleteBook);
constructor() {
this.deleteBookObservable$.subscribe((book: Book) => {
console.log("Book emitted: ", book);
});
}
onDelete() {
this.deleteBook.emit({
title: "Angular Core Deep Dive",
synopsis: "A deep dive into Angular core concepts",
});
}
}
استخدمنا outputToObservable لتحويل deleteBook output إلى Observable اسمه deleteBookObservable$.
دلوقتي أي قيمة بيصدرها deleteBook output، هيتم استقبالها برضه في deleteBookObservable$.
استخدمنا subscribe لمتابعة القيم الصادرة وطباعة أي book object جديد في الكونسول.
🚀 الميزة هنا هي إننا نقدر نشتغل مع outputs كـ Observables، وده بيسهل التكامل مع RxJs وreactive programming.
بالإضافة إلى ()input
و ()output
، أضافت Angular API جديدة اسمها ()model
، واللي بنستخدمها لإنشاء model inputs.
الModel Input هو نوع خاص من inputs بيكون writeable، يعني قابل للقراءة والكتابة! وده معناه إننا بنقدر نعمل two-way data binding بين الـ parent component والـ child component.
ببساطة، بيسمح للـ parent component إنه يمرر بيانات للـ child component، وفي نفس الوقت، child component يقدر يرجع بيانات للـ parent component.
مثال على استخدام ()model
خلينا نشوف مثال يوضح استخدام ()model
في two-way binding:
import { Component, model } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<input [(ngModel)]="bookModel().title" />
<div>{{ bookModel().synopsis }}</div>
<button (click)="updateBook()">Update Book</button>
</div>`,
})
class BookComponent {
bookModel = model<Book>();
updateBook() {
const updatedBook = {
...this.bookModel(),
title: "Updated Title",
};
this.bookModel.set(updatedBook); // Emit updated data to the parent
}
}
الـ parent component ممكن يمرر قيمة للـ model باستخدام [(bookModel)] بالطريقة المعتادة. child component يقدر يقرأ أو يعدل قيمة الـ model input، ولما يعمل تعديل باستخدام ()set, البيانات الجديدة بتترجع للـ parent component مباشرة.
من منظور parent component:
<book [(bookModel)]="parentBook"></book>
في بعض الحالات، مثلًا لو عندنا date picker component بيحتوي على قيمة رئيسية (زي قيمة date نفسها)، ()model
ممكن يكون مفيد جدًا. في المثال ده:
الparent component هيحدد initial value للتاريخ.
الchild component يقدر يرجع القيم المحدثة للـ parent لما المستخدم يغيرها.
✨ومع ذلك، في الغالب:
يفضل استخدام inputs وoutputs العادية لأنها أكثر وضوحًا وأسهل في الفهم.
ال()model
لو تم استخدامها بدون سبب قوي، ممكن تؤدي لكتابة كود صعب الفهم وصعب تتبعه أثناء الـ debugging.
تخيل إنك بتستخدم model input عبر عدة مستويات من components متداخلة، هيبقى صعب تعرف مصدر قيمة معينة أثناء تتبع الأخطاء.
في Angular v19 حيكون فيه طريقة جديدة تقدر من خلالها تجيب بيانات من سيرفر، وتعرف إذا كانت العملية شغالة أو خلصت، وكمان تقدر تغير البيانات اللي عندك بشكل مباشر لما تحتاج. الطريقة دي بتسهل عليك متابعة الحالة، يعني لو البيانات لسه ما وصلتش، أو حصلت مشكلة، أو حتى لو وصلت وعايز تعدل عليها، كله حيكون واضح وأسهل في التعامل. الفكرة إنها بتنظم العملية كلها وتخليك تركز على شغلك من غير تعقيد.
🔴 الـ Resource API الجديدة في Angular فكرتها بسيطة جدًا. خلينا نبص على أبسط مثال لاستخدامها.
import { resource } from "@angular/core";
@Component({})
export class MyComponent {
todoResource = resource({
loader: () => {
return Promise.resolve({ id: 1, title: "Hello World", completed: false });
},
});
constructor() {
effect(() => {
console.log("Value: ", this.todoResource.value());
console.log("Status: ", this.todoResource.status());
console.log("Error: ", this.todoResource.error());
});
}
}
إحنا بنستخدم resource
عشان نجيب بيانات، وهنا بنستعمل Promise بسيط جدًا كـ مثال.
الـ resource
بيرجع حاجة اسمها WritableResource
. ده نوع بيساعدنا إننا نحدث البيانات لو احتجنا.
المميز في النوع ده إنه مش بس بيخلينا نقرأ البيانات (زي القيمة الحالية أو الحالة)، لكن كمان بنقدر نعدل عليها يدويًا لو احتجنا، من غير ما نستنى الـ loader يشتغل من جديد.
عشان نعرف القيمة اللي جت من الـ Resource
بنستخدم ()value
عشان نجيب البيانات الحالية.
بنستخدم ()status
عشان نعرف حالة البيانات (مثلاً: بتتحمل ولا خلصت).
بنستخدم ()error
لو حصلت مشكلة.
Value: undefined
Status: 'loading'
Error: undefined
Value: { id: 1, title: "Hello World", completed: false }
Status: 'resolved'
Error: undefined
الValue: undefined (لإن البيانات لسه ما وصلتش).
الStatus: 'loading' (معناها إن البيانات لسه في مرحلة التحميل).
الError: undefined (لإن مفيش أي مشاكل).
الValue: { id: 1, title: "Hello World", completed: false }
(البيانات اللي رجعت).
الStatus: 'resolved' (معناها إن التحميل خلص).
الError: undefined (برضه مفيش مشاكل).
توضيح فكرة تحديث البيانات محليًا
لما نتكلم عن تحديث البيانات محليًا باستخدام الـ WritableResource
، إحنا بنقصد إننا نغير البيانات اللي عندنا من غير ما نعمل طلب جديد للسيرفر. دي طريقة ممتازة لو عايز الـ UI يتحدث بسرعة بناءً على تغييرات المستخدم، بدل ما تستنى رد من السيرفر.
import { resource } from "@angular/core";
@Component({
template: ` <button (click)="updateTodo()">Update</button> `,
})
export class MyComponent {
todoResource = resource({
loader: () => {
return Promise.resolve({ id: 1, title: "Hello World", completed: false });
},
});
updateTodo() {
this.todoResource.value.update((value) => {
if (!value) return undefined;
return { ...value, title: "updated" };
});
}
}
This will print the following:
Value: { id: 1, title: "updated", completed: false }
Status: 'local'
Error: undefined
الlocal: بتوضح إن التعديل ده حصل محليًا.
ده بيساعدك لو عايز تفرق بين البيانات اللي جت من السيرفر وبين اللي تم تعديلها يدويًا.
تحميل البيانات من السيرفر
دلوقتي هنشوف إزاي نستخدم الـ resource
عشان نجيب بيانات من السيرفر باستخدام API حقيقي زي JSONPlaceholder.
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component()
export class MyComponent {
todosResource = resource({
loader: () => {
return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`).then(
(res) => res.json() as Promise<Todo[]>
);
},
});
}
تعريف واجهة (Interface)
عرّفنا الـ Todo عشان نحدد شكل البيانات اللي راجعة (id, title, completed).
استخدمنا loader لكتابة دالة بتحمل البيانات من السيرفر باستخدام fetch. الطلب بيسحب 10 عناصر من قائمة todos.
أول ما يتم إنشاء todosResource، بيبدأ الطلب على طول. أثناء ما الطلب شغال، القيمة (value) بتكون undefined، والحالة (status) بتكون 'loading'.
القيمة (value) هتتحول للبيانات اللي جت من السيرفر. الحالة (status) هتكون 'resolved'.
أثناء التحميل
Value: undefined;
Status: "loading";
Error: undefined;
بعد انتهاء التحميل
Value: [
{ id: 1, title: "Hello World", completed: false },
{ id: 2, title: "Hello World", completed: false },
...
]
Status: 'resolved'
Error: undefined
الحالة (status)
ال'loading': الطلب شغال.
ال'resolved': البيانات وصلت بنجاح.
ال'error': لو حصلت مشكلة أثناء التحميل.
بمجرد ما البيانات توصل، تقدر تعرضها مباشرة في الـ UI. لو حصلت مشكلة، الـ error هتوضح لك السبب.
في بعض الأحيان، ممكن تحتاج تعيد تحميل البيانات (refresh) بناءً على تفاعل المستخدم، زي لما يضغط على زرار.
import { resource } from "@angular/core";
@Component({
template: ` <button (click)="refresh()">Refresh</button> `,
})
export class MyComponent {
todosResource = resource({
loader: () => {
return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`).then(
(res) => res.json() as Promise<Todo[]>
);
},
});
refresh() {
this.todosResource.refresh();
}
}
الميثود ()refresh
اللي موجودة في الـ resource بتشتغل على إعادة تشغيل الـ loader مرة تانية لتحميل البيانات من جديد.
لو استدعيت ()refresh
أكتر من مرة في نفس الوقت
مش هيبدأ طلب جديد إلا بعد انتهاء الطلب الحالي.
ده معناه إن الطلبات مش هتتكرر أو تتداخل، وده نفس السلوك اللي بنشوفه مع exhaustMap في RxJS.
تحميل البيانات بناءً على Signals
لما نحتاج نحمل بيانات بناءً على Signal زي todoId، الميثود loader لوحدها مش بتتتبع تغييرات الـ Signal بشكل تلقائي. يعني لو todoId اتغيرت، الـ load مش هيتم استدعاؤه مرة تانية تلقائيًا.
import { resource } from "@angular/core";
@Component()
export class MyComponent {
todoId = signal(1); // Signal لتحديد ID المطلوب
todoResource = resource({
loader: () => {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${this.todoId()}`
).then((res) => res.json() as Promise<Todo>);
},
});
}
This will work fine, but one this to notice is that loader
is untracked
and that means, that if the todoId signal changes, the load won't be called again. Let's make it more reactive!
رغم إن todoId عبارة عن Signal، الـ loader هنا مش بيتتبع التغييرات اللي بتحصل في todoId. لو غيرت قيمة todoId، البيانات مش هتتحدث تلقائيًا.
عند الحاجة إلى جعل البيانات تتحدث تلقائيًا عند تغيير Signal مثل todoId
، يمكننا استخدام خاصية request
داخل الـ resource
. الـ request
تتيح تمرير Signal أو مجموعة من Signals ليتم تتبعها تلقائيًا.
todoResource = resource({
request: this.todoId,
loader: ({ request: todoId }) => {
return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`).then(
(res) => res.json() as Promise<Todo>
);
},
});
تم تمرير this.todoId كـ Signal.
لما تتغير قيمة todoId، الـ loader يتم استدعاؤه تلقائيًا لتحميل البيانات الجديدة.
الloader يستقبل القيمة الحالية لـ todoId عبر الخاصية request. يقوم بعمل طلب البيانات بناءً على هذه القيمة.
إذا تغيرت قيمة todoId أثناء وجود طلب سابق قيد التنفيذ، يمكن إلغاء الطلب القديم باستخدام خاصية abortSignal
.
todoResource = resource({
request: this.todoId,
loader: ({ request: todoId, abortSignal }) => {
return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
signal: abortSignal,
}).then((res) => res.json() as Promise<Todo>);
},
});
limit = signal(10);
query = signal("");
todosResource = resource({
request: () => ({ limit: this.limit(), query: this.query() }),
loader: ({ request, abortSignal }) => {
const { limit, query } = request as { limit: number; query: string };
return fetch(
`https://jsonplaceholder.typicode.com/todos?_limit=${limit}&query=${query}`,
{ signal: abortSignal }
).then((res) => res.json() as Promise<Todo[]>);
},
});
الـ abortSignal بتستخدم لإلغاء الطلبات اللي لسه شغالة (Unfinished Requests) لما يحصل تغيير يستدعي طلب جديد. وده بيحسن الأداء ويمنع التداخل بين الطلبات.
لو المستخدم غيّر القيم بسرعة:
مثلاً: المستخدم زوّد limit من 10 لـ 20 لـ 30 في وقت قليل. كل قيمة جديدة بتحتاج طلب جديد للبيانات.
بدون abortSignal: الطلبات القديمة هتكمل لحد ما تخلص، وده هيستهلك موارد السيرفر
مع abortSignal: الطلب القديم يتلغي أول ما يبدأ طلب جديد. لو حصل تغيير في أكثر من Signal مع بعض:
زي تغيير limit وquery في نفس الوقت. abortSignal يضمن إن الطلبات القديمة تُلغى بسرعة.
عندك طلب بيانات شغال (جاري تحميل البيانات من السيرفر).
في نفس الوقت، قررت إنك تعدّل البيانات محليًا (من غير ما تستنى الرد من السيرفر).
البيانات المحلية تتحدث فورًا:
أول ما تعدّل البيانات، Angular Resource API بتقوم بتحديث البيانات اللي عندك مباشرة في الواجهة (Local Update).
يعني المستخدم هيشوف التحديث الجديد بدون انتظار.
إلغاء الطلب الجاري:
الطلب اللي كان شغال من السيرفر (request in progress) يتم إلغاؤه تلقائيًا. السبب: البيانات اللي كان هيجيبها الطلب مبقتش لازمة لأنك بالفعل عدّلتها محليًا.
عن طريق فصل القيم التفاعلية (reactive values) عن الـ loader ممكن ننقل loader logic إلى function مستقلة ونستخدمها في أماكن مختلفة بسهولة.
الكود قبل التعديل:
todoResource = resource({
request: this.todoId,
loader: ({ request: todoId, abortSignal }) => {
return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
signal: abortSignal,
}).then((res) => res.json() as Promise<Todo>);
},
});
الكود بعد التعديل:
import { ResourceLoaderParams } from "@angular/core";
function todoLoader({
request: todoId,
abortSignal,
}: ResourceLoaderParams<number>): Promise<Todo> {
return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
signal: abortSignal,
}).then((res) => res.json() as Promise<Todo>);
}
todoResource = resource({ request: this.todoId, loader: todoLoader });
الـ function دي بتنقل loader logic الخارجي بعيدًا عن الـ resource.
بتعتمد على النوع ResourceLoaderParams عشان تحصل على المعلومات اللي محتاجة زي request و abortSignal.
في Angular، الاعتماد على Observables لعمليات تحميل البيانات هو أمر شائع. باستخدام rxResource، يمكننا استخدام Observables بدلاً من Signals وPromises لتحميل البيانات بشكل أكثر تفاعلية ومرونة.
import { rxResource } from "@angular/core/rxjs-interop";
@Component()
export class MyComponent {
limit = signal(10);
todosResource = rxResource({
request: this.limit,
loader: (limit) => {
return this.http.get<Todo[]>(
`https://jsonplaceholder.typicode.com/todos?_limit=${limit}`
);
},
});
}
طيب، إزاي بنستدعي الـ Service؟ بنعمل حاجة اسمها Dependency Injection، وده معناه إننا بنضيف الـ Service للـ Component أو لـ Service تانية عن طريق الـ constructor. Angular بتسهل لنا القصة دي باستخدام حاجة اسمها Providers، اللي بنحطها في الـ Module بتاعنا علشان تقول لـ Angular إن الـ Service دي متاحة للاستخدام في كل الـ Components اللي محتاجاها.
✨ النقطة التانية المهمة هي إن Angular بيبني حاجة اسمها Injector Tree. الفكرة إن لما يكون عندك (Components) كتيرة في التطبيق، Angular بيعمل شجرة Injectors علشان يعرف يوزع الـ Services دي بناءً على احتياجات كل Component. الطريقة دي اسمها Hierarchical Pattern، واللي معناها إن الـ Injectors بتتبنى بشكل شجري، زي ما الـ Components بتتبنى على شكل شجرة.
بالتالي، لو Component معين محتاج Service معينة، Angular هيدور عليها في أقرب Injector ليه في الشجرة. لو ملقهاش، هيكمل يدور لحد ما يطلع لفوق في الشجرة ويلاقيها.
يعني الـ AppComponent مش مرتبط بالـ ProductService اللي انت بتديله. عادي ممكن تديله BetterProductService أو MockProductService من غير ما يعرف الفرق. هو مش محتاج يعرف كل التفاصيل.
دلوقتي الاختبار بقى سهل. بما إن الـ AppComponent مش مرتبط بنوع معين من ProductService، فإنت ممكن تعمل mock للـ ProductService وتستخدمه في الاختبار من غير ما يبقى عندك مشاكل في الكود الأساسي.
دلوقتي بقى عندك component ممكن تعيد استخدامه في أماكن تانية بسهولة، لأن طالما الخدمة اللي بتشتغل مع component بتلتزم بالـ interface اللي محتاجه، خلاص مفيش مشكلة.
ده اللي هو الكلاس اللي محتاج الـ Dependency. يعني هو اللي عايز "الخدمة" أو "الـ Service" علشان يستخدمها في الكود بتاعه. وده ممكن يكون أي حاجة زي الـ Component أو الـ Directive أو حتى الـ Service تانية.
Dependency الـ Dependency هو الحاجة اللي الـ Consumer محتاجها. يعني هي الـ Service اللي إحنا عايزين نحقنها في الـ Consumer
الInjection Token (DI Token) ده حاجة زي "المفتاح" اللي بنستخدمه علشان نقول للـ Angular إحنا عايزين نحقن أي Dependency. كل Dependency لازم يبقى ليها حاجة بتحددها زي ID، علشان الـ Angular يعرف يلاقيها ويحقنها في الـ Consumer. يعني بنستخدم الـ DI Token علشان نسجل الـ Dependency بتاعتنا.
الProvider ده الشخص اللي "بيوفر" الـ Dependencies. يعني عنده لستة بكل الـ Dependencies اللي بنستخدمها في الأبلكيشن بتاعنا، مع الـ Tokens بتاعتها. الـ Provider هو اللي بيستخدم الـ Token علشان يحدد الـ Dependency ويجهزها علشان الـ Consumer يقدر يستخدمها.
الInjector ده هو المسؤول إنه يمسك الـ Providers ويشوف إيه الـ Dependency اللي الـ Consumer طالبها. بعد كده هو اللي بيعمل "إنشاء" للـ Dependency (يعني بيخلق instance منها) وبيحقنها في الـ Consumer. الـ Injector بيستخدم الـ DI Token علشان يدور على الـ Dependency ويحقنها في الكلاس اللي طالبها.
بمعنى تاني، لو انت في الـ Component بتاعك بتقول إنك محتاج "Service" معينة، الـ Injector بيدور في الـ Providers ويشوف الـ Service اللي انت طالبها ويبعتهالك.
2- أما لو وفرت الـ service في مستوى الـ component نفسه، يبقى الـ service دي متاحة بس للـ component ده والـ child components اللي جواه. يعني لو في components برا مش هتقدر تستخدم الـ service دي.
3- لو وفرت الـ service دي في أي module تاني (غير الـ Lazy Loaded Module)، برضو هتبقى متاحة لكل التطبيق. يعني تقدر تستخدمها في أي حتة من المشروع بتاعك.
الLazy Loaded Module هو module بيتم تحميله بس لما يبقى محتاجه، مش بيتحمل مع بداية التطبيق. عشان كده، الـ services اللي بتتوفر في الـ Lazy Loaded Module بتكون محصورة (scoped) جوا الـ module ده بس.
يعني إيه الكلام ده؟ يعني لو أنت وفرت service معينة جوا Lazy Loaded Module، الـ service دي هتكون متاحة فقط للعناصر (components أو services) اللي موجودة جوا نفس الـ module ده. يعني لو في components أو services تانية موجودة في أجزاء تانية من التطبيق (مش موجودة جوه الـ Lazy Loaded Module) مش هتقدر تستخدم الـ service دي.
لو عندك تطبيق كبير فيه Modules كتير، وليكن مثلاً: AdminModule UserModule لو عملت Lazy Loading للـ AdminModule ووفرت فيه service خاصة (مثلاً AdminService)، الخدمة دي هتبقى متاحة فقط في الـ AdminModule. لو حاولت تستخدمها في UserModule أو أي module تاني، مش هتشتغل ومش هتبقى متاحة هناك.
الـ Angular Injector هو اللي بيقوم بإنشاء الـ dependency (يعني أي حاجة محتاجها الكومبوننت أو الخدمة عشان تشتغل) ويحقنها في الكومبوننت أو الخدمة اللي طالبة الحاجة دي.
دلوقتي، عشان الـ Injector يعرف يجيب الـ dependency دي، بيشوف حاجة اسمها Injection Token. الـ Injection Token ده زي اسم أو علامة مميزة للـ dependency اللي انت طالبها. بعد كده، الـ Injector بيروح يبص في قائمة الـ Providers اللي هي المكان اللي بيحتفظ فيه Angular بطريقة إنشاء الـ dependency دي.
الـ Provider ده بيحتوي على معلومات عن إزاي نعمل instance (نسخة جديدة) من الـ dependency دي. فاللي بيحصل إن الـ Injector أول ما يلاقي الـ Provider المناسب، بيعمل نسخة من الـ dependency دي، وبعد كده يحقنها في الكومبوننت أو الخدمة اللي محتاجاها.
✨ الموضوع زي لما تكون بتطلب خدمة معينة (زي سباك أو كهربائي)، والـ Injector هو الشخص اللي بيوصل الخدمة دي ليك، بس هو لازم يروح يدور في دليل الخدمات (اللي هو الـ Providers) عشان يلاقي الشخص المناسب (الـ dependency) اللي هيعمل الخدمة المطلوبة.
لما البرنامج بيبدأ (الـ bootstrap)، Angular بيحمل أول حاجة Root Module اللي هي AppModule. الموديول ده بيبقى زي الأصل اللي كل حاجة بعد كده هتتحمل منه. مع تحميله، Angular بيخلق حاجة اسمها RootModule Injector. ده المسؤول عن البرنامج كله، وبنقول عليه الـ application-wide scope، يعني ينفع يستعمل أي حاجة في أي حتة في التطبيق.
لما Root Module يخلص تحميل، يروح يجيب الـ AppComponent اللي هو أهم Component في التطبيق لأنه الأب لكل الـ Components اللي بعد كده. مع كل Component بيتخلق Injector خاص بيه، فالـ AppComponent بيأخذ حاجة اسمها root Injector ودي اللي بتبقى جذر شجرة الـ ElementInjector tree اللي هي للشغل الخاص بالـ Components.
كل Component تحت AppComponent بيبقى ليه أولاده من الـ Components، وكل واحد فيهم برضه بياخذ Injector خاص بيه. فكل ما التطبيق يكبر ويتفرع، الـ Injectors بتكبر وتتفرع بنفس الطريقة في شجرة موازية للـ Components tree، وده اللي بنسميه شجرة الـ ElementInjector.
- الModuleInjector tree للتعامل مع الموديولز.
- الElementInjector tree اللي بتوازي شجرة الـ Components وبتبني Injectors ليهم.
✨ الموضوع ده بيساعد Angular إنه ينظم استدعاء الـ Dependencies بشكل سليم ويوفرها في الأماكن الصح.
لو سجلت الـ Service في الـ Module:
ده معناه إن الخدمة دي هتكون متاحة في كل مكان في التطبيق. يعني لو أنت ضفت الـ Service بتاعتك في الـ Providers بتاعة الـ NgModule@، هتكون متاحة في كل المكونات (components) وكل الحتت اللي في المشروع.
providers: [ProductService, LoggerService];
في الحالة دي، الخدمة هتكون متاحة بس للمكون ده (Component) وأي مكونات (Components) بتكون جواه أو تابعاله. يعني هيكون ليها نطاق أصغر.
في طريقة تانية لتسجيل الـ Service وهي باستخدام الـ providedIn جوه الـ Injectable@. دي تعتبر طريقة مريحة لأنك بتسجل الـ Service بشكل مباشر في المكان اللي هي متعرفة فيه، وغالباً بنستخدم providedIn: 'root' علشان تكون الخدمة متاحة للتطبيق كله من غير ما نضيفها في الـ Module بشكل صريح.
@Injectable({
providedIn: "root",
})
export class ProductService {}
دي Decorator لازم تضيفها على أي خدمة علشان Angular يعرف إنه يقدر يعمل Inject للخدمة دي في (components) اللي محتاجاها. لما تستخدم Injectable@، Angular بيدير العملية دي ويقدر يجيب الخدمات اللي Component محتاجها ويضيفها أوتوماتيكي.
في طريقتين أساسيين علشان تسجل الخدمة في الـ Providers:
إنك تضيفها مباشرة في الـ Providers array سواء في NgModule@ لو عايزها على مستوى التطبيق كله، أو في Component@ أو Directive@ لو عايزها على مستوى مكون معين.
أو إنك تستخدم خاصية providedIn في الـ Injectable@ decorator. ودي طريقة تانية أسهل شوية لأنك بتقول للـ Angular إن الخدمة دي متاحة على مستوى التطبيق كله (Root) أو على مستوى موديول معين (Module).
لما الـ Angular بيشغل التطبيق، بيعمل حاجة اسمها Injector. الـ Injector ده مسؤول عن إنه يجيبلك الكائنات أو الـ dependencies اللي محتاجها الكود. فيه Root-level Injector بيبقى شغال على مستوى التطبيق كله، وبيبقى فيه Module-level Injector لو عندك Lazy Loaded Modules (ودي أجزاء من التطبيق بتتحمل بس لما تحتاجها).
1- Class Provider : useClass 2- Value Provider: useValue 3- Factory Provider: useFactory 4- Aliased Class Provider: useExisting
يعني إيه؟: لما بنستخدم الـ useClass بنقول للـ Angular يعمل إنستنس جديدة (instance) من كلاس معين، وده زي ما بنستخدم الكلمة المفتاحية new لإنشاء كائن من الكلاس. إزاي نستخدمها؟: لو عندك كلاس اسمه ProductService وعايز توفر إنستنس منه في التطبيق:
providers: [{ provide: ProductService, useClass: ProductService }];
فايدة الطريقة دي إنك تقدر بسهولة تبدل الكلاس الأصلي بكلاس تاني (مثلاً كلاس فيك للاختبارات). ده مفيد جدًا في unit testing لما مش عايز تتعامل مع البيانات أو العمليات الحقيقية، وبدلها بتستخدم بيانات وهمية.
providers: [{ provide: ProductService, useClass: FakeProductService }];
يعني إيه؟: هنا بنستخدم useValue عشان نوفر قيمة ثابتة مش خدمة أو كلاس، يعني مثلاً لو عايز توفر URL أو إعدادات عامة.
providers: [{ provide: "USE_FAKE", useValue: true }];
constructor(@Inject('USE_FAKE') public useFake: boolean) {}
بنستخدم useFactory لما نحتاج نرجع قيمة أو object بعد تنفيذ function. يعني لو عايز ترجع قيمة بناءً على شرط معين.
providers: [{ provide: "GREETING", useFactory: () => "Hello World" }];
constructor(@Inject('GREETING') public greetingFunc: any) {
console.log(greetingFunc());
}
بنستخدم useExisting لما عايزين نوفر نفس الإنستنس بتاعت كلاس تاني، يعني بدل ما نعمل إنستنس جديدة، نستخدم إنستنس موجودة بالفعل.
providers: [{ provide: "ExistingService", useExisting: ProductService }];
✨ ببساطة، لما تيجي تستدعي الـ Service اللي استخدمت فيها useExisting، تقدر تستدعيها باستخدام الـ Token اللي حطيته في الـ provide.
constructor(@Inject('ExistingService') private productService: ProductService) {}
الـ Injector ده هو المسئول إنه يجيب الـ Dependency اللي أي Component أو Service محتاجها. يعني لو عندك Service معينة وعايز تستخدمها في Component، مش هتعمل new بنفسك، لكن هتعتمد على الـ Injector إنه هو اللي يعملها لك.
في الحقيقة، Angular مش بيعمل Injector واحد بس. لأ، هو بيعمل Injector Tree (شجرتين في الحقيقة، واحدة للـ Modules وواحدة للـ Elements زي Components وDirectives).
- الModule Injector Tree: الشجرة دي خاصة بالـ Modules زي Root Module أو أي Lazy Loaded Module.
- الElement Injector Tree: دي خاصة بالـ Components والـ Directives اللي بتتعامل مع الـ DOM.
الModule Injector بيتبني لما التطبيق بيبدأ، وبيحتوي على كل الـ Providers اللي أنت معرفها في الـ Module. فيه طريقتين عشان تسجل الـ Providers على مستوى الـ Module تحطهم في providers بتاع الـ NgModule@ تستخدم providedIn: 'root' في الـ Injectable@ عشان تعمل الـ Service متاحة في كل التطبيق.
لما الـ Component يطلب Service معينة، Angular بيشوف الأول الـ Injector بتاع الـ Component نفسه، لو لقاها بيجبها، لو ملقهاش، بيطلع لفوق للـ Injectors الأعلى (زي بتاع الـ Module أو حتى الـ Root Injector) لحد ما يلاقي الـ Dependency المطلوبة. لو ملقهاش خالص، بيرجع Error.
في أعلى شجرة الـ Injectors، Angular بيعمل حاجة اسمها Null Injector، اللي دايماً بترمي Error لو حاولت تجيب منها حاجة مش موجودة، إلا لو كنت حاطط عليها الـ Optional@ عشان ماترميش Error.
مثال بسيط لو عندك Module معين بيستخدم Service، تقدر تحط الـ Service دي في providers بتاع الـ NgModul@، ساعتها الـ Injector اللي خاص بالـ Module ده بس هو اللي هيجيب الـ Service. ولو عايزها متاحة في كل التطبيق، تحط providedIn: 'root
ببساطة، شجرة Element Injector في Angular بتُستخدم علشان توفر الـ services على مستوى العناصر زي Components و Directives. لما التطبيق بيبدأ، Angular بيبني شجرة Injectors دي بحيث كل عنصر في التطبيق سواء كان Component أو Directive يكون ليه Injector خاص بيه.
أول Injector بيتعمل هو بتاع Root Component، وده اللي بيبقى بمثابة الأب لكل العناصر اللي بعد كده. كل عنصر في التطبيق ممكن يبقى له عناصر تانية بداخله، وده بيخلق شجرة من العناصر وكل عنصر ليه Injector خاص بيه.
ببساطة، كل Injector في Angular بياخد الـ Providers من الـ Component@ أو Directive@ اللي بيكون مربوط بيه. بمعنى، لو عندك Component معين فيه قائمة من services أو dependencies محتاجة تكون متاحة، Angular بيستخدم الـ providers الموجودة في الـ Component@ علشان يوفرها للـ Injector.
لكن لو الـ Component@ أو Directive@ مش بيحتوي على أي Providers (يعني القائمة فاضية)، Angular بيعمل Injector، لكنه بيبقى فاضي، ومش بيحتوي على أي خدمات أو services متاحة للعنصر ده.
لما ينتهي عمر العنصر أو الـ component (يعني لما يتم إزالته من الـ DOM أو يخلص دورة حياته)، Angular بيتخلص من الـ Injector اللي مرتبط بيه علشان يحرر الذاكرة ويحسن الأداء.
لو عندك Component بيطلب Service معينة في الـ constructor، Angular بيبدأ يدور على الـ dependency دي في الـ Injector اللي خاص بالـ component نفسه. لو لقاها، بيحقنها جوه الـ Component. لو مش لقاها، بينادي على الأب بتاع الـ Component ويدور في الـ Injector بتاعه.
لو في النهاية مش لقى الـ dependency في شجرة الـ Element Injector كلها، يبدأ يدور في شجرة الـ Module Injector، والبحث يبدأ من الـ Root Module Injector (أو Lazy Loaded Module Injector لو بتستخدم lazy loading).
لو عندك (Service) وعايزها تكون متاحة لكل التطبيق كله (بما في ذلك كل Components والكود اللي بتم تحميله سواء Lazy أو Eager)، تقدر تستخدم providedIn: 'root'. ده معناه إن Service دي هتكون singleton يعني موجودة بنسخة واحدة في كل التطبيق. الفكرة هنا إن لو Service دي متمتش استخدامها في أي مكان في التطبيق، Angular هيشيلها من الملف النهائي عشان يحافظ على حجم التطبيق خفيف.
لو عايز Service تكون متاحة بس في module معين ومش على مستوى التطبيق بالكامل، ممكن تضيفها في الـ providers بتاع الموديول ده فقط. في الحالة دي، Service هتكون singleton بس جوه الموديول ده، يعني مش هتتشارك مع باقي الموديولات أو التطبيق.
لو قمت بتسجيل Service باستخدام الطريقتين (يعني حطيت providedIn: 'root' وفي نفس الوقت أضفتها في الـ providers لموديول معين):
هتكون singleton Service على مستوى التطبيق كله، ما عدا في الموديول اللي سجلتها فيه في الـ providers، لأنه هيكون عنده نسخة جديدة منفصلة عن باقي التطبيق. ده بيدي كل موديول القدرة إنه يستخدم نسخة خاصة بيه من Service من غير ما يشاركها مع موديولات تانية.
ده بقى مختلف شوية. لو عندك أكتر من lazy-loaded module وكل موديول محتاج نسخة خاصة بيه من الخدمة، تستخدم providedIn: 'any'. يعني كل lazy-loaded module هيكون عنده نسخة جديدة من الخدمة دي. لكن eager-loaded modules (اللي بتتحمل في أول التطبيق) هيفضلوا يستخدموا النسخة اللي في Root Module Injector زي العادي.
دي بقى للناس اللي بتعمل حاجات أكتر تعقيدًا. لما عندك أكتر من تطبيق Angular شغالين على نفس الصفحة، ومحتاج خدمة تكون مشتركة بينهم كلهم، تستخدم providedIn: 'platform'. الـ Platform Injector هو الأب بتاع Root Module Injector، يعني أي خدمة بتتحط هنا تبقى متاحة لكل التطبيقات اللي شغالة في الصفحة. مفيد جدًا لما تكون بتستخدم حاجة زي Angular Elements عشان تشارك نفس الخدمة بين العناصر المختلفة اللي شغالة على نفس الصفحة.
الديكوراتور ده بيقول لـ Angular يدور على الـ dependency اللي انت محتاجها فقط في المكان اللي انت حقنت فيه الـ dependency. بمعنى إن لو فيه component أو service معين وحقنت فيه dependency وكنت مستخدم Self@، Angular مش هيبص في الأماكن الأعلى (الـ parent injectors) عشان يجيب الـ dependency دي، لو مش لقاها في المكان المحدد، هيطلع error.
مثال: لو انت بتقول لـ Angular: "ادور على الـ Service دي في نفس الـ component ده، ومش عايزك تبص في أي مكان تاني."
constructor(@Self() private myService: MyService) { }
الديكوراتور ده بيعمل العكس. هو بيقول لـ Angular "أنا عايز الـ dependency دي، بس متدورش عليها في نفس الـ injector اللي فيه الـ component ده، دور عليها في الـ parent injectors بس." يعني بيتخطى (skip) المكان الحالي ويروح يدور في الأماكن الأعلى (الـ parent injectors).
مثال: لو عايز تعتمد على service موجودة في مكان أعلى من الـ component اللي انت فيه، تقدر تستخدم SkipSelf@
constructor(@SkipSelf() private myService: MyService) { }
الديكوراتور ده بيقول لـ Angular "لو لقيت الـ dependency اللي بطلبها، حقنها عادي، ولو ملقتهاش، متطلعش error." يعني ببساطة بـ Optional@، Angular بيعمل الموضوع ده اختياري، وبيسيب الـ dependency بـ null لو ملقهاش.
مثال: لو انت مش متأكد لو الـ MyService موجودة ولا لأ، بس مش عايز تطلع error لو مش موجودة، تقدر تستخدم Optional@
constructor(@Optional() private myService: MyService) { }
constructor(
@Self() private serviceA: ServiceA, // لازم يلاقيها في نفس المكان
@SkipSelf() private serviceB: ServiceB, // متدورش عليها هنا، بس دور في اللي فوق
@Optional() private serviceC: ServiceC // لو ملقتهاش متطلعش error
) { }
في الـ JavaScript العادي، أنت اللي بتطلب البيانات (ده زي الـ pull). لكن في الـ RxJS Observable، البيانات بتتبعت لك تلقائيًا أول ما تعمل subscribe للـ Observable (ده نظام الـ push).
في Angular، الـ Observable بتبدأ تشتغل لما تعملها subscribe، وبتقدر تعمل كده إما باستخدام subscribe مباشرةً أو باستخدام async pipe.
دي اللي بتشتغل مع Observable.pipe() وبتاخد observable كـ input وتطلع observable جديد. أمثلة عليها: map، switchMap، mergeMap، concatMap، takeUntil، retry، catchError، و throwError.
دي اللي بنستخدمها كـ functions مستقلة عشان ننشئ observables. أمثلة عليها: of، from، interval، fromEvent، generate، و range.
of(101, 102)
.pipe(
delay(1000),
map((num) => num * 2)
)
.subscribe((v) => console.log(v)); // 202, 204
في المثال ده، أول حاجة بنستخدم delay(1000) علشان نأخر التنفيذ ثانية (1000 ميلي ثانية). بعد كده، بنستخدم map اللي بيضرب كل رقم في 2. النتيجة هتكون: 202، 204 (يعني الأرقام اتضربت في 2 بعد التأخير).
الخلاصة: الـ pipe بيسهل عليك تنفيذ عمليات متتابعة على البيانات اللي جاية من observable، وده بيديك تحكم أكبر في تعديل الداتا أو توقيتها أو التعامل مع الأخطاء.
بتاخد مجموعة من القيم (زي "a" و "b" و "c") وتحولهم لـ observable sequence، يعني كل قيمة بتطلع لوحدها.
of("a", "b", "c").subscribe((e) => console.log(e));
بتاخد array أو حاجة شبهه (زي promise) وتحوّله لـ observable، يعني كل عنصر جوه الـ array بيتعامل معاه كجزء من الـ observable.
from(["a", "b", "c"]).subscribe((e) => console.log(e));
of(1, 2, 3, 4)
.pipe(map((e) => e * e))
.subscribe((output) => console.log(output));
عندنا observable بيرسل القيم (1, 2, 3, 4). كل قيمة من القيم دي بتدخل في map، اللي بتضرب القيمة في نفسها (يعني بتربع القيمة). بعد كده الـ observable الجديد اللي طالع بيرجع القيم: 1, 4, 9, 16. يعني لما بنعمل subscribe على الـ observable الناتج، بنشوف القيم الجديدة اللي هي 1، 4، 9، و 16 في الـ console.
بكده تقدر تستخدم map علشان تعدل أو تعمل عمليات على البيانات اللي بتيجي من الـ observable الأصلي بطريقة بسيطة.
بس علشان كده، الـ switchMap() مش مناسب في السيناريوهات اللي محتاج فيها إن كل الاشتراكات تخلص للآخر، زي لما بتستخدمه مع طلبات HTTP اللي بتكتب في قاعدة البيانات زي (POST، PUT، PATCH، DELETE). في الحالات دي الأفضل إنك تستخدم mergeMap()، لأنه بيسمح لكل الاشتراكات تكمل بدون ما يوقف أي واحدة منهم.
@Component({
selector: "my-car-brand-detail",
standalone: true,
imports: [CommonModule],
providers: [CarBrandService],
template: `
Details:
<ng-container *ngIf="carBrand$ | async as brand">
<p>Brand ID:</p>
<p>Brand Name:</p>
</ng-container>
`,
})
export class CarBrandDetailComponent {
private readonly carBrandService = inject(CarBrandService);
private readonly activatedRoute = inject(ActivatedRoute);
// Listen for changes on the Router Params and map the params to an ID value
private readonly id$ = this.activatedRoute.params.pipe(
map((params) => params["id"])
);
// Every time the id$ changes, the carBand will be fetched
public readonly carBrand$: Observable<CarBrand> = this.id$.pipe(
switchMap(
// SwitchMap returns an observable, in this case an Observable<CarBrand>
(id) => this.carBrandService.getById(id)
)
);
}
@Component({
selector: "my-app",
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule],
providers: [CarService],
template: `
<input [formControl]="searchTermControl" type="text" />
<ul>
<li *ngFor="let brand of filteredCarBrands$ | async"></li>
</ul>
`,
})
export class AppComponent {
private readonly carBrandService = inject(CarService);
public readonly searchTermControl = new FormControl("");
public readonly filteredCarBrands$: Observable<string[]> =
// Every time the value changes of the fornControl this stream will be executed again
this.searchTermControl.valueChanges.pipe(
debounceTime(300), // Wait 300ms before searching
switchMap((searchTerm) =>
// Returns a list of Car Brands
this.carBrandService.searchCarBrands(searchTerm)
)
);
}
@Component({
selector: "my-app",
standalone: true,
imports: [CommonModule, FormsModule],
providers: [CarService],
template: `
<input
[ngModel]="searchTerm$$.value"
(ngModelChange)="searchTerm$$.next($event)"
type="text"
/>
<ul>
<li *ngFor="let brand of filteredCarBrands$ | async"></li>
</ul>
`,
})
export class AppComponent {
private readonly carBrandService = inject(CarService);
public readonly searchTerm$$ = new BehaviorSubject("");
private readonly searchTerm$ = this.searchTerm$$.asObservable();
// Every time the searchTerm is changed then the carBrandService.searchCarBrands function is called
public readonly filteredCarBrands$: Observable<string[]> =
this.searchTerm$.pipe(
debounceTime(300), // Wait 300ms before searching
switchMap((searchTerm) =>
// Returns a list of Car Brands
this.carBrandService.searchCarBrands(searchTerm)
)
);
}
فايدته الأساسية إنه بيشتغل بشكل متوازي، يعني لو عندك مثلاً HTTP requests وكل واحدة محتاجة تطلع observable تانية عشان تعمل حاجة زي جلب بيانات، الـ mergeMap هيشغل كل الطلبات دي مع بعض من غير ما يستنى واحدة تخلص عشان يبدأ التانية.
مثال على تنفيذ طلبات HTTP متعددة بشكل متوازي باستخدام mergeMap
import { fromEvent } from "rxjs";
import { mergeMap } from "rxjs/operators";
import { ajax } from "rxjs/ajax";
const button = document.getElementById("myButton");
fromEvent(button, "click")
.pipe(mergeMap(() => ajax.getJSON("https://api.example.com/buttonData")))
.subscribe((response) => {
console.log("Button click data:", response);
});
استخدم الmergeMap لو عند requests زي الPOST , DELETE الي لازم تكتمل بدون الغاء
الـ concatMap بياخد كل قيمة جاية من الـ observable الأساسي وبيطلع منها observable جديد، بس هنا الـ observables اللي بيطلعها بتشتغل واحدة واحدة بالتتابع. يعني مش هيبدأ الـ observable التاني غير لما الأولاني يخلص. بمعنى آخر، هو بيعمل flattening للـ observables بس بشكل متسلسل بدل ما يكون كله شغال بالتوازي زي ما بيحصل في mergeMap.
of("Mohit", "Nilesh")
.pipe(
concatMap((se) =>
of("Shree").pipe(
delay(2000),
map((e) => e + " " + se)
)
)
)
.subscribe((res) => console.log(res));
// the output.
(after 2 seconds)
Shree Mohit
(after 2 seconds)
Shree Nilesh
خلينا نقول إن عندك زرار في تطبيق بيفتح حاجة معينة، لو المستخدم ضغط بسرعة على الزرار أكتر من مرة، exhaustMap هيهتم بالضغطة الأولى بس، وهيتجاهل الباقي لحد ما الحاجة اللي بدأها أول مرة تخلص، وبعد كده يبدأ يستقبل حاجات جديدة.
ليه ده مفيد؟ لأنه بيساعد إنك تمنع طلبات كتير تشتغل في نفس الوقت، زي لما تضغط على زر تسجيل الدخول أكتر من مرة، ومش عايز تبعت بياناتك للسيرفر كل مرة غير لما الطلب الأولاني يخلص.
import { fromEvent } from "rxjs";
import { exhaustMap, interval, take } from "rxjs/operators";
const button = document.getElementById("myButton");
const clicks$ = fromEvent(button, "click");
clicks$
.pipe(
exhaustMap(() => {
return interval(1000).pipe(take(3));
})
)
.subscribe((val) => console.log(val));
هنا:
fromEvent بيتابع الكليكات اللي بتحصل على الزرار. exhaustMap بيتأكد إن مفيش كليك جديد هيتم معالجته غير لما الطلب الحالي يخلص. interval هنا مجرد مثال لعملية بتاخد شوية وقت (ممكن تعتبره طلب HTTP مثلاً). كل ضغطة هتستنى لحد ما الكليك الأولاني يخلص قبل ما تقبل كليك جديد.
import { of, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
const myObservable = of("Data 1", "Data 2", "Data 3").pipe(
throwError("Error occurred!"),
catchError((error) => {
return of("Default value");
})
);
myObservable.subscribe(
(data) => console.log("Data:", data),
(error) => console.log("Error:", error),
() => console.log("Complete")
);
throwError: لما يحصل خطأ، الـobservable هيعمل emit للخطأ ده. catchError: بنستخدمه عشان نعمل handle للخطأ. في المثال ده، لو حصل خطأ، بنرجع observable تاني بقيمة بديلة (of('Default value')). الـsubscribe: لو مفيش أخطاء، الـobserver هياخد الـdata اللي الـobservable بيعملها emit. ولو حصل خطأ هيتم الإمساك بيه في الـcatchError قبل ما يوصل للـsubscribe.
الـretry بياخد عدد من المرات اللي عايز تكرر فيها المحاولة لما يحصل خطأ. لو عدد المحاولات خلص ولسه فيه خطأ، الـobservable هيرجع خطأ عادي.
import { of, throwError } from "rxjs";
import { retry, catchError } from "rxjs/operators";
const myObservable = throwError("Error occurred!").pipe(
retry(2),
catchError((error) => {
console.log("Failed after retries:", error);
return of("Fallback value");
})
);
myObservable.subscribe(
(data) => console.log("Data:", data),
(error) => console.log("Error:", error),
() => console.log("Complete")
);
throwError: بنحاكي سيناريو إن الـobservable بيفشل وبيطلع خطأ. retry(2): هنا بنقول للـobservable إنه يحاول مرتين لو حصل خطأ. يعني بعد أول خطأ، هيحاول مرة كمان، ولو فشل برضه، يحاول للمرة الأخيرة. catchError: لو المحاولات كلها فشلت، بنمسك الخطأ ونتعامل معاه. في المثال، بنرجع قيمة افتراضية (Fallback value). الـsubscribe: لو مفيش أخطاء، الـobservable هيعمل emit للـdata عادي، لكن لو كل المحاولات فشلت، هيوصل الخطأ للـcatchError وهنرجع القيمة البديلة.
سيناريوهات للاستخدام: طلبات HTTP متكررة: ممكن تكون بتعمل طلب لـAPI وفيه مشاكل مؤقتة في الاتصال، زي انقطاع الشبكة. في الحالة دي، بدل ما الطلب يفشل مباشرة، ممكن تحاول كذا مرة باستخدام retry، وتدي فرصة إن المشكلة تتحل.
عمليات غير مستقرة: في عمليات زي الكتابة أو القراءة من قاعدة بيانات أو التعامل مع ملفات، ممكن يحصل فشل بسبب عوامل خارجية (زي مشكلة في التخزين المؤقت). باستخدام retry، تقدر تعيد المحاولة بدون الحاجة لتدخل المستخدم كل مرة.
import { ajax } from "rxjs/ajax";
import { catchError, retry } from "rxjs/operators";
import { of } from "rxjs";
// مثال لطلب HTTP
const apiCall$ = ajax.getJSON("https://api.example.com/data").pipe(
// هنحاول إعادة المحاولة 3 مرات لو فشل
retry(3),
catchError((error) => {
console.log("Request failed after retries:", error);
return of({ error: "Failed to fetch data", data: [] });
})
);
apiCall$.subscribe(
(response) => console.log("API Response:", response),
(error) => console.log("Error:", error),
() => console.log("Complete")
);
retry(3): بنطلب من RxJS إعادة المحاولة 3 مرات لو حصل فشل في طلب الـAPI. catchError: لو كل المحاولات فشلت، بنعمل catch للخطأ ونتعامل معاه، مثلاً نرجع بيانات افتراضية بدل ما نوقف التطبيق. الـretry بيكون مفيد في الحالات اللي بتحصل فيها أخطاء غير متوقعة أو مؤقتة، وبيسمح للتطبيق يكون أكثر مرونة في التعامل مع الأخطاء بدون الحاجة لتدخل المستخدم في كل مرة
الـfilter بياخد كل قيمة جاية من الـobservable ويشوف لو القيمة دي بتوافق الشرط اللي في الـcallback. لو القيمة مطابقة للشرط، بتكمل في الـobservable stream، ولو لأ، بتتفلتر وتتجاهل.
import { from } from "rxjs";
import { filter } from "rxjs/operators";
// observable بيمثل مجموعة من الأرقام
const numbers$ = from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// بنستخدم filter عشان نخلي بس الأرقام الزوجية تعدي
const evenNumbers$ = numbers$.pipe(
filter((num) => num % 2 === 0) // هنا الشرط إن الرقم لازم يكون زوجي
);
evenNumbers$.subscribe(
(value) => console.log("Even number:", value),
(error) => console.log("Error:", error),
() => console.log("Complete")
);
فلترة الأحداث: مثلاً، في حالة استماعك لأحداث معينة (زي ضغطات المستخدم على زر معين)، ممكن تستخدم filter عشان تتعامل بس مع نوع معين من الأحداث (زي لو المستخدم ضغط على زرار معين في الـUI).
طلبات الـHTTP: لو عندك استجابة API وبيجيلك فيها بيانات كتيرة، ممكن تستخدم filter عشان تمرر فقط القيم اللي تحقق شروط معينة (مثلاً، طلبات فيها status معين أو بيانات بنطاق معين).
import { fromEvent } from "rxjs";
import { filter } from "rxjs/operators";
const button = document.getElementById("myButton");
const clicks$ = fromEvent(button, "click");
// بنستخدم filter عشان نتعامل بس مع الكليكات اللي بتحصل في الجانب الأيسر من الشاشة
clicks$
.pipe(filter((event: MouseEvent) => event.clientX < window.innerWidth / 2))
.subscribe((event) => console.log("Clicked on the left side!", event));
fromEvent: بنعمل observable للضغطات على زرار معين. filter: هنا بنفلتر الأحداث ونتعامل بس مع الضغطات اللي حصلت في النصف الأيسر من الشاشة (على أساس إن قيمة clientX أقل من نصف عرض الشاشة).
الـtap بياخد function بتتنفذ لكل قيمة بتعدي في الـobservable. القيم بتعدي بدون أي تعديل، فالـtap مش بيغير الـstream، لكنه بيسمح لك تعمل شيء إضافي أثناء مرور البيانات.
import { of } from "rxjs";
import { tap, map } from "rxjs/operators";
const numbers$ = of(1, 2, 3, 4, 5).pipe(
tap((value) => console.log("Before map:", value)),
map((value) => value * 10),
tap((value) => console.log("After map:", value))
);
numbers$.subscribe(
(value) => console.log("Final value:", value),
(error) => console.log("Error:", error),
() => console.log("Complete")
);
Logging: لو عايز تتابع القيم اللي بتعدي في الـobservable لأي سبب، زي الـdebugging. Side effects: لو محتاج تعمل أي عملية أثناء مرور البيانات من غير ما تعدل على القيم. مثال على ده: تحديث UI، تسجيل أحداث، أو تعديل بيانات في مكان تاني بناءً على القيم اللي بتعدي.
عندك observable رئيسي بيرسل قيم (مثلاً، تدفق للأرقام، أو استجابة لأحداث). وعندك observable تاني بنسميه notifier، وده مجرد observable بيتراقب لحد ما بيرسل أي قيمة. بمجرد ما الـnotifier بيرسل أول قيمة، الـobservable الرئيسي بيتوقف عن ارسال أي قيم جديدة.
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
@Component({
selector: "app-my-component",
templateUrl: "./my-component.component.html",
styleUrls: ["./my-component.component.css"],
})
export class MyComponent implements OnInit, OnDestroy {
// الـnotifier اللي هيشغل takeUntil
private componentDestroyed: Subject<void> = new Subject<void>();
constructor() {}
ngOnInit() {
// Example: لو عندك Observable مثلاً بيسمع للأحداث أو بيطلب بيانات
this.someObservable()
.pipe(
takeUntil(this.componentDestroyed) // هيوقف الـObservable لما الـcomponent يتدمر
)
.subscribe((data) => {
console.log("Data received:", data);
});
}
someObservable() {
// ده الـObservable اللي هيشترك فيه الـcomponent (مجرد مثال)
return of("Some data");
}
ngOnDestroy() {
// هنا بنعمل emit للقيمة عشان نوقف الـObservables
this.componentDestroyed.next();
this.componentDestroyed.complete(); // عشان نضمن إن الـSubject مش هيستخدم تاني
}
}
إدارة الاشتراكات: الـtakeUntil بيضمن إن أي اشتراكات للـobservables بتتوقف تلقائيًا لما الـcomponent يتدمر، وده بيساعد على عدم تسرب الذاكرة (memory leaks).
مرونة: الطريقة دي سهلة وبتشتغل مع أي observable في الـcomponent سواء كان HTTP request، أو WebSocket، أو أي نوع تاني من الـobservables.
باستخدام takeUntil مع الـSubject، بتقدر تضمن إن أي اشتراكات بتتوقف بطريقة منظمة وفعالة لما الـcomponent يخرج من الـDOM.
تأخير القيم: أول ما تيجي قيمة من الـobservable، الـdebounceTime بيبدأ يعد الزمن المحدد (مثلاً 300 مللي ثانية). إعادة ضبط المؤقت: لو حصلت قيمة جديدة قبل ما الزمن المحدد ينتهي، المؤقت بيبدأ يعد من الأول تاني. القيمة الأخيرة فقط: لما الزمن المحدد ينتهي ومافيش قيم جديدة جات، القيمة الأخيرة هي اللي بتعدي.
const result = fromEvent(document, "click").pipe(
// Click event is delayed for 1000ms or 1s
// It emits the most recent click event, even after burst clicking
debounceTime(1000)
);
result.subscribe((value) => console.log(value));
لما المستخدم يكتب في خانة البحث، بنقدر نستخدم debounceTime عشان نمنع إرسال طلب للسيرفر مع كل ضغطة على الكيبورد. بدال ما نبعت طلب بعد كل حرف، بنستنى فترة قصيرة قبل ما نبعته فعليًا.
@Component({
...
template: `
<input [formControl]="searchBar" type="text">
`,
})
export class AppComponent {
public readonly searchBar = new FormControl();
public readonly searchResults$ = this.searchBar.valueChanges.pipe(
debounceTime(1000), // Waits for 1000ms or 1s before emitting
distinctUntilChanged() // doesn't emit if current value = previous value
);
constructor() {
this.searchResults$.subscribe((query) => {
console.log(query);
});
}
}
@Component({
...
template: `
<form [formGroup]="form" (ngSubmit)="save()">
<input type="text" formControlName="firstName">
<input type="text" formControlName="lastName">
<button type="submit">Save</button>
</form>
`,
})
export class UserFormComponent implements OnInit {
...
public readonly form = this.fb.group(...);
ngOnInit() {
this.form.valueChanges
// Wait for 500ms after each keystroke before saving the user
.pipe(debounceTime(500))
.subscribe((user: User) => {
this.userService.saveUser(user);
});
}
...
}
الـdebounceTime هو واحد من أفضل الـoperators في RxJS لما بتتعامل مع سلاسل متتابعة من القيم بسرعة، وبيساعد في تحسين الأداء وتجربة المستخدم عن طريق تقليل الطلبات أو الاستجابات اللي ممكن تكون غير ضرورية.
الـcombineLatest بياخد array من الـobservables كمصدر، وبيطلع observable جديد بيحسب بناءً على أحدث القيم من كل observable مصدر. الموضوع باين إنه بسيط، لكن فيه شوية حاجات لازم نكون عارفينها:
كل الـobservables اللي في المصدر لازم يعملوا emit مرة على الأقل قبل ما الـcombineLatest يبدأ يطلع بيانات. كل مرة observable من المصادر يعمل emit لقيمة جديدة، الـcombineLatest بيحسب القيمة من الأول. وده ممكن يخلي الـobservable يطلع بيانات كتير بسرعة، ودي حاجة لازم نخلي بالنا منها. في أغلب الأوقات، هتحتاج تستخدم الـcombineLatest مع Observables طويلة المدى (long-lived observables).
ممكن نعمل فلتر متعدد باستخدام combineLatest في Angular لما يكون عندنا أكتر من مصدر للفلترة، زي فلترة على اسم، تاريخ، وكاتيجوري مثلاً. باستخدام combineLatest، بنقدر ندمج الفلاتر دي كلها عشان نعمل الفلترة المطلوبة.
خلينا نشوف مثال عملي على عمل فلتر متعدد باستخدام combineLatest.
خطوات: هنفترض إن عندنا Array من العناصر اللي محتاجين نفلترها. هنستخدم أكتر من FormControl عشان نتحكم في الفلاتر. هنستخدم combineLatest عشان ندمج القيم من كل فلتر ونعرض النتيجة النهائية.
import { Component, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { combineLatest, of } from "rxjs";
import { map, startWith } from "rxjs/operators";
@Component({
selector: "app-multi-filter",
template: `
<input [formControl]="nameFilter" placeholder="Filter by name" />
<input [formControl]="categoryFilter" placeholder="Filter by category" />
<input
type="date"
[formControl]="dateFilter"
placeholder="Filter by date"
/>
<ul>
<li *ngFor="let item of filteredItems$ | async">
{{ item.name }} - {{ item.category }} - {{ item.date }}
</li>
</ul>
`,
})
export class MultiFilterComponent implements OnInit {
// الفلاتر
nameFilter = new FormControl("");
categoryFilter = new FormControl("");
dateFilter = new FormControl("");
// قائمة العناصر اللي هنفلترها
items = [
{ name: "Item 1", category: "A", date: "2023-01-01" },
{ name: "Item 2", category: "B", date: "2023-01-02" },
{ name: "Item 3", category: "A", date: "2023-01-03" },
{ name: "Item 4", category: "C", date: "2023-01-04" },
];
// الـObservable اللي هيحتوي على العناصر المفلترة
filteredItems$ = combineLatest([
this.nameFilter.valueChanges.pipe(startWith("")),
this.categoryFilter.valueChanges.pipe(startWith("")),
this.dateFilter.valueChanges.pipe(startWith("")),
]).pipe(
map(([name, category, date]) => {
return this.items.filter(
(item) =>
item.name.toLowerCase().includes(name.toLowerCase()) &&
item.category.toLowerCase().includes(category.toLowerCase()) &&
item.date.includes(date)
);
})
);
ngOnInit() {}
}
بنمرر له العنصر اللي عايزين نسمع منه الأحداث (زي زرار أو صفحة كاملة). بنحدد نوع الحدث اللي عايزين نسمعه (زي 'click'، 'keyup'، 'mousemove'، إلخ). بعد كده بنستخدم الـobservable الناتج عشان نعمل اشتراك (subscription) ونتعامل مع الحدث.
import { fromEvent } from "rxjs";
import { filter } from "rxjs/operators";
const button = document.getElementById("myButton");
const clicks$ = fromEvent(button, "click").pipe(
filter((event: MouseEvent) => event.clientX < window.innerWidth / 2) // نتعامل مع الكليكات في النصف الأيسر من الشاشة فقط
);
clicks$.subscribe((event) => {
console.log("Clicked on the left side!", event);
});
What is the difference between Subject, BehaviorSubject, ReplaySubject, and AsyncSubject in RxJS? How do they differ in terms of behavior and use cases?
Subject هو observable عادي، لكن مع ميزات إضافية. يمكنه أن يستقبل بيانات ويصدرها، ويستطيع أكثر من subscriber الاشتراك فيه. لا يحتفظ بأي بيانات سابقة: لو اشتركت في الـSubject بعد ما أرسل بيانات بالفعل، لن تتلقى البيانات السابقة، وستبدأ في تلقي القيم الجديدة فقط.
أي مشترك جديد يبدأ في استقبال البيانات من اللحظة اللي اشترك فيها فقط، ولا يتلقى أي قيم سابقة.
مناسب للبث المباشر (real-time broadcasting) حيث لا نحتاج للاحتفاظ بالبيانات السابقة.
import { Subject } from "rxjs";
const subject = new Subject();
subject.subscribe((value) => console.log("Subscriber 1:", value));
subject.next(1); // Subscriber 1: 1
subject.next(2); // Subscriber 1: 2
subject.subscribe((value) => console.log("Subscriber 2:", value));
subject.next(3); // Subscriber 1: 3, Subscriber 2: 3
💡Hint: المشترك الأول يتلقى جميع القيم، لكن المشترك الثاني يتلقى فقط القيم الجديدة التي تم إرسالها بعد الاشتراك.
BehaviorSubject هو نوع خاص من الـSubject يحتفظ دائمًا بآخر قيمة تم إصدارها. عندما يشترك أي subscriber جديد، يستلم فورًا آخر قيمة تم بثها حتى لو تم الاشتراك بعد إرسال هذه القيمة.
كل مشترك جديد يستلم آخر قيمة تم إصدارها، ثم يتلقى التحديثات الجديدة.
مفيد عندما تحتاج أن يحصل المشترك الجديد على آخر حالة أو آخر قيمة تم إصدارها بالفعل (مثل تحديث حالة المستخدم، أو تحميل بيانات التطبيق).
import { BehaviorSubject } from "rxjs";
const behaviorSubject = new BehaviorSubject(0); // القيمة الافتراضية هي 0
behaviorSubject.subscribe((value) => console.log("Subscriber 1:", value));
behaviorSubject.next(1); // Subscriber 1: 1
behaviorSubject.next(2); // Subscriber 1: 2
behaviorSubject.subscribe((value) => console.log("Subscriber 2:", value)); // Subscriber 2: 2
behaviorSubject.next(3); // Subscriber 1: 3, Subscriber 2: 3
💡Hint المشترك الثاني استلم القيمة الأخيرة (2) فور اشتراكه، ثم استلم القيمة الجديدة التي أرسلت بعد ذلك (3).
ReplaySubject هو نوع آخر من Subjects يمكنه أن يحتفظ بعدد معين من القيم السابقة (أو حتى كل القيم) ويعيد بثها لأي مشترك جديد عند الاشتراك. يمكن تحديد عدد القيم التي يتم إعادة بثها للمشتركين الجدد، مثل آخر قيمة أو آخر قيمتين أو أكثر.
أي مشترك جديد يتلقى عددًا معينًا من القيم السابقة (يمكن تحديد العدد)، ثم يبدأ في تلقي التحديثات الجديدة.
مفيد في التطبيقات التي تحتاج إلى إعادة تشغيل التاريخ (history replay) لمشتركين جدد، مثل تسجيل النشاطات أو الأحداث السابقة.
import { ReplaySubject } from "rxjs";
const replaySubject = new ReplaySubject(2); // يحتفظ بآخر قيمتين
replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);
replaySubject.subscribe((value) => console.log("Subscriber 1:", value)); // Subscriber 1: 2, 3
replaySubject.next(4); // Subscriber 1: 4
replaySubject.subscribe((value) => console.log("Subscriber 2:", value)); // Subscriber 2: 3, 4
3;
💡 Hint: المشترك الأول استلم القيمتين الأخيرتين (2 و 3)، بينما المشترك الثاني استلم (3 و 4) لأنه اشترك بعد إرسال القيم.
AsyncSubject هو نوع من الـSubject لا يصدر أي قيم للمشتركين إلا بعد اكتمال الـobservable، وعندها يقوم بإرسال آخر قيمة فقط تم بثها قبل الاكتمال لجميع المشتركين. لا يصدر أي شيء حتى يتم استدعاء complete
المشتركين يستلمون آخر قيمة فقط عندما يتم استدعاء complete
يستخدم عندما تحتاج إلى إصدار آخر قيمة فقط عند انتهاء العمل، مثل استرجاع بيانات بعد إتمام عملية معقدة أو عند اكتمال عملية حسابية.
import { AsyncSubject } from "rxjs";
const asyncSubject = new AsyncSubject();
asyncSubject.subscribe((value) => console.log("Subscriber 1:", value));
asyncSubject.next(1);
asyncSubject.next(2);
asyncSubject.next(3);
asyncSubject.subscribe((value) => console.log("Subscriber 2:", value));
asyncSubject.next(4);
asyncSubject.complete(); // عند الاكتمال، يتم إرسال آخر قيمة
// Subscriber 1: 4
// Subscriber 2: 4
💡 Hint : هنا، المشتركين الاثنين استلموا فقط القيمة الأخيرة (4) بعد اكتمال الـobservable.
Subject: لما تكون مش محتاج تحتفظ بأي بيانات سابقة، وعايز تبث القيم الجديدة فقط للمشتركين الحاليين. BehaviorSubject: لما تحتاج أن يحصل المشترك الجديد على آخر قيمة تم إصدارها، حتى لو اشترك بعد إرسالها (مثل حالة المستخدم أو حالة التطبيق). ReplaySubject: لما تحتاج أن يحصل المشترك الجديد على عدد من القيم السابقة أو كل القيم التي تم إصدارها (مثل تسجيل الأحداث السابقة). AsyncSubject: لما تحتاج أن يحصل المشترك على آخر قيمة فقط بعد اكتمال العمل (مثل نتائج عملية معقدة أو حسابية).
What are the best practices for managing Observable subscriptions in Angular and ensuring there are no memory leaks?
في Angular، لما بتشتغل مع Observables زي HTTP requests أو حتى events زي كليك أو تغيير بيانات، لازم تتأكد إنك بتفصل (unsubscribe) عنهم لما ما تكونش محتاجهم، زي لما الـcomponent يتشال من الصفحة أو يتدمر. لو ما عملتش كده، ممكن يحصل تسرب في الذاكرة (memory leaks)، وده بيأثر على أداء التطبيق بتاعك مع الوقت.
- استخدام async pipe دي أسهل طريقة ومريحة جدًا، وبتستخدمها لما يكون عندك observable في (template) بتاعك، وعايز تشتغل معاه. async pipe بيعمل كل الشغل الصعب عنك. يعني بيشترك في الـobservable، ولما الـcomponent يتدمر أو يخرج من الشاشة، بيعمل unsubscribe تلقائيًا.
<div *ngIf="data$ | async as data">
{{ data }}
</div>
- استخدام takeUntil مع Subject الفكرة إنك تعمل Subject كإشارة (notifier) عشان تدي أمر لجميع الـobservables إنهم يوقفوا لما الـcomponent يتم تدميره. بتستخدم takeUntil كـ operator في الـ pipe عشان يتحقق من الـ Subject، وأول ما تبعتله إشعار (signal)، هو هيوقف كل الاشتراكات.
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
export class MyComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.myObservable$
.pipe(takeUntil(this.destroy$)) // هنا بنوقف الاشتراك لما الـdestroy$ تعمل emit
.subscribe((data) => {
console.log(data);
});
}
ngOnDestroy() {
this.destroy$.next(); // بنعمل إشعار إن الـcomponent اتدمر
this.destroy$.complete(); // بننهي الـSubject تمامًا
}
}
في المثال ده، بنستخدم destroy$ كإشارة لوقف الاشتراكات لما ngOnDestroy يتنفذ (الـcomponent يتشال). كل الاشتراكات بتقف لما الـcomponent يتدمر، وده بيمنع تسرب الذاكرة.
- التحديث الجديد في Angular 16: takeUntilDestroyed في Angular 16، ظهر شيء جديد اسمه takeUntilDestroyed وده بيسهل الموضوع أكتر. بدل ما تكتب كود كتير عشان تعمل takeUntil مع Subject، دلوقتي Angular وفرت لك حل جاهز.
دلوقتي، مفيش داعي لإنك تعمل Subject بنفسك وتتحكم فيه. Angular هتتعامل مع الموضوع بشكل أوتوماتيكي.
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
export class MyComponent {
ngOnInit() {
this.myObservable$
.pipe(takeUntilDestroyed(this)) // هنا بنمرر الـcomponent كـcontext
.subscribe((data) => {
console.log(data);
});
}
}
takeUntilDestroyed بيخليك تعمل نفس اللي كنا بنعمله مع takeUntil و Subject، بس بشكل أبسط وأسرع. مش محتاج تعمل أي Subject، ومجرد ما الـcomponent يتدمر، Angular هتعمل unsubscribe تلقائي.
الـ Cold Observable هو النوع اللي بيبدأ يشتغل لما حد يشترك فيه. يعني إيه؟ يعني مش بيبدأ يبعث بيانات أو يعمل أي حاجة إلا بعد ما أول مشترك يظهر.
💡 مثال بسيط: لو عندك طلب HTTP (زي لما تطلب بيانات من سيرفر)، الطلب ده بيكون Cold Observable. ليه؟ لأنه هيبدأ يجيب البيانات ويشتغل بس لما المشترك الأول يشترك فيه.
كل مشترك جديد هيبدأ من الأول كأنه بيطلب نفس البيانات لأول مرة. يعني، لو فيه أكتر من مشترك، كل واحد هيعمل الطلب الخاص بيه.
const coldObservable = new Observable((observer) => {
observer.next(Math.random());
});
coldObservable.subscribe((value) => console.log("Subscriber 1:", value));
coldObservable.subscribe((value) => console.log("Subscriber 2:", value));
في المثال ده، كل مشترك هيستقبل قيمة مختلفة لأن الـobservable هيشتغل من الأول مع كل اشتراك
الـ Hot Observable، على العكس، بيبدأ يبعث بيانات بمجرد ما يشتغل، حتى لو مفيش حد مشترك فيه. وده معناه إن أي مشترك جديد هيشترك في النص ويبدأ ياخد البيانات من اللحظة اللي هو اشترك فيها، مش من الأول.
💡 مثال بسيط: لو عندك event زي حركة الماوس أو ضغطات زرار، ده يعتبر Hot Observable، لأنه بيبعت البيانات أول ما يحصل الحدث، بغض النظر لو كان فيه مشتركين ولا لأ.
كل مشترك جديد بيشترك بياخد البيانات من اللحظة اللي اشترك فيها. لو حاجة حصلت قبل كده، المشترك الجديد مش هيشوفها.
const hotObservable = new Subject();
hotObservable.subscribe((value) => console.log("Subscriber 1:", value));
hotObservable.next(Math.random());
hotObservable.subscribe((value) => console.log("Subscriber 2:", value));
hotObservable.next(Math.random());
- الاتنين بيشتغلوا مع العمليات Asynchronous (غير المتزامنة).
- الاتنين بيتحكموا في إمتى الكود اللي حاطه المستخدم هياخد القيمة.
بيتنفذ فورًا بمجرد ما يتكتب. يعني بمجرد ما تكتب الكود اللي بيعمل promise، هيتنفذ على طول حتى لو لسه ماعملتش then عليه. يعني الكود اللي جواه هيشتغل مباشرة.
بيشتغل بس لما تعمل subscribe، يعني مش هيبدأ يشتغل غير لما تطلبه. ده معناه إنك ممكن "تحضر" الـobservable لعملية async لكن ما تنفذوش غير لما تحتاجه.
بيطلع قيمة واحدة بس أو خطأ. يعني هو يا إما يرجعلك النتيجة أو يرمي خطأ لو حصل مشكلة.
ممكن يطلعلك أكتر من قيمة أو قيمة واحدة أو مفيش قيمة خالص. ده معناه إنه مناسب لحاجات زي الأحداث المتكررة (زي الضغط على زرار أكتر من مرة) أو التعامل مع event emitters في Angular.
مثال: لو عندك حاجة بتعمل استجابة للأحداث زي حركة الماوس أو كتابة المستخدم، الـobservable هو الحل الأمثل.
مينفعش يتلغي. بمجرد ما يبدأ، بيكمل شغله حتى لو مش محتاجه. فيه بعض الطرق أو المكتبات اللي بتحاول تحايل على الموضوع، بس الـPromise في الأساس مش معمول للإلغاء.
معمول إنه يكون قابل للإلغاء، وده بيساعدك في إدارة الموارد بتاعتك بشكل أحسن. تقدر تلغي الاشتراك باستخدام unsubscribe أو من خلال الـoperators.
في Angular، ده بيساعدك تعمل حاجات زي الـ typeahead (بحث مباشر) وتمنع تسرب الذاكرة باستخدام takeUntil اللي بيوقف الـobservable لما الـcomponent يتشال.
مفيهوش operators، يعني مفيش طرق جاهزة للتعامل مع البيانات اللي بترجع من promise غير بإنك تستعمل then أو catch.
عنده أنواع كتير من الـ operators، زي التحويل، الفلترة، التعامل مع الأخطاء، والـoperators دي بتخليك تعمل حاجات معقدة بطرق بسيطة جدًا.
مثال: تقدر تعمل حاجات زي map لتحويل البيانات، أو debounceTime عشان تأخر تنفيذ الـobservable، أو catchError للتعامل مع الأخطاء.
بيبدأ الشغل فورًا وبيطلعلك قيمة واحدة بس. مش قابل للإلغاء، لكن Observable قابل للإلغاء.
بيبدأ بس لما تعمل اشتراك وممكن يطلعلك قيم متعددة. عندها operators قوية جدًا بتخليك تتعامل مع البيانات بسهولة، وده بيفرق كتير في المشاريع الكبيرة.
لما يكون عندك عمليات غير متزامنة كتيرة، زي إنك تعمل طلبات HTTP متتابعة أو تتعامل مع أحداث معينة بشكل مرتب، هنا بيجي دور الـ Higher-order Observable. هو بيساعدك على إنك "تسلسل" العمليات دي بشكل مرتب وفعال.
عشان نتعامل مع الـ Higher-order Observables، بنستخدم Operators زي: mergeMap switchMap concatMap
الـ Operators دي بتساعد على "flatten " أو "دمج" الـObservables اللي طالعين من الـ Higher-order Observable عشان يكون عندك Observable واحد في النهاية، وتقدر تشترك فيه بشكل عادي وتتعامل مع القيم اللي بتطلع منه.
بيعمل flatten للـObservables اللي بتطلع من الـObservable الأصلي، ويشغلهم كلهم بالتوازي. يعني لو فيه أكتر من Observable طالع، كلهم هيشتغلوا في نفس الوقت.
ده بيعمل flatten زي mergeMap، لكن الفرق إنه بيبدل الاشتراك لو فيه Observable جديد طلع. يعني لو فيه Observable جديد طلع، بيكنسل القديم ويشتغل على الجديد بس.
ده بيشتغل زيهم بس بالتتابع. يعني كل Observable يخلص الأول قبل ما اللي بعده يبدأ، وده بيفيد لما يكون فيه ترتيب مهم لازم يتبع.
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable({
providedIn: "root",
})
export class UserService {
private apiUrl = "https://jsonplaceholder.typicode.com/users";
constructor(private http: HttpClient) {}
getUserById(id: number): Observable<any> {
return this.http.get(`${this.apiUrl}/${id}`);
}
}
import { Component, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { UserService } from "./user.service";
import { switchMap, debounceTime, distinctUntilChanged } from "rxjs/operators";
@Component({
selector: "app-user-search",
template: `
<h2>Search for User</h2>
<input
type="number"
[formControl]="userIdControl"
placeholder="Enter User ID"
/>
<div *ngIf="userData">
<h3>User Details:</h3>
<p>Name: {{ userData.name }}</p>
<p>Email: {{ userData.email }}</p>
<p>Address: {{ userData.address.street }}, {{ userData.address.city }}</p>
</div>
`,
})
export class UserSearchComponent implements OnInit {
userIdControl = new FormControl();
userData: any;
constructor(private userService: UserService) {}
ngOnInit() {
// Listen for changes in the user ID input
this.userIdControl.valueChanges
.pipe(
debounceTime(300), // Small delay to allow the user to finish typing
distinctUntilChanged(), // Ensure the new value is different from the previous one
switchMap((id: number) => this.userService.getUserById(id)) // Switch to a new request based on the new ID
)
.subscribe((data) => {
this.userData = data; // Store the user data from the API response
});
}
}
لما يكون عندك Observable بيعمل عملية معينة زي طلب HTTP، الطبيعي إن كل مشترك هيعمل طلب جديد، وده ممكن يكون مشكلة لو عندك مشتركين كتير، لأن كل واحد هيبدأ الطلب من الأول.
لكن لما تستخدم share، المشتركين كلهم هيشتركوا في نفس الـObservable اللي شغال بالفعل، فمش هيعملوا طلبات متكررة، هيستفيدوا من نفس النتيجة.
ده بيشترك في الـObservable الأصلي ويشترك فيه كل المشتركين، بس المشكلة إنه ما بيحتفظش بالقيم اللي طلعت. يعني لو واحد اشترك متأخر، مش هيشوف القيم اللي طلعت قبل ما يشترك.
هنا بقى، ده بيشتغل زي share بس بميزة إضافية، إنه بيحتفظ بعدد معين من القيم اللي طلعت، عشان لو مشترك جديد اشترك متأخر، يقدر يشوف القيم اللي طلعت قبل كده. يعني بيعيد إرسال القيم للمشتركين الجدد.
import { HttpClient } from "@angular/common/http";
import { Component, OnInit } from "@angular/core";
import { shareReplay } from "rxjs/operators";
@Component({
selector: "app-user",
template: `
<h2>User Information</h2>
<div *ngIf="userData">
<p>Name: {{ userData.name }}</p>
<p>Email: {{ userData.email }}</p>
</div>
`,
})
export class UserComponent implements OnInit {
userData: any;
constructor(private http: HttpClient) {}
ngOnInit() {
const userObservable = this.http
.get("https://jsonplaceholder.typicode.com/users/1")
.pipe(
shareReplay(1) // Replay the last emitted value for any new subscribers
);
// First subscriber
userObservable.subscribe((data) => {
console.log("Subscriber 1 received data:", data);
this.userData = data;
});
// Second subscriber
userObservable.subscribe((data) => {
console.log("Subscriber 2 received data:", data);
});
}
}
وده معناه إنك بتتحكم في الفورم من خلال كود الـ TypeScript بدل الـ HTML. الفورم دي مناسبة جدًا لو عايز تعمل فورم معقدة أو فيها عناصر كتير، وبتقدر تتحكم في التحقق من البيانات (validation) بشكل أدق.
ده بيعمل عنصر تحكم واحد في الفورم (زي input واحد).
ده بيجمع مجموعة من عناصر التحكم (مثلاً inputs متعددة).
ده بيعمل لك عناصر تحكم ديناميكية يعني ممكن تزود أو تقلل منهم زي ما تحب.
بيسهل عليك عملية إنشاء الـ FormControl وFormGroup وFormArray بطريقة أسرع وبكود أقل.
النوع ده بيستخدم أكتر مع الفورم البسيطة زي فورم تسجيل دخول
هنا التحكم في الفورم بيكون عن طريق الـ HTML باستخدام ديركتيفات (Directives)، وده أسهل لكن أقل تحكم مقارنة بالـ Reactive Forms.
بتتابع تغييرات القيم في عناصر الفورم زي الـ input وبتتعامل مع التحقق من المدخلات.
دي بتربط نفسها بالفورم كله وبتتابع القيم الكلية وحالة التحقق للفورم كله.
دي بتتعامل مع جزء معين من الفورم وتتحقق من البيانات فيه بشكل منفصل عن باقي الفورم.
لو الفورم بتاعتك بسيطة وعايز تعملها بسرعة، استخدم Template-Driven Forms. لو الفورم معقدة وعايز تحكم أكتر في البيانات والتحقق منها، استخدم Reactive Forms.
userForm = this.formBuilder.group({
name: ['', Validators.required ],
pwd: ['', Validators.required ],
});
onFormSubmit() {
console.log(this.userForm.value);
}
في ملف الـ HTML، هتعمل form فيه form controls. اربط الـ FormGroup في الـ
باستخدام الـ directive formGroup. وكمان اربط كل form control باستخدام الـ directive formControlName.<form [formGroup]="userForm" (ngSubmit)="onFormSubmit()">
<p>Username: <input formControlName="empId"></p>
<p>Password: <input type="password" formControlName="empId"></p>
<button>Submit</button>
</form>
<form #userForm="ngForm" (ngSubmit)="onFormSubmit(userForm)">
<input name="username" ngModel required>
<input type="password" name="pwd" ngModel required>
<button>Submit</button>
</form>
onFormSubmit(form: NgForm) {
console.log(form.value);
}
لما بتعمل form، بتحتاج تتابع البيانات اللي المستخدم بيدخلها وتتأكد إنها صح عن طريق الـ validation، زي مثلًا إنها تكون إيميل صحيح، أرقام معينة، أو حتى قيم مش فاضية. هنا بييجي دور الـ FormControl، اللي بيساعدك تتابع وتحكم في كل الحاجات دي.
const control = new FormControl("initial value", Validators.required);
دي القيمة اللي المستخدم دخلها حاليًا.
ده بيحدد إذا كان الإدخال صحيح ولا لأ.
ده بيحدد إذا كان في أخطاء ولا لأ.
لو المستخدم ما عدلش في الإدخال، بتبقى true.
لو المستخدم غيّر في الإدخال، بتبقى true.
ال FormGroup في Angular بيُستخدم عشان يجمع مجموعة من الـ FormControls في نموذج واحد. يعني لو عندك فورم فيه أكتر من input (زي اسم، إيميل، وكلمة مرور)، تقدر تجمعهم كلهم في FormGroup عشان تتحكم فيهم كلهم مع بعض.
الفكرة ببساطة إنك بتتعامل مع الفورم ككل، بدل ما تتعامل مع كل (input) لوحده. بتقدر تتابع الـ validation والحالة بتاعة كل جوا المجموعة (group).
const form = new FormGroup({
name: new FormControl(""),
email: new FormControl(""),
password: new FormControl(""),
});
دي بترجعلك كل الـ FormControls اللي جوه المجموعة.
بترجع القيم بتاعة كل الحقول في صورة object.
لو كل الـ FormControls جوا المجموعة صالحة (valid)، بتكون true.
لو فيه أي input (input) في المجموعة مش صالح، بتكون true.
بتكون true لو مفيش أي تعديل حصل في أي input.
بتكون true لو حصل تعديل في أي input.
يعني مثلًا لو عندك form فيه أكتر من input والـ input ده ممكن يتكرر زي مثلًا لما المستخدم يضيف أكتر من عنوان بريد إلكتروني أو أكتر من رقم تليفون، هنا بتستخدم FormArray عشان تقدر تضيف الـ inputs دي وتتحكم فيها.
const formArray = new FormArray([new FormControl(""), new FormControl("")]);
ممكن تضيف FormControl جديد في الـ array باستخدام push
formArray.push(new FormControl(""));
formArray.removeAt(0);
const formArray = new FormArray([
new FormControl("", Validators.required),
new FormControl("", Validators.email),
]);
الـ FormRecord في Angular شبه الـ FormGroup، بس الفرق إنه بيتعامل مع مجموعة من الـ FormControl اللي كلهم من نفس نوع القيمة. يعني، الـ FormRecord بيتتبع القيمة وحالة الـ validity لمجموعة من الـ FormControl اللي ليهم نفس نوع البيانات.
الـ FormRecord مفيد جدًا لما تحب تبني form بشكل ديناميكي. تقدر تضيف form controls باستخدام addControl، وتشيلهم بـ removeControl، وهكذا.
@Component({
selector: "my-app",
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: "./my.component.html",
})
export class MyComponent implements OnInit {
userForm!: FormRecord;
formData = [
{ title: "Name", key: "name" },
{ title: "City", key: "city" },
];
ngOnInit() {
this.userForm = new FormRecord<FormControl<string | null>>({});
this.formData.forEach((data) =>
this.userForm.addControl(data.key, new FormControl())
);
}
onFormSubmit() {
console.log(this.userForm.value);
}
}
<form [formGroup]="userForm" (ngSubmit)="onFormSubmit()">
<div *ngFor="let data of formData">
{{data.title}}: <input [formControlName]="data.key"><br />
</div>
<button>Submit</button>
</form>
لما المستخدم يضغط على زرار "Submit"، الـ onFormSubmit هتتنفذ وهتطبع قيم الـ form في الـ console.
بكده، تقدر تستخدم FormRecord عشان تبني forms ديناميكية بسهولة في Angular، وتتحكم في القيم والـ validation بتاعتها.
يعني لو المستخدم عدل في الـ input، الـ valueChanges هيشتغل وهيعمل subscribe لأي function انت محددها. وكمان لو انت برمجياً غيرت القيمة أو عملت enable/disable للـ control، برضه الـ valueChanges هيعمل emit.
تقدر تستخدمه مع FormControl، FormGroup، وFormArray.
studentForm = this.formBuilder.group({
stdName: ['', [ Validators.required ]],
});
this.studentForm.get('stdName').valueChanges.subscribe(
stdName => console.log(stdName);
);
this.studentForm.valueChanges.subscribe(student => {
console.log(student.stdName);
});
studentForm = this.formBuilder.group({
stdName: ['', [ Validators.required ]],
});
this.studentForm.get('stdName').statusChanges.subscribe(
status => console.log(status);
);
this.studentForm.statusChanges.subscribe(status => {
console.log(status);
});
أول حاجة: بنستخدم statusChanges عشان نراقب حالة الـ stdName. لو حصل أي تغيير في الـ validation (يعني مثلاً المستخدم يدخل قيمة أو يسيبها فاضية)، هيتعمل emit للحالة سواء كانت valid أو invalid.
تاني حاجة: بنراقب حالة الـ FormGroup كله عن طريق الـ statusChanges بتاع الـ studentForm. وده بيعرض لنا حالة النموذج بالكامل.
يعني باختصار، الـ statusChanges ده بيساعدك تعرف إمتى الـ form أو الـ input بيغير حالته من صالح (valid) أو غير صالح (invalid) وتقدر تتصرف بناءً على الحالة دي.
setValue بيطلب منك تدي كل القيم لكل الـ controls اللي في الـ form أو الـ FormGroup. يعني لو الفورم بتاعك فيه 3 حقول، وانت استخدمت setValue، لازم تدي قيم للـ 3 حقول دول. لو نسيت أي حقل، هيطلعلك error.
patchValue بيبقى مرن أكتر. مش شرط تدي قيم لكل الـ controls. يعني ممكن تحدث جزء من الفورم بس من غير ما يحصل أي مشكلة. فلو الفورم فيه 3 حقول، وانت استخدمت patchValue لحقلين بس، الحقل التالت هيفضل زي ما هو من غير مشاكل.
form.setValue({
name: "Abdelrahman",
email: "abdelrahman@example.com",
age: 30,
});
form.patchValue({
name: "Abdelrahman",
email: "abdelrahman@example.com",
});
باختصار:
setValue لازم تدي كل القيم لكل الـ inputs. patchValue تقدر تدي قيم لجزء من الـ inputs بس.
addControl(name: string, control: AbstractControl)
this.customerForm.addControl(
"city",
this.formBuilder.control("", [Validators.required])
);
removeControl(name: string)
this.customerForm.removeControl("city");
هنفترض إن عندنا FormGroup اسمه customerForm، وعايزين نضيف حقل (control) اسمه "city" ونزيله بعد كده.
أول حاجة هننشئ FormGroup. هنستخدم addControl عشان نضيف الـ control. بعد كده، لو عايزين نزيل الـ control، هنستخدم removeControl.
import { Component } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
selector: "app-customer-form",
templateUrl: "./customer-form.component.html",
})
export class CustomerFormComponent {
customerForm: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.customerForm = this.formBuilder.group({
name: ["", Validators.required],
});
}
addCityControl() {
this.customerForm.addControl(
"city",
this.formBuilder.control("", [Validators.required])
);
}
removeCityControl() {
this.customerForm.removeControl("city");
}
}
HTML
<form [formGroup]="customerForm">
<label for="name">Name:</label>
<input id="name" formControlName="name" />
<div *ngIf="customerForm.get('city')">
<label for="city">City:</label>
<input id="city" formControlName="city" />
</div>
<button type="button" (click)="addCityControl()">Add City</button>
<button type="button" (click)="removeCityControl()">Remove City</button>
</form>
الngModelChange ده بيشتغل مع حاجة اسمها ngModel، وده حاجة خاصة بـ Angular. يعني لو عندك input في النموذج بتاعك (form)، وعايز تتابع التغير اللي بيحصل في القيمة بتاعته بشكل مباشر، بتستخدم ngModelChange. كل ما القيمة تتغير جوه ngModel، الحدث ده بيشتغل وبيجيبلك القيمة الجديدة على طول باستخدام حاجة اسمها $event.
<input [(ngModel)]="name" (ngModelChange)="onNameChange($event)">
الchange ده عبارة عن DOM event عادي خاص بـ HTML، وبيشتغل لما يحصل تغيير في القيمة بتاعت العنصر (زي input أو select)، لكنه بيتفاعل بس لما المستخدم يخلص التغيير (زي لما يكتب حاجة ويضغط Enter أو يطلع من input).
<input (change)="onInputChange($event)">
بيراقب التغيير بشكل فوري جوه ngModel وبيجيبلك القيمة مباشرة من $event.
بيتفاعل مع التغيير في HTML elements العادية بعد ما المستخدم يخلص التغيير، وبتجيب القيمة باستخدام event.target.value. يعني لو عايز تعرف التغيير أول ما يحصل في الـ input وتتحكم فيه، استخدم ngModelChange. لكن لو عايز تعرف التغيير بعد ما المستخدم يخلص تعديل القيمة، استخدم change.
تقدر تمرر array من async validators كـ argument تالت في FormControl. مثال:
customerForm = this.formBuilder.group({
customerName: ["", [Validators.required], [myCustomAsyncValidator()]],
});
customerForm = this.formBuilder.group({
customerName: [
"",
{
validators: [Validators.required],
asyncValidators: [myCustomAsyncValidator()],
},
],
});
هديك مثال واقعي لموقف ممكن تستخدم فيه async validation في FormControl. خلينا نفترض إنك بتعمل form للمستخدم عشان يسجل بياناته، ومن ضمن البيانات دي اسم المستخدم (username)، ولازم تتأكد إن الاسم ده مش متسجل قبل كده في قاعدة البيانات (دي عملية بتحتاج تحقق من قاعدة البيانات، وبالتالي هتكون async).
@Injectable({ providedIn: "root" })
export class UserService {
constructor(private http: HttpClient) {}
checkUsername(username: string): Observable<boolean> {
return this.http.get<boolean>(
`/api/users/check-username?username=${username}`
);
}
}
export class UserFormComponent {
constructor(
private formBuilder: FormBuilder,
private userService: UserService
) {}
customerForm = this.formBuilder.group({
username: ["", [Validators.required], [this.checkUsernameAvailability()]],
});
checkUsernameAvailability(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return this.userService.checkUsername(control.value).pipe(
map((isAvailable) => {
return isAvailable ? null : { usernameTaken: true };
})
);
};
}
}
<form [formGroup]="customerForm" (ngSubmit)="onSubmit()">
<label for="username">Username</label>
<input id="username" formControlName="username">
<div *ngIf="customerForm.get('username').hasError('usernameTaken')">
This username is already taken.
</div>
<button type="submit" [disabled]="customerForm.invalid">Submit</button>
</form>
<input name="uname" ngModel required maxlength="20" #usrname="ngModel">
<div *ngIf="usrname.errors?.['required']">
Enter user name.
</div>
<div *ngIf="usrname.errors?.['maxlength']">
Maxlength is 20.
</div>
{{usrname.status}}
message = "Hello World!";
<input [(ngModel)]="message">
{{message}}
لو عايز تسأل المستخدم "عايز تحفظ التعديلات قبل ما تسيب الصفحة؟
عايز تتحكم مين يدخل على أجزاء معينة في التطبيق (زي صفحات الأدمن مثلاً)
لو محتاج تتأكد من باراميتر معين (زي ID بتاع المستخدم مثلاً) قبل ما تروح لصفحة معينة
لو عندك بيانات محتاج تجيبها من السيرفر قبل ما تعرض الكومبوننت،
فيه أكتر من حالة ممكن تحتاج فيها CanActivate Guard
لو المستخدم مسجّل دخول: بنستخدمه عشان نتأكد إن المستخدم عامل تسجيل دخول في النظام. لو مش عامل تسجيل، نرجعه لصفحة تسجيل الدخول.
لو المستخدم ليه الصلاحية: بنشوف لو المستخدم عنده الصلاحية يدخل على صفحة معينة (زي صفحة الأدمن).
عشان نستخدم CanActivate Guard، بنعمل Service في Angular.
إنشاء Service: بنعمل Service جديد في التطبيق يكون مسؤول عن الحماية دي.
ب Import CanActivate Interface: في الـ Service، بنستورد CanActivate Interface من @angular/router، ودي بتخلينا نحدد طريقة اسمها canActivate.
تنفيذ canActivate Method: الطريقة دي بتتأكد لو الشرط اتحقق وترجع true لو ممكن نكمل وندخل، أو false لو مش هينفع نكمل. كمان ممكن ترجع UrlTree (دي زي لينك) عشان تحول المستخدم على صفحة تانية لو الشرط مش موجود.
import { Injectable } from "@angular/core";
import {
Router,
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from "@angular/router";
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private _router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
//check some condition
if (someCondition) {
alert("You are not allowed to view this page");
//redirect to login/home page etc
//return false to cancel the navigation
return false;
}
return true;
}
}
Update the route definition with the canActivate guard as shown below. You can apply more than one guard to a route and a route can have more than one guard
{ path: 'product', component: ProductComponent, canActivate : [AuthGuardService] },
الCanActivateChild Guard ده زي CanActivate Guard، بس الفرق إنه بيتطبق على الأب اللي جواه Routes الفكرة هنا إنك لما تحط CanActivateChild Guard على أي Route أب، كل ما المستخدم يحاول يدخل على واحدة من الصفحات الفرعية (الـ Children)، الـ Guard ده بيشتغل ويتأكد لو مسموح له يدخل ولا لأ.
الCanActivate: بتحطه على أي Route (سواء Route عادي أو Parent) وبيتحكم في دخول المستخدم للـ Route دي كلها.
الCanActivateChild: بتحطه على الـ Parent Route وبيشتغل على كل الـ Child Routes اللي جواه. يعني بدل ما تحط Guard على كل Route فرعية، ممكن تحط CanActivateChild مرة واحدة على الـ Parent وهو هيعمل الحماية المطلوبة لكل اللي جواه.
لو عندك مثلاً صفحة Products (اللي هي الـ Parent)، وعندك جواها كذا Route فرعي (زي عرض منتج معين، تعديل منتج، إضافة منتج)
باستخدام CanActivate، لو حطيت Guard على Products، ده هيمنع الدخول على الـ Parent والـ Children كلها في حالة إن المستخدم مش عامل تسجيل دخول.
باستخدام CanActivateChild، ممكن تسمح لكل الناس تشوف صفحة Products، بس تمنع الدخول للـ Routes الفرعية (زي التعديل والإضافة) وتخليها بس للأدمن.
import { Injectable } from "@angular/core";
import {
Router,
CanActivateChild,
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from "@angular/router";
@Injectable()
export class AdminGuardService implements CanActivateChild {
constructor(private _router: Router) {}
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
// بنفترض إن في شرط معين زي إن المستخدم يكون أدمن
const isAdmin = false; // حط الشرط المناسب هنا
if (!isAdmin) {
alert("مش مسموح ليك تدخل هنا!");
this._router.navigate(["/login"]); // يرجع المستخدم لصفحة تانية لو مش أدمن
return false;
}
return true;
}
}
{ path: 'product', component: ProductComponent, canActivate : [AuthGuardService] ,
canActivateChild : [AuthGuardService],
children: [
{ path: 'view/:id', component: ProductViewComponent },
{ path: 'edit/:id', component: ProductEditComponent },
{ path: 'add', component: ProductAddComponent }
]
}
CanActivate: يمنع أو يسمح بالوصول للـ Route ككل (الـ Parent والـ Children).
CanActivateChild: بيتحط على الـ Parent وبيشتغل على كل Child جواه، وبيمنع الوصول للـ Routes الفرعية بناءً على الشرط.
الCanDeactivate Guard ده بيشتغل لما المستخدم يحاول يسيب الصفحة أو الـ Component الحالية وعايز يروح لصفحة تانية.
الهدف الأساسي منه إنه يمنع المستخدم من الخروج لو عنده بيانات لسه متسجلتش، زي لما يكون كاتب بيانات في فورم بس لسه ما حفظهاش.
مثلاً في حالات زي ملء بيانات في فورم: لو المستخدم كتب بيانات بس لسه ما ضغطش على "حفظ" وحاول يسيب الصفحة، CanDeactivate Guard يقدر يوقفه ويسأله "هل انت متأكد إنك عايز تخرج من غير ما تحفظ؟". لو وافق، ينقله للصفحة الجديدة، ولو رفض، يخليه يكمل في نفس الصفحة.
عشان تفعّل CanDeactivate Guard، محتاج تعمل Service خاصة بيه.
خطوات التنفيذ: إنشاء Service: بنعمل Service جديد، والـ Service ده بيستورد CanDeactivate Interface من angular/router@
تنفيذ canDeactivate Method: الطريقة canDeactivate بتاخد الكومبوننت الحالي كـ argument، وبتاخد كمان معلومات عن الـ Route الحالي والـ Route اللي المستخدم رايحله. بناءً على الشرط اللي تحدده، بتحدد لو ممكن نسيب الصفحة أو لأ.
أول حاجة، بنعمل الكومبوننت اللي عايزين نحميه، وليكن اسمه RegisterComponent. جواه بنعمل طريقة اسمها canExit، والطريقة دي بتتحقق لو فيه بيانات غير محفوظة وتسأل المستخدم لو عايز يخرج أو يكمل.
import { Component } from "@angular/core";
@Component({
templateUrl: "register.component.html",
})
export class RegisterComponent {
// الطريقة دي بتتحقق لو فيه بيانات غير محفوظة وتسأل المستخدم
canExit(): boolean {
if (confirm("هل تريد فعلاً الخروج بدون حفظ التعديلات؟")) {
return true; // يسمح بالخروج
} else {
return false; // يخلّي المستخدم في الصفحة
}
}
}
import { Injectable } from "@angular/core";
import { CanDeactivate } from "@angular/router";
import { RegisterComponent } from "./register.component";
@Injectable({
providedIn: "root",
})
export class CanDeactivateGuardService
implements CanDeactivate<RegisterComponent>
{
canDeactivate(component: RegisterComponent): boolean {
return component.canExit(); // بيشغل canExit وبيتأكد لو المستخدم عايز يسيب الصفحة فعلاً
}
}
في ملف الـ Routing، بنضيف الـ Guard ده على الصفحة اللي عايزين نحميها، مثلاً على RegisterComponent
{ path: 'register', component: RegisterComponent, canDeactivate: [CanDeactivateGuardService] }
الCanDeactivate Guard بيمنع الخروج من الصفحة لو فيه بيانات غير محفوظة.
بيسأل المستخدم لو عايز يخرج بدون حفظ، ولو وافق، يكمل، ولو رفض، يفضل في نفس الصفحة.
بيساعدك تحمي بيانات المستخدم لو كان لسه ما حفظهاش قبل ما يخرج.
الResolve Guard في Angular بيعمل حاجة مهمة جداً وهي إنه يحمل البيانات من السيرفر قبل ما الصفحة تفتح أصلاً. بدل ما الكومبوننت يظهر فاضي وبعدين يستنى لحد ما البيانات توصله، Resolve Guard بيحمل البيانات الأول وبعدين يفتح الكومبوننت على طول والبيانات جاهزة.
عادةً في الكومبوننت العادية، لما بنستخدم ngOnInit لجلب البيانات، المستخدم ممكن يشوف صفحة فاضية لحد ما البيانات توصله. في الحالة دي، ممكن نستخدم loading spinner عشان نبين إن فيه حاجة بتحصل. لكن Resolve Guard بيخلّي البيانات موجودة من قبل ما الصفحة تفتح، فبيحسن التجربة ويخلي الصفحة تظهر كاملة للمستخدم.
إنشاء Service: بنعمل Service جديدة بتحمل البيانات المطلوبة. الـ Service دي هتطبق Resolve Interface
تنفيذ resolve method: جوه الـ Service، هنضيف method اسمها resolve بتجيب البيانات المطلوبة وترجعها. لازم ترجع Observable أو Promise عشان Angular تستنى البيانات دي قبل ما تفتح الكومبوننت.
import { Injectable } from "@angular/core";
import {
Resolve,
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from "@angular/router";
import { ProductService } from "./product.service";
import { Observable } from "rxjs";
@Injectable({
providedIn: "root",
})
export class ProductListResolveService implements Resolve<any> {
constructor(private productService: ProductService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> {
return this.productService.getProducts();
}
}
عشان تفعّل الـ Resolve Guard ده، لازم تضيفه على Route بتاع الصفحة اللي عايز تفتحها بالبيانات الجاهزة.
{ path: 'product', component: ProductComponent, resolve: { products: ProductListResolveService } },
الproducts هو اسم المتغير اللي هنستخدمه في الكومبوننت عشان نجيب البيانات. ProductListResolveService هو الـ Service اللي جبنا منه البيانات.
جوه الكومبوننت، هنجيب البيانات الجاهزة من ActivatedRoute بدل ما نعمل طلب جديد.
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
@Component({
selector: "app-product",
templateUrl: "./product.component.html",
})
export class ProductComponent implements OnInit {
products: any;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.products = this.route.snapshot.data["products"]; // هنا بنجيب البيانات
}
}
🔴 أولوية التنفيذ: Resolve Guard بيتنفذ بعد كل الـ Guards التانية.
🔴 إلغاء التنقل: لو الـ Resolver رجّع null أو حصل خطأ، Angular بتلغي التنقل ومش هتروح للـ Route.
🔴 استخدام أكتر من Resolver: تقدر تحط أكتر من Resolve على نفس الـ Route، يعني تقدر تجيب بيانات من مصادر مختلفة في نفس الوقت.
{ path: 'product', component: ProductComponent,
resolve: {products: ProductListResolveService, , data:SomeOtherResolverService} }
الCanLoad Guard ده بيمنع تحميل الـ Lazy Loaded Modules لو المستخدم مالوش صلاحية.
بمعنى تاني، لو عندك Module مش عايز مستخدم معين يشوفه (زي صفحة الـ Admin)، الـ CanLoad بيمنع تحميل الـ Module ده بالكامل عشان المستخدم لا يقدر يشوف الصفحة ولا حتى يشوف كود الصفحة من المتصفح.
الCanActivate: بيمنع المستخدم من الدخول للصفحة، لكن ما بيمنعش تحميل كود الصفحة، يعني ممكن المستخدم يشوف الكود من الـ Developer Tools.
الCanLoad: بيمنع تحميل كود الصفحة بالكامل. المستخدم مش هيشوف الكود ولا هيقدر يدخل على الصفحة.
إنشاء Service جديدة: بنعمل Service بتطبق CanLoad Interface عشان نحدد الشروط اللي تمنع أو تسمح بالتحميل.
تنفيذ canLoad method: الطريقة دي بتتحقق من الـ Route اللي المستخدم عايز يدخل عليها، ولو الشروط متحققة بترجع true عشان يتم التحميل، أو false عشان تمنع التحميل.
نبدأ بإنشاء Service جديدة، وليكن اسمها AuthGuardService، ونضيف فيها الشروط.
import { Injectable } from "@angular/core";
import { CanLoad, Route, Router } from "@angular/router";
@Injectable()
export class AuthGuardService implements CanLoad {
constructor(private router: Router) {}
canLoad(route: Route): boolean {
let url: string = route.path;
console.log("Url:" + url);
if (url === "admin") {
// لو المستخدم حاول يدخل صفحة الأدمن
alert("غير مسموح ليك تدخل الصفحة دي");
return false; // تمنع تحميل الـ Module
}
return true; // تسمح بالتحميل للصفحات التانية
}
}
عشان تربط الـ Guard ده بالـ Routes، بتحط AuthGuardService في خاصية canLoad للـ Route اللي عايز تحميها.
const routes: Routes = [
{
path: "admin",
loadChildren: "./admin/admin.module#AdminModule",
canLoad: [AuthGuardService],
},
{
path: "test",
loadChildren: "./test/test.module#TestModule",
canLoad: [AuthGuardService],
},
];
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { AuthGuardService } from "./auth-gaurd.service";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [AuthGuardService],
bootstrap: [AppComponent],
})
export class AppModule {}
الHTTP Interceptor في Angular ده بيشتغل كوسيط ما بين التطبيق بتاعنا والسيرفر.
لما التطبيق يبعت طلب للسيرفر (زي GET أو POST)، الـ Interceptor بيقدر يمسك الطلب ده قبل ما يتبعت ويقدر يعدل عليه.
كمان لما السيرفر يرد علينا، الـ Interceptor يقدر يمسك الرد ويعدّل فيه لو محتاج.
واحدة من أهم الفوايد هي إنك تقدر تضيف Authorization Header (زي توكن بتاع تسجيل الدخول) على كل طلب.
يعني بدل ما تحط التوكن بشكل يدوي في كل مكان، الـ Interceptor بيضيفه تلقائي لكل طلب، وده بيوفر مجهود ويقلل الأخطاء.
كمان تقدر تستخدمه عشان تمسك الأخطاء اللي بترجع من السيرفر، زي لو في مشكلة في الاتصال أو السيرفر رد بحاجة غلط. ممكن تخليه يسجل الأخطاء دي في اللوج أو يعرض للمستخدم رسالة.
إضافة Headers: تقدر تضيف Headers مخصصة (زي Authorization) لكل طلب خارج.
التعامل مع الأخطاء: تقدر تمسك الأخطاء اللي بترجع من السيرفر وتتعامل معاها قبل ما توصل للتطبيق.
إنشاء Service جديدة: الأول بنعمل Service جديدة، ودي لازم تطبّق HttpInterceptor Interface من Angular.
import { Injectable } from "@angular/core";
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
} from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable()
export class AppHttpInterceptor implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// هنا تقدر تعمل أي تعديل على الطلب
console.log("Interceptor شغال!");
// بترجع الطلب عشان يكمل ويعمله send
return next.handle(req);
}
}
الintercept هي الطريقة الأساسية اللي بتشتغل على كل طلب. بتاخد req اللي هو الطلب الحالي وnext اللي هو المسؤول عن تمرير الطلب للخطوة اللي بعدها.
الnext.handle(req) بيكمل إرسال الطلب بعد التعديلات اللي انت عملتها. تسجيل الـ Interceptor في الـ Root Module: عشان Angular تستخدم الـ Interceptor، لازم تضيفه في providers في AppModule وتستخدم الـ HTTP_INTERCEPTORS عشان تسجله كـ Multi Provider.
import { HTTP_INTERCEPTORS } from "@angular/common/http";
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AppHttpInterceptor,
multi: true,
},
],
})
export class AppModule {}
هنا بنضيف AppHttpInterceptor كـ provider في التطبيق، وmulti: true معناها إنه يقدر يستخدم أكتر من Interceptor في نفس الوقت لو عندك أكتر من واحد.
لما بنبعت طلب HTTP من Angular للسيرفر (زي لما نجيب بيانات أو نبعت بيانات)، الطلب ده بيتبعت ومعاه Headers (زي بطاقة تعريف بتحط شوية معلومات عن الطلب، زي نوع البيانات اللي بنبعتها أو إن كان المستخدم مسجّل دخول).
فيه حالات بنحتاج نضيف أو نعدل أو حتى نحذف Headers قبل ما الطلب يتبعت للسيرفر.
هنا بنستخدم حاجة اسمها HTTP Interceptor، ودي بتخلينا "نعترض" الطلب ونعدل عليه قبل ما يتبعت.
req = req.clone({
headers: req.headers.set("Content-Type", "application/json"),
});
في Angular، الطلبات (requests) والأجزاء بتاعتها زي الـ Headers بتكون ثابتة (Immutable)، يعني ماينفعش نعدل عليها مباشرةً. علشان كده، لما نحتاج نغير حاجة في الطلب، لازم نعمل نسخة (Clone) منه.
لما نبعت بيانات للسيرفر، بنحط نوع البيانات اللي بنبعتها في الهيدر Content-Type. مثلاً لو البيانات بصيغة JSON، بنضيف الهيدر بالشكل ده
هنا set بتعمل نسخة جديدة من الهيدر وبتضيف Content-Type لو مش موجود أو بتعدله لو كان موجود.
ممكن تستخدم append بدل set، ودي بتضيف Header جديد حتى لو كان نفس الهيدر موجود قبل كده:
req = req.clone({
headers: req.headers.append("Content-Type", "application/json"),
});
لو مش عايز تضيف نفس Header مرتين، ممكن تتأكد قبل بإستخدام:
if (!req.headers.has("Content-Type")) {
req = req.clone({
headers: req.headers.set("Content-Type", "application/json"),
});
}
الـ Token هو زي كود سري بيستخدمه المستخدم لما يكون عامل تسجيل دخول.
بنضيفه في الهيدر Authorization عشان نقول للسيرفر "المستخدم ده معاه صلاحيات".
بنستخدم طريقة زي دي، نفترض إن التوكن متخزن في Service بتاعتك:
const token: string = authService.Token; // بنجيب التوكن من الـ Service
if (token) {
req = req.clone({
headers: req.headers.set("Authorization", "Bearer " + token),
});
}
في الكود ده بنضيف "Bearer" قبل التوكن، وده أسلوب متعارف عليه في الـ Authorization Headers.
لما نبعت طلب للسيرفر، بنستنى رد (Response) يوصلنا.
الـ Interceptor بيخلينا "نعترض" الرد ده، نعدله أو نسجل أي حاجة محتاجينها قبل ما يوصل للتطبيق نفسه.
في الحالة دي، بنستخدم RxJS Operators زي map, tap catchError, و retry عشان نتحكم أكتر في الرد.
الtap بيخلينا نقدر نسجل أحداث معينة، زي الوقت اللي أخده الطلب عشان يخلص.
دي طريقة كويسة لو عايز تعرف الوقت اللي الطلب استغرقه، أو تسجل أي معلومات إضافية عن الطلب أو الرد.
في الكود ده، tap بيتنفذ مرتين: مرة لما الطلب يبعت، ومرة تانية لما الرد يوصل.
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
req = req.clone({ headers: req.headers.append('Content-Type', 'application/json') });
const started = Date.now();
return next.handle(req).pipe(
tap(event => {
const elapsed = Date.now() - started;
console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
if (event instanceof HttpResponse) {
console.log('Response Received');
}
})
);
}
✨الكود ده بيحسب الزمن اللي أخده الطلب عشان يوصل الرد، وبيسجل رسالة لما الاستجابة توصل.
الmap بنستخدمه عشان نعدل محتوى الرد قبل ما يوصل للتطبيق. لو فيه بيانات محتاجين نغيرها أو نبدّلها، بنقدر نستخدم map عشان نعمل ده.
في المثال ده، بنستخدم map عشان نغير محتوى الرد بالكامل، ونحط بيانات جديدة في (body) بتاع الرد:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
map(resp => {
const myBody = [{ 'id': '1', 'name': 'TekTutorialsHub', 'html_url': 'www.tektutorialshub.com', 'description': 'description' }];
if (resp instanceof HttpResponse) {
resp = resp.clone<any>({ body: myBody });
return resp;
}
return resp;
})
);
}
الcatchError بنستخدمه عشان نمسك أي خطأ حصل أثناء الطلب ونتعامل معاه، زي مثلاً لو الطلب رجّع 401 Unauthorized، نعرف نوجّه المستخدم لصفحة تسجيل الدخول.
في الكود ده، catchError بيمسك الأخطاء ويعرض التفاصيل الخاصة بيها، ولو كان الخطأ 401، ممكن نوجه المستخدم للصفحة المناسبة:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token: string = 'invalid token';
req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + token) });
return next.handle(req).pipe(
catchError(err => {
console.error(err);
if (err instanceof HttpErrorResponse) {
console.log(err.status, err.statusText);
if (err.status === 401) {
// توجيه المستخدم لصفحة تسجيل الدخول
}
}
return of(err); // بترجع الخطأ كـ Observable
})
);
}
لو المستخدم مش مسجّل دخول مثلاً، ممكن نلغي الطلب عشان ما يتبعتش للسيرفر، وده بنعمله بإرجاع EMPTY (اللي هو Observable فاضي).
import { EMPTY } from 'rxjs';
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (NotLoggedIn) {
return EMPTY; // الطلب مش هيتبعت
}
return next.handle(req);
}
في Angular، التعامل مع errors جزء مهم من تصميم التطبيق لأن JavaScript ممكن ترمي errors في أي وقت يحصل فيه حاجة غلط. مثلاً
بجانب الحاجات دي، ممكن يحصل errors غير متوقعة زي انقطاع الاتصال، مفيش إنترنت، أو HTTP errors زي المستخدم غير مصرح له أو إن (Session) انتهت.
الـ HTTP Errors بتحصل لما نبعت request باستخدام HttpClient في Angular. الـ errors دي ممكن تكون إما من السيرفر (زي مستخدم غير مسموح أو انتهاء الجلسة)، أو من عند العميل (زي فشل الاتصال). الفكرة إن HttpClient بيقدر يتعامل مع الـ HTTP responses، والـ errors بتتقسم لنوعين رئيسيين:
الErrors من السيرفر: زي ما يكون المستخدم غير مصرح له (401 Unauthorized) أو انتهت Session أو السيرفر واقع.
الأخطاء اللي بتحصل في الكود مباشرة بتتسمى Client-Side Errors، ودي بيتم التعامل معاها عن طريق ErrorHandler وهو الـ default error handler اللي Angular بيستخدمه عشان يمسك أي exception غير متوقع.
بشكل افتراضي، ErrorHandler في Angular بيمسك كل errors اللي بتحصل في التطبيق، لكن دوره بيقتصر على طباعتها في الـ console. لكن في التطبيقات الكبيرة، لازم يكون عندنا Global Error Handler مخصص عشان نقدر نعرض رسائل واضحة للمستخدم أو نبعت التقارير دي للسيرفر لمتابعة errors.
import { ErrorHandler, Injectable } from "@angular/core";
@Injectable()
export class GlobalErrorHandlerService implements ErrorHandler {
handleError(error) {
console.error("An error occurred:", error.message);
alert("An error occurred, please try again later!");
}
}
ممكن تحتاج إنك تستخدم service زي Router عشان تعمل توجيه للمستخدم لصفحة مخصصة للخطأ. بس Angular بتعمل الـ Error Handler قبل ما باقي الـ services تبقى جاهزة، عشان كده بنستخدم حاجة اسمها Injector عشان نحقن الservice يدويًا.
import { ErrorHandler, Injectable, Injector } from "@angular/core";
import { Router } from "@angular/router";
@Injectable()
export class GlobalErrorHandlerService implements ErrorHandler {
constructor(private injector: Injector) {}
handleError(error) {
let router = this.injector.get(Router);
console.log("Navigating to error page");
router.navigate(["/error"]);
}
}
ال object ده فيه معلومات مهمة عن الـ error زي سبب المشكلة والـ status code (زي 401 Unauthorized أو 404 Not Found). الـ HttpErrorResponse بيحتوي على الـ error اللي حصل سواء من السيرفر أو من الكود بتاع العميل.
أنت ممكن تمسك الـ HTTP errors في الكومبوننت بعد ما تعمل request. على سبيل المثال، لو أنت عامل GitHubService عشان تجيب البيانات من API زي الـ Repositories الخاصة بمستخدم، هتكتب subscribe للـ HTTP request في الكومبوننت عشان تتعامل مع الـ response والـ error.
public getRepos() {
this.loading = true;
this.errorMessage = "";
this.githubService.getRepos(this.userName)
.subscribe(
(response) => { // Next callback
console.log('response received');
this.repos = response;
},
(error) => { // Error callback
console.error('error caught in component');
this.errorMessage = error;
this.loading = false;
}
);
}
أنت كمان ممكن تمسك الـ HTTP errors في الـ service اللي بيعمل الـ request باستخدام الـ catchError من مكتبة RxJS.
getRepos(userName: string): Observable<any> {
return this.http.get<repos[]>(this.baseURL + 'usersY/' + userName + '/repos')
.pipe(
catchError((err) => {
console.log('error caught in service');
console.error(err);
return throwError(err); // رمي الـ error تاني عشان الكومبوننت يتعامل معاه
})
);
}
لما تيجي تتعامل مع HTTP errors اللي بتحصل كتير (زي Unauthorized 401 أو Not Found 404)، مش منطقي إنك تمسك كل error في كل component أو service. هنا بييجي دور الـ HTTP Interceptor. الـ interceptor هو سيرفس بيشتغل في الخلفية وبيمسك كل request وresponse وبيعمل interception للـ errors عشان يتعامل معاها.
@Injectable()
export class GlobalHttpInterceptorService implements HttpInterceptor {
constructor(public router: Router) {}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error) => {
if (error instanceof HttpErrorResponse) {
switch (error.status) {
case 401:
this.router.navigateByUrl("/login");
break;
case 403:
this.router.navigateByUrl("/unauthorized");
break;
}
}
return throwError(error);
})
);
}
}
Error 401 (Unauthorized): هنا ممكن نعمل redirect لصفحة تسجيل الدخول. Error 403 (Forbidden): نقدر نوجه المستخدم لصفحة غير مصرح له بالدخول ليها.
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: GlobalHttpInterceptorService,
multi: true,
},
];