الEffects أداة قوية في Angular، لكن مش لازم تستخدمها في كل الحالات.
في حالات معينة، استخدام effects بيكون مناسب، وفي حالات تانية ممكن يسبب تعقيد بدون فائدة حقيقية.
خلينا نستعرض إمتى نستخدمها وإمتى لأ، وإيه البدائل المتاحة.
الEffects مفيدة بشكل أساسي للتعامل مع الحاجات اللي مش بتقدر تعرضها باستخدام (data binding).
في Angular، ربط البيانات (data binding) هو الطريقة اللي تقدر بيها تعرض أو تربط البيانات بالواجهة (UI) بسهولة، زي لما تظهر قيمة في مكان معين أو تربط زرار بوظيفة. لكن في بعض الحالات، ربط البيانات ما بيكونش كافي أو ما ينفعش أصلاً بسبب احتياجك لتفاعل مباشر أكتر.
في Angular، فيه حاجة اسمها (auto-tracking)، ودي شغالة مع computed وeffects. يعني Angular بتراقب أي قيمة (Signal) حتى لو مش مستخدمة بشكل مباشر في الكود، لكن طالما فيه تأثير عليها بشكل غير مباشر.
error = signal(null); // تبدأ بقيمة null أو undefined
effect(() => {
this.logError();
});
logError(): void {
const error = this.error();
if (error) {
console.error(error);
}
}هنا عندنا effect بيشغل logError، وداخل logError بنستخدم قيمة error. مع إن error مش موجودة مباشرة في effect،
الAngular عارفة إن فيه علاقة غير مباشرة بينها وبين effect. فلو حصل تغيير في error، effect هيشتغل تاني تلقائي.
التصميم ده بيأكد إن Effects في Angular معمولة بشكل أساسي للتعامل مع عرض البيانات (rendering).
أي قيمة بيتم التعامل معاها أثناء العرض، Angular بتراقبها تلقائي، ولو حصل أي تغيير فيها، بيتم تحديث العرض مباشرة.
مع ظهور الـ Signals في Angular، بقى عندنا دلوقتي طريقة جديدة تماماً لكتابة الـ Components في Angular باستخدام الـ Signal-based APIs، ومن غير الحاجة للـ Decorators.
جزء كبير من الطريقة الجديدة دي في كتابة الـ Components هو إدخال حاجة جديدة اسمها Signal-based Template Queries
الـ APIs دي بقت بديل جديد عن الـ Decorator-based Template Queries التقليدية زي ViewChild@، ContentChild@، ViewChildren@، و ContentChildren@.
🔴 بيشتغلوا بنفس الطريقة بالظبط، لكن:
هنتعمق في التفاصيل بتاعت الـ Signal Queries دي، وهنستكشف الـ Syntax بتاعهم، طرق استخدامهم، والمميزات اللي بتخليهم أحسن من الـ Traditional Decorator-based Queries.
الviewChild ده أسلوب بنستخدمه عشان نوصل لعناصر موجودة جوه تصميم (template) الكومبوننت بتاعنا.
العناصر دي ممكن تكون يا إما أجزاء (components) تانية معمولالها design في Angular أو عناصر HTML عادية زي <div> و <button>.
الـ viewChild بيعمل نفس وظيفة الـ ViewChild decorator، يعني الاتنين بيساعدونا نوصل للعناصر دي ونتعامل معاها.
هنبدأ الأول إزاي نوصل لعناصر الـ HTML العادية، وبعد كده هنشوف إزاي نوصل للكومبوننتات (components)
عشان نجيب عنصر من الـtemplate، لازم نديله "template reference" باستخدام علامة #.
ده مثال على الطريقة:
@Component({
selector: "book",
template: `
<div>
<b #title>Title</b>
</div>
`,
})
class BookComponent {
title = viewChild<ElementRef>("title");
constructor() {
effect(() => {
console.log("Title: ", this.title()?.nativeElement);
});
}
}زي ما انت شايف، استخدمنا title # كـ template reference عشان نوصل للعنصر الـ HTML نفسه.
وبعدين استخدمنا viewChild عشان نجيب العنصر اللي حطينا عليه الـ reference ده.
الـ viewChild بيرجع لنا "signal" بتحتوي على العنصر اللي جبناه، بس بيكون جوه ElementRef.
عشان نوصل للنتيجة اللي رجعته الـ query، كل اللي علينا إننا نتابع "الsignal" دي باستخدام أي API بيتعامل مع الإشارات زي ()effect أو ()computed.
لاحظ إن عشان نوصل للعنصر الـ HTML الفعلي، لازم نستخدم خاصية nativeElement من قيمة الـ signal.
ده لأن ElementRef هو مجرد غلاف حوالين عنصر الـ DOM الحقيقي، مش العنصر نفسه.
خد بالك كمان إننا استخدمنا الـ <ElementRef> في viewChild، عشان نحدد نوع القيم اللي الـ signal هترجعها.
لو ماعملناش كده، الـ signal هتتعامل على إنها بترجع قيم من نوع unknown، وده مش اللي احنا عايزينه.
وبكده تكون عرفت إزاي تعمل query لعناصر HTML العادية باستخدام ()viewChild بكل بساطة!
لاحظ إننا مش محتاجين نستخدم الـ AfterViewInit lifecycle hook التقليدي زي ما كنا بنعمل مع الـ ViewChild decorator.
بدل كده، استخدمنا ()effect كـ signal بسيطة عشان نستقبل إشعار لما عنصر title يبقى جاهز.
وكمان ممكن نستخدم ()computed عشان نستخرج قيم من الـ title signal.
الـ title signal هي مجرد signal للقراءة فقط (read-only)، فده بيخليها سهلة في التعامل وبتندمج بشكل سلس مع باقي الأدوات المبنية على signals في الـ framework.
ده معناه إننا بشكل عام مش هنحتاج نستخدم AfterViewInit تاني لما نشتغل باستخدام query signals بدل الـ decorator التقليدي.
<div>
<b #title>First Title</b>
<b #title>Second Title</b>
</div>
<p #title>Paragraph Title</p>في الحالة دي، viewChild هيختار أول عنصر موجود فيه title#، ومش هيظهر أي خطأ.
ده يعني إنه لما يكون في أكتر من عنصر ليهم نفس الـ template reference (زي title# هنا)، viewChild دايمًا هياخد أول واحد بس ويهمله الباقي.
بجانب عناصر HTML العادية، نقدر كمان نجيب الـ component instances باستخدام ميزة viewChild.
ممكن نعمل query للـ components إما باستخدام template references زي ما عملنا قبل كده، أو باستخدام كلاس الـ component نفسه.
خلينا نبدأ بعمل query للـ components باستخدام template references:
@Component({
template: `
<div>
<book #book></book>
</div>
`,
})
class BookListComponent {
bookComponent = viewChild<BookComponent>("book");
}زي ما شايف، إحنا هنا بنستخدم component في الـ template، وحطينا ليه template reference باسم book.
لو مررنا الـ reference ده لـ viewChild، الـ query signal هترجع لنا الـ instance بتاع الـ BookComponent نفسه، مش عنصر الـ HTML المرتبط بيه.
✨ده معناه إننا نقدر نتعامل مع الـ component instance مباشرة، زي كده:
this.bookComponent().title;
this.bookComponent().hello();خد بالك من استخدام الـ generic parameter BookComponent . البراميتر ده مهم عشان يعرف viewChild نوع الـ component اللي متوقعه من الـ query دي.
وبكده تكون عرفت إزاي تستخدم viewChild عشان تجيب الـ component instances بشكل مباشر، بكل بساطة!
الـ viewChild signal query بيعمل الـ query أول مرة لما الـ view بيتم تهيئته (initialize).
لو في أي وقت العنصر اللي جبناه اتحذف أو اتعمله إعادة عرض (re-rendered) أو اتحدّث في شجرة الكومبوننت، الـ signal query هتعيد الـ query عشان تحدث نفسها بالحالة الحالية للعنصر.
لو العنصر اتحذف، قيمة الـ signal query هتبقى undefined.
لكن لما العنصر يتخلق تاني، الـ signal query هترجع تجيب العنصر من الـ template view مرة تانية.
يعني زي ما شايف، الـ signal query بتحدّث نفسها تلقائيًا في كل مرة يحصل فيها تغيير في الـ detection run.
الـ viewChild بتشتغل بشكل افتراضي إنها بترجع:
العنصر HTML عادي مغلف في ElementRef لو اللي بنسأل عنه هو عنصر HTML.
الكومبوننت نفسه لو العنصر ده هو كومبوننت.
لكن في بعض الحالات ممكن نحتاج نغير السلوك ده عشان نرجع حاجة معينة مرتبطة بالعنصر ده، سواء كانت الكومبوننت نفسه أو حاجات مرتبطة بيه.
مثال 1: الوصول للعنصر الـ HTML بدل الكومبوننت
لو إحنا عندنا كومبوننت اسمه <book/> في الـ template، ومش عايزين نوصل للكومبوننت نفسه، لكن عايزين نوصل للعنصر الـ HTML اللي بيحتويه.
@Component({
template: `
<div>
<book #book></book>
</div>
`,
})
class BookListComponent {
bookComponent = viewChild("book", {
read: ElementRef,
});
}هنا استخدمنا read: ElementRef مع viewChild. ده بيقول لـ viewChild إنها ترجع ElementRef اللي بيحتوي على العنصر HTML نفسه، مش الـ BookComponent.
مثال 2: الوصول لDirective معينة مرتبطة بالعنصر
ممكن يكون عندنا Directive معينة مضافة للعنصر، زي matTooltip اللي بتضيف tooltip على العنصر.
@Component({
template: `
<div>
<book #book matTooltip="I'm a tooltip!"> </book>
</div>
`,
imports: [MatTooltip],
})
class BookListComponent {
bookComponent = viewChild("book", {
read: MatTooltip,
});
constructor() {
effect(() => {
console.log("Tooltip: ", this.bookComponent()?.message);
});
}
}لو عايزين نوصل لحاجة معينة مرتبطة بالعنصر (زي الـ HTML element نفسه أو Directive معينة)، ممكن نحدد النوع اللي محتاجينه باستخدام read مع viewChild.
بشكل افتراضي، ()viewChild هترجع undefined لو العنصر اللي بنسأل عنه مش موجود في الـ template view.
لكن ممكن نخلي ()viewChild تعمل query تكون "إجبارية"، بمعنى إنها لازم تلاقي حاجة تطابق الـ query في الـ template. لو مفيش حاجة تطابق، هيظهر خطأ.
<div>
<b #title>Title</b>
</div>titleRef = viewChild.required("bold");في المثال ده، الـ query بتدور على عنصر بالـ reference bold، لكن مفيش عنصر بالـ reference ده، وبالتالي هيظهر الخطأ التالي:
ERROR Error: NG0951: Child query result is required but no value is available