Angular من الصفر إلى 100 مثال: المكوّنات والقوالب والإشارات والنماذج والتوجيه وHTTP وكل أنواع الخصائص

Angular من الصفر إلى 100 مثال: المكوّنات والقوالب والإشارات والنماذج والتوجيه وHTTP وكل أنواع الخصائص

Angular إطار عمل من Google لبناء تطبيقات ويب كبيرة ومنظمة باستخدام TypeScript. تبدأ هذه المقالة من إنشاء المشروع وتنتهي بـ 100 مثال عملي. ستتعلم المكوّنات والقوالب، وصيغة التحكم في التدفق، والأنابيب، والمدخلات والمخرجات، وكل نوع من الخصائص التي يمكن للمكوّن أن يحملها، ووظائف دورة الحياة، وحقن التبعيات، والتوجيه، والنماذج التفاعلية ونماذج القالب، وHTTP مع RxJS، والتوجيهات، والإشارات، والاختبارات، والنشر. كل مثال شيفرة قصيرة. تم فحص صياغة شيفرات TypeScript بمحلل TypeScript 5.6، أما القوالب وأوامر البناء فتُعرض كما تُكتب في مشروع Angular 19.

ما الذي تحتاجه

Node.js 20 أو أحدث مع npm، ومحرر شيفرة مثل Visual Studio Code، ومتصفح. ثبّت Angular CLI بالأمر npm install -g @angular/cli. يجب أن تعرف أساسيات HTML وCSS وJavaScript، ومعرفة معنى الصنف والدالة تساعدك على فهم أمثلة TypeScript.

كيفية استخدام المثال

تُوضع أمثلة TypeScript في مشروع Angular مستقل، داخل ملف المكوّن أو الخدمة المذكور. تُنفَّذ أمثلة الأوامر في طرفية المشروع، وأمثلة HTML قوالب توضع في ملف HTML الخاص بالمكوّن.

الموضوع 1: إعداد المشروع وأول مكوّن

تطبيق Angular شجرة من المكوّنات. تنشئ واجهة الأوامر المشروع، وكل مكوّن صنف مع مُزخرِف يحدد المحدد والقالب.

إنشاء المشروع

ينشئ الأمر ng new المشروع بملفات تطبيق مستقل، أي أن المكوّنات لا تحتاج إلى NgModule. يشغّل ng serve خادم تطوير مع إعادة تحميل تلقائية، وينشئ ng generate ملفات مكوّن أو خدمة أو أنبوب جديد.

التشغيل والإعدادات

يستدعي main.ts الدالة bootstrapApplication مع المكوّن الجذري وإعدادات التطبيق. تجمع الإعدادات الـ providers، مثل الموجّه وعميل HTTP، في مكان واحد.

المكوّنات والمحددات والأنماط

يحدد المُزخرِف @Component المحدد، وهو وسم HTML، والقالب، والأنماط. تضيف Angular سمة فريدة لعناصر كل مكوّن، لذلك تنطبق الأنماط عليه وحده، ويمكن لمكوّنين أن يستخدما صنف card دون تعارض.

رسوم توضيحية

كيف يظهر مكوّن Angular على الصفحة: يُترجم القالب والصنف معًا، ويحتفظ Angular بحالة المكوّن، وتحدّث الإشارات واكتشاف التغييرات الأجزاء المتغيرة من العرض فقط
كيف يظهر مكوّن Angular على الصفحة: يُترجم القالب والصنف معًا، ويحتفظ Angular بحالة المكوّن، وتحدّث الإشارات واكتشاف التغييرات الأجزاء المتغيرة من العرض فقط

مثال 1: إنشاء مشروع بواجهة الأوامر

ثبّت Angular CLI وأنشئ تطبيقًا مستقلًا.

npm install -g @angular/cli
ng new blog --standalone --routing --style=css
cd blog
ng serve

مثال 2: إنشاء مكوّن

تنشئ واجهة الأوامر ملفات المكوّن الجديد.

ng generate component hello
# short form
ng g c hello --standalone

مثال 3: تشغيل التطبيق

شغّل المكوّن الجذري مع إعدادات التطبيق.

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

مثال 4: مكوّن مستقل

مكوّن لا يحتاج إلى NgModule.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: '<h1>Hello, Angular!</h1>',
})
export class AppComponent {}

مثال 5: قالب مضمّن متعدد الأسطر

اكتب القالب على عدة أسطر باستخدام علامات الاقتباس العكسية.

import { Component } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <section>
      <h2>Welcome</h2>
      <p>This template spans several lines.</p>
    </section>
  `,
})
export class CardComponent {}

مثال 6: أنماط المكوّن

أنماط تنطبق على هذا المكوّن وحده.

import { Component } from '@angular/core';

@Component({
  selector: 'app-badge',
  standalone: true,
  template: '<span class="badge">New</span>',
  styles: [`.badge { background: #3366ff; color: white; padding: 4px 8px; border-radius: 6px; }`],
})
export class BadgeComponent {}

مثال 7: إعدادات التطبيق مع providers

سجّل الموجّه والخدمات الأخرى في مكان واحد.

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)],
};

مثال 8: العنصر الجذري في index.html

يرسم Angular التطبيق داخل app-root.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Blog</title>
  <base href="/">
</head>
<body>
  <app-root></app-root>
</body>
</html>

الموضوع 2: القوالب وربط البيانات

القالب هو HTML مع صيغة إضافية. يعرض القيم من المكوّن، ويرسل إجراءات المستخدم إليه، ويُبقي المدخلات والقيم متزامنة.

الاستيفاء والتعبيرات

تعرض الأقواس المعقوفة المزدوجة قيمة داخل النص، ويمكن للتعبير أن يحسب، أو يستخدم الشرط الثلاثي، أو يستخدم عامل الوصول الآمن ?. فلا يسبب الكائن غير الموجود خطأً.

ربط الخصائص والسمات والأصناف

تربط الأقواس المربعة خاصية DOM مثل [disabled]. ويحتاج ربط السمات بـ attr. إلى سمات HTML التي لا تقابلها خاصية، مثل تسميات aria. وتضيف روابط الأصناف والأنماط أصناف CSS أو أنماطًا مضمّنة حسب القيمة.

ربط الأحداث والربط ثنائي الاتجاه

تستمع الأقواس المستديرة إلى الأحداث، مثل (click). ويُبقي الربط ثنائي الاتجاه [(ngModel)] حقل النموذج وقيمة المكوّن متساويين، ويحتاج إلى FormsModule. ويسمّي متغير المرجع في القالب، مثل #box، العنصر ليستخدمه باقي القالب.

مثال 9: الاستيفاء

اعرض قيمة داخل النص باستخدام الأقواس المعقوفة المزدوجة.

import { Component } from '@angular/core';

@Component({
  selector: 'app-greeting',
  standalone: true,
  template: '<p>Hello, {{ name }}! You have {{ count + 1 }} message(s).</p>',
})
export class GreetingComponent {
  name = 'Ada';
  count = 2;
}

مثال 10: تعبيرات القالب

احسب القيم واستخدم الشرط الثلاثي والوصول الآمن داخل القالب.

<p>{{ price * quantity }}</p>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please sign in' }}</p>
<p>{{ user?.name ?? 'Guest' }}</p>

مثال 11: ربط الخصائص

اضبط خاصية عنصر من قيمة في المكوّن.

<img [src]="photoUrl" [alt]="photoAlt" width="120">
<button [disabled]="saving">Save</button>

مثال 12: ربط السمات

اضبط سمة HTML لا يوجد لها خاصية مطابقة.

<button [attr.aria-label]="closeLabel">X</button>
<td [attr.colspan]="columns">Total</td>

مثال 13: ربط الأحداث

نفّذ دالة في المكوّن عندما ينقر المستخدم.

import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Clicked {{ clicks }} times</p>
    <button (click)="increment()">Click me</button>
  `,
})
export class CounterComponent {
  clicks = 0;
  increment() {
    this.clicks++;
  }
}

مثال 14: الربط ثنائي الاتجاه مع ngModel

أبقِ حقل الإدخال وقيمة المكوّن متطابقين.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-name-field',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="name">
    <p>You typed: {{ name }}</p>
  `,
})
export class NameFieldComponent {
  name = '';
}

مثال 15: ربط الأصناف

أضف أصناف CSS أو احذفها حسب قيمة.

<div [class.active]="isActive" [class.error]="hasError">Panel</div>
<div [ngClass]="{ 'big': size === 'lg', 'hidden': !visible }">Box</div>

مثال 16: ربط الأنماط

اضبط الأنماط المضمّنة من قيمة في المكوّن.

<p [style.color]="textColor" [style.font-size.px]="fontSize">Styled text</p>

مثال 17: متغير مرجعي في القالب

سمِّ العنصر ليستخدمه جزء آخر من القالب.

<input #nameBox type="text">
<button (click)="greet(nameBox.value)">Greet</button>

الموضوع 3: التحكم في التدفق: @if و@for و@defer و@switch

منذ Angular 17 يتضمن القالب كتلًا مدمجة للشروط والتكرار. وهي تحل محل التوجيهات الهيكلية القديمة، وتسهل القراءة.

@if و@else

تعرض @if الكتلة فقط عندما يكون شرطها صحيحًا. وتختار @else if و@else واحدة من عدة كتل. ويمكن تداخل الكتل، فيمكن مثلًا وضع if داخل حلقة for.

@for مع track و@empty

تكرر @for الكتلة لكل عنصر. ويخبر track الإطار بأن هذا العنصر هو ذاك، فيُعاد رسم الصفوف المتغيرة فقط. وتعرض @empty رسالة عندما تكون القائمة فارغة. وtrack إلزامي.

@switch و@defer

تختار @switch الكتلة حسب قيمة التعبير، ويُستخدم @default احتياطيًا. تحمّل @defer جزءًا من الصفحة لاحقًا، مثلًا عند ظهوره أو عند تفاعل المستخدم معه، وتعرض @placeholder أو @loading حتى ذلك الوقت.

مثال 18: @if

اعرض الكتلة فقط عندما يكون الشرط صحيحًا.

@if (items.length > 0) {
  <p>You have {{ items.length }} items.</p>
}

مثال 19: @if مع @else

اختر كتلة واحدة من عدة كتل.

@if (isLoggedIn) {
  <p>Welcome back!</p>
} @else if (isGuest) {
  <p>Browsing as a guest.</p>
} @else {
  <p>Please sign in.</p>
}

مثال 20: @for مع track

كرر الكتلة لكل عنصر، ويحدد track هوية ثابتة للعنصر.

<ul>
  @for (fruit of fruits; track fruit.id) {
    <li>{{ fruit.name }}</li>
  }
</ul>

مثال 21: @for مع @empty

اعرض رسالة عندما تكون القائمة فارغة.

@for (post of posts; track post.id) {
  <article>{{ post.title }}</article>
} @empty {
  <p>No posts yet.</p>
}

مثال 22: @switch

اختر كتلة حسب قيمة التعبير.

@switch (status) {
  @case ('draft') { <span>Draft</span> }
  @case ('published') { <span>Live</span> }
  @default { <span>Unknown</span> }
}

مثال 23: @defer مع placeholder

حمّل جزءًا من الصفحة لاحقًا، واعرض عنصرًا مؤقتًا أولًا.

@defer (on viewport) {
  <app-heavy-chart />
} @placeholder {
  <p>Chart will load when visible.</p>
} @loading {
  <p>Loading chart...</p>
}

مثال 24: @let لقيمة محلية

سمِّ قيمة مرة واحدة واستخدمها عدة مرات في القالب.

@let total = price * quantity;
<p>Total: {{ total }}</p>

الموضوع 4: الأنابيب

يحوّل الأنبوب قيمة داخل القالب، مثل تنسيق التاريخ أو المال. ولا تتغير القيمة في المكوّن.

أنابيب التنسيق المدمجة

تنسّق date وcurrency وnumber وuppercase وlowercase وtitlecase وslice القيم داخل القالب. وتُمرَّر المعاملات بعد النقطتين، مثل date: 'longDate' أو currency: 'USD'.

json وkeyvalue وasync

يعرض json الكائن نصًا، وهذا مفيد عند التصحيح. ويمرّ keyvalue على مفاتيح الكائن وقيمه. ويشترك async في Observable ويعرض أحدث قيمة، ويلغي الاشتراك تلقائيًا عند تدمير المكوّن.

الأنابيب المخصصة

الصنف الذي يحمل المُزخرِف @Pipe وينفّذ transform هو أنبوب جديد. اكتب أنبوبًا عندما تنسّق نوعًا واحدًا من القيم في أماكن كثيرة، واجعله خاليًا من الآثار الجانبية حتى تستطيع Angular إعادة استخدام نتيجته.

مثال 25: أنبوب التاريخ

نسّق التاريخ للعرض.

<p>{{ today | date: 'longDate' }}</p>
<p>{{ today | date: 'shortTime' }}</p>

مثال 26: أنابيب العملة والأرقام

نسّق المبالغ المالية والأعداد العشرية.

<p>{{ 1234.5 | currency: 'USD' }}</p>
<p>{{ 3.14159 | number: '1.2-2' }}</p>

مثال 27: أنابيب النصوص

غيّر حالة الأحرف أو اقتطع جزءًا من النص داخل القالب.

<p>{{ 'angular' | uppercase }}</p>
<p>{{ 'HELLO' | lowercase }}</p>
<p>{{ 'hello world' | titlecase }}</p>
<p>{{ 'Angular' | slice: 0 : 3 }}</p>

مثال 28: أنبوبا json وkeyvalue

اعرض كائنًا لأغراض التصحيح، أو مرّ على مفاتيحه وقيمه.

<pre>{{ user | json }}</pre>
<ul>
  @for (item of settings | keyvalue; track item.key) {
    <li>{{ item.key }}: {{ item.value }}</li>
  }
</ul>

مثال 29: أنبوب async

اشترك في Observable واعرض أحدث قيمة.

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable, of } from 'rxjs';

@Component({
  selector: 'app-clock',
  standalone: true,
  imports: [AsyncPipe],
  template: '<p>Value: {{ value$ | async }}</p>',
})
export class ClockComponent {
  value$: Observable<number> = of(42);
}

مثال 30: أنبوب مخصص

اكتب أنبوبًا خاصًا بك يختصر النص.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'shorten', standalone: true })
export class ShortenPipe implements PipeTransform {
  transform(value: string, max = 20): string {
    return value.length > max ? value.slice(0, max) + '...' : value;
  }
}

الموضوع 5: تواصل المكوّنات مع بعضها

تتدفق البيانات من الأب إلى الابن عبر المدخلات، وتصعد الأحداث عبر المخرجات. ويمكن للمكوّنات التي ليست أبًا وابنًا أن تتشارك خدمة بدلًا من ذلك.

المدخلات

تُعلن input() عن قيمة يضبطها الأب. أعطها قيمة افتراضية، واستخدم input.required عندما يجب أن يضبطها الأب، وأضف alias عندما يجب أن يختلف اسم السمة. ما زال المُزخرِف القديم @Input يعمل.

المخرجات و@Output

تُعلن output() عن حدث يُطلقه الابن بالدالة emit(). يستمع الأب بربط الأحداث، مثل (liked)='save()'. ويعمل المُزخرِف القديم @Output مع EventEmitter بالطريقة نفسها.

الإسقاط والاستعلامات والخدمات المشتركة

يتيح ng-content للأب أن يضع محتواه داخل الابن. ويجد viewChild وcontentChild العناصر أو القوالب بالاسم. وتتيح الخدمة المشتركة التي تحمل Subject أو إشارة للمكوّنات البعيدة أن تتواصل.

مثال 31: input() كإشارة

استقبل قيمة من المكوّن الأب مع قيمة افتراضية.

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: '<p>Hello, {{ name() }}</p>',
})
export class UserCardComponent {
  name = input<string>('Guest');
}

مثال 32: input إلزامي

أجبر المكوّن الأب على توفير القيمة.

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-price',
  standalone: true,
  template: '<p>{{ amount() }} THB</p>',
})
export class PriceComponent {
  amount = input.required<number>();
}

مثال 33: حدث output()

أرسل حدثًا من المكوّن الابن إلى الأب.

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-like-button',
  standalone: true,
  template: '<button (click)="liked.emit()">Like</button>',
})
export class LikeButtonComponent {
  liked = output<void>();
}

مثال 34: EventEmitter مع @Output

الطريقة القديمة لإصدار الأحداث من المكوّن الابن.

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: '<input (keyup.enter)="submit($event)">',
})
export class SearchBoxComponent {
  @Output() searched = new EventEmitter<string>();

  submit(event: Event) {
    this.searched.emit((event.target as HTMLInputElement).value);
  }
}

مثال 35: استعلام viewChild كإشارة

احصل على مرجع لعنصر داخل قالب المكوّن.

import { Component, ElementRef, viewChild } from '@angular/core';

@Component({
  selector: 'app-focus',
  standalone: true,
  template: '<input #box type="text">',
})
export class FocusComponent {
  box = viewChild<ElementRef<HTMLInputElement>>('box');

  focusBox() {
    this.box()?.nativeElement.focus();
  }
}

مثال 36: إسقاط المحتوى بـ ng-content

اسمح للمكوّن الأب بوضع محتواه داخل المكوّن الابن.

import { Component } from '@angular/core';

@Component({
  selector: 'app-panel',
  standalone: true,
  template: `
    <section class="panel">
      <ng-content></ng-content>
    </section>
  `,
})
export class PanelComponent {}

مثال 37: contentChild وng-template

استقبل قالبًا من الأب واعرضه لكل عنصر.

import { Component, contentChild, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-list',
  standalone: true,
  imports: [],
  template: `
    @for (item of items; track item) {
      <ng-container [ngTemplateOutlet]="itemTpl()" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
    }
  `,
})
export class ListComponent {
  items = ['a', 'b'];
  itemTpl = contentChild<TemplateRef<unknown>>('itemTpl');
}

مثال 38: host binding وhost listener

اربط العنصر المضيف واستمع إلى أحداثه.

import { Component, HostBinding, HostListener } from '@angular/core';

@Component({
  selector: 'app-hover-card',
  standalone: true,
  template: '<p>Hover me</p>',
})
export class HoverCardComponent {
  @HostBinding('class.highlighted') highlighted = false;

  @HostListener('mouseenter') onEnter() {
    this.highlighted = true;
  }

  @HostListener('mouseleave') onLeave() {
    this.highlighted = false;
  }
}

مثال 39: بيانات host الوصفية

اضبط أصناف العنصر المضيف وسماته في المُزخرِف.

import { Component } from '@angular/core';

@Component({
  selector: 'app-tag',
  standalone: true,
  template: '<ng-content></ng-content>',
  host: {
    'role': 'note',
    '[class.small]': 'small',
  },
})
export class TagComponent {
  small = true;
}

مثال 40: خدمة مشتركة للرسائل

أرسل رسائل بين مكوّنات ليست أبًا وابنًا.

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MessageBus {
  private messages = new Subject<string>();
  messages$ = this.messages.asObservable();

  send(text: string) {
    this.messages.next(text);
  }
}

الموضوع 6: كل أنواع الخصائص

للمكوّن عدة أنواع من الخصائص. ولكل نوع حاجة مختلفة: قيم عادية، وقيم للقراءة فقط، وقيم محسوبة، وإشارات تفاعلية، وقيم ثنائية الاتجاه.

الخصائص العادية وreadonly والاختيارية

الخاصية العادية حقل له قيمة ويمكن تغييرها. وتمنع readonly إعادة الإسناد بعد ضبط الحقل. وعلامة الاستفهام على الخاصية، مثل email?، تعني أن القيمة قد تغيب، لذلك يجب أن يعالج الكود القيمة undefined.

الـ getter والـ setter والخصائص الثابتة

يُنفَّذ الـ getter عند قراءة الخاصية، ويُنفَّذ الـ setter عند الكتابة. تنتمي الخاصية static إلى الصنف نفسه لا إلى كل نسخة، وهي مناسبة لقيمة افتراضية مشتركة مثل اسم السمة.

الإشارات: signal وcomputed وeffect

تحتفظ signal() بقيمة، وتخبر Angular عند تغيّرها، وتُقرأ كدالة مثل count(). وتشتق computed() قيمة من الإشارات، وتتحدث عند تغيّرها. وتُنفّذ effect() الشيفرة كلما تغيّرت الإشارات التي تقرؤها.

model() واسم المدخل المستعار وinput.required

تمثل model() مدخلًا ثنائي الاتجاه: يستطيع الابن ضبطها، فتتبعه قيمة الأب، كما يفعل ngModel في المكوّنات المخصصة. ويعطي alias المدخل اسم سمة مختلفًا. ويُلزم input.required الأب بتوفير القيمة.

الخصائص ذات الدوال السهمية

تحتفظ الخاصية التي تحمل دالة سهمية بقيمة this للمكوّن عند تمرير الدالة كـ callback، لأن الدالة السهمية تلتقط this وقت إنشائها.

مثال 41: خاصية صنف بقيمة ابتدائية

خاصية عادية لها قيمة ابتدائية.

import { Component } from '@angular/core';

@Component({
  selector: 'app-profile',
  standalone: true,
  template: '<p>{{ username }} ({{ role }})</p>',
})
export class ProfileComponent {
  username = 'mali';
  role: string = 'editor';
}

مثال 42: خاصية readonly

خاصية يمكن ضبطها مرة واحدة فقط عند التصريح.

import { Component } from '@angular/core';

@Component({
  selector: 'app-version',
  standalone: true,
  template: '<p>Version {{ version }}</p>',
})
export class VersionComponent {
  readonly version = '1.4.0';
}

مثال 43: خاصية getter وsetter

خاصية تنفّذ شيفرة عند القراءة أو الكتابة.

import { Component } from '@angular/core';

@Component({
  selector: 'app-temperature',
  standalone: true,
  template: '<p>{{ fahrenheit }} F</p>',
})
export class TemperatureComponent {
  private celsius = 0;

  get fahrenheit(): number {
    return this.celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

مثال 44: خاصية إشارة مع set وupdate

قيمة تفاعلية تغيّرها بـ set أو update.

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter-signal',
  standalone: true,
  template: `
    <p>{{ count() }}</p>
    <button (click)="count.update(n => n + 1)">+1</button>
    <button (click)="count.set(0)">Reset</button>
  `,
})
export class CounterSignalComponent {
  count = signal(0);
}

مثال 45: خاصية computed

قيمة مشتقة من الإشارات، وتتحدّث وحدها.

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-cart',
  standalone: true,
  template: '<p>Total: {{ total() }}</p>',
})
export class CartComponent {
  prices = signal([120, 80, 50]);
  total = computed(() => this.prices().reduce((sum, p) => sum + p, 0));
}

مثال 46: effect

نفّذ الشيفرة كلما تغيّرت الإشارات التي تقرؤها.

import { Component, effect, signal } from '@angular/core';

@Component({
  selector: 'app-theme',
  standalone: true,
  template: '<p>Theme: {{ theme() }}</p>',
})
export class ThemeComponent {
  theme = signal('light');

  constructor() {
    effect(() => {
      console.log('Theme changed to', this.theme());
    });
  }
}

مثال 47: model() للربط ثنائي الاتجاه

مدخل يستطيع المكوّن الابن تحديثه أيضًا، مثل ngModel.

import { Component, model } from '@angular/core';

@Component({
  selector: 'app-toggle',
  standalone: true,
  template: '<button (click)="on.set(!on())">{{ on() ? "On" : "Off" }}</button>',
})
export class ToggleComponent {
  on = model(false);
}

مثال 48: input باسم مستعار

استخدم اسم سمة مختلفًا في القالب.

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-avatar',
  standalone: true,
  template: '<img [alt]="label()">',
})
export class AvatarComponent {
  label = input('Profile picture', { alias: 'caption' });
}

مثال 49: خاصية اختيارية

خاصية قد لا تكون لها قيمة، وتُعلَّم بعلامة استفهام.

interface Customer {
  id: number;
  name: string;
  email?: string;
}

const shopper: Customer = { id: 1, name: 'Ploy' };
console.log(shopper.email ?? 'no email');

مثال 50: خاصية static وخاصية الدالة السهمية

قيمة مشتركة في الصنف كله، ودالة تحتفظ بقيمة this.

import { Component } from '@angular/core';

@Component({
  selector: 'app-config',
  standalone: true,
  template: '<p>{{ appName }}</p>',
})
export class ConfigComponent {
  static readonly defaultTheme = 'light';
  appName = 'Blog';
  logName = () => console.log(this.appName);
}

الموضوع 7: وظائف دورة الحياة

يستدعي Angular دوالًا خاصة في نقاط ثابتة من حياة المكوّن: عند الإنشاء، وعند تغيّر المدخلات، وبعد رسم العرض، وقبل الإزالة.

ngOnInit وngOnChanges

يعمل ngOnInit مرة واحدة بعد أن يضبط Angular المدخلات الأولى، لذلك هو المكان المناسب لتحميل البيانات. ويعمل ngOnChanges في كل مرة يتغير فيها مدخل، ويستقبل القيم القديمة والجديدة في SimpleChanges.

ngOnDestroy

يعمل ngOnDestroy قبل إزالة المكوّن مباشرة. ألغِ الاشتراك في التدفقات الطويلة وأوقف المؤقتات هناك، لمنع تسرب الذاكرة.

ngAfterViewInit وngDoCheck

يعمل ngAfterViewInit بعد رسم عرض المكوّن وعروض أبنائه، فتصبح عناصر العرض متاحة. ويعمل ngDoCheck في كل دورة لاكتشاف التغييرات، لذلك اجعله خفيفًا.

مثال 51: ngOnInit

شغّل شيفرة الإعداد مرة واحدة بعد أن يضبط Angular المدخلات.

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-todo-list',
  standalone: true,
  template: '<p>{{ items.length }} tasks loaded</p>',
})
export class TodoListComponent implements OnInit {
  items: string[] = [];

  ngOnInit() {
    this.items = ['Write', 'Test', 'Ship'];
  }
}

مثال 52: ngOnChanges

استجب عندما يتغير مدخل قادم من المكوّن الأب.

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-badge-count',
  standalone: true,
  template: '<span>{{ count }}</span>',
})
export class BadgeCountComponent implements OnChanges {
  @Input() count = 0;

  ngOnChanges(changes: SimpleChanges) {
    if (changes['count']) {
      console.log('Count changed to', changes['count'].currentValue);
    }
  }
}

مثال 53: ngOnDestroy

أغلق الاشتراكات والمؤقتات عند إزالة المكوّن.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subscription } from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: '<p>Ticks: {{ ticks }}</p>',
})
export class TimerComponent implements OnInit, OnDestroy {
  ticks = 0;
  private sub?: Subscription;

  ngOnInit() {
    this.sub = interval(1000).subscribe(() => this.ticks++);
  }

  ngOnDestroy() {
    this.sub?.unsubscribe();
  }
}

مثال 54: ngAfterViewInit

استخدم عناصر العرض بعد رسم القالب.

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';

@Component({
  selector: 'app-autofocus',
  standalone: true,
  template: '<input #field>',
})
export class AutofocusComponent implements AfterViewInit {
  @ViewChild('field') field!: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    this.field.nativeElement.focus();
  }
}

مثال 55: ngDoCheck

نفّذ فحصًا في كل دورة لاكتشاف التغييرات.

import { Component, DoCheck } from '@angular/core';

@Component({
  selector: 'app-watcher',
  standalone: true,
  template: '<p>Checks run: {{ checks }}</p>',
})
export class WatcherComponent implements DoCheck {
  checks = 0;

  ngDoCheck() {
    this.checks++;
  }
}

الموضوع 8: حقن التبعيات والخدمات

تحتفظ الخدمات بالمنطق والبيانات التي يحتاجها عدة مكوّنات. ينشئها Angular ويمررها عبر حقن التبعيات، فلا يحتاج المكوّن إلى إنشاء نسخته الخاصة.

@Injectable وprovidedIn root

يُنشأ الصنف الذي يحمل @Injectable({ providedIn: 'root' }) مرة واحدة لكامل التطبيق. ويحصل عليه المكوّن بالدالة inject() أو بمعامل في المُنشئ. استخدم إشارة داخل الخدمة لتخزين حالة يستطيع القالب قراءتها.

InjectionToken والـ providers بالقيم

يحدد الرمز المميّز قيمة ليست صنفًا، مثل كائن إعدادات. وتسجّله بموفّر: useValue لقيمة ثابتة، وuseClass لصنف، وuseFactory لبناء القيمة.

الـ providers على مستوى المكوّن

تمنح مصفوفة providers على المكوّن ذلك المكوّن وأبناءه نسخة خاصة من الخدمة. وهذا مفيد للحالة التي يجب أن تُعاد تهيئتها كلما أُنشئ المكوّن.

مثال 56: الدالة inject()

احصل على خدمة دون معامل في المُنشئ.

import { Component, inject } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-items',
  standalone: true,
  template: '<p>{{ data.count() }} items</p>',
})
export class ItemsComponent {
  data = inject(DataService);
}

مثال 57: خدمة قابلة للحقن

صنف مشترك يمكن لأي مكوّن أن يستقبله.

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartService {
  private itemsSignal = signal<string[]>([]);
  readonly items = this.itemsSignal.asReadonly();

  add(item: string) {
    this.itemsSignal.update((list) => [...list, item]);
  }
}

مثال 58: InjectionToken

وفّر قيمة ليست صنفًا، مثل الإعدادات.

import { InjectionToken } from '@angular/core';

export interface AppSettings {
  apiUrl: string;
  pageSize: number;
}

export const APP_SETTINGS = new InjectionToken<AppSettings>('app.settings');

مثال 59: provider مع useValue

أعطِ الحاقن كائنًا ثابتًا.

import { ApplicationConfig } from '@angular/core';
import { APP_SETTINGS } from './app-settings.token';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_SETTINGS,
      useValue: { apiUrl: 'https://api.example.com', pageSize: 20 },
    },
  ],
};

مثال 60: useClass وuseFactory

اختر صنفًا أو أنشئ قيمة عند الحاجة إليها.

import { ApplicationConfig, inject } from '@angular/core';
import { LoggerService } from './logger.service';
import { ConsoleLogger } from './console-logger';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: LoggerService, useClass: ConsoleLogger },
    {
      provide: 'clock',
      useFactory: () => () => new Date().getFullYear(),
    },
  ],
};

مثال 61: providers على مستوى المكوّن

امنح الخدمة نسخة خاصة بشجرة مكوّنات واحدة.

import { Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';

@Component({
  selector: 'app-scoped-counter',
  standalone: true,
  template: '<p>{{ store.value() }}</p>',
  providers: [CounterStore],
})
export class ScopedCounterComponent {
  store = inject(CounterStore);
}

الموضوع 9: التوجيه

يربط الموجّه مسارات الروابط بالمكوّنات، ويعرض الصفحة المطابقة في router-outlet، ويستطيع تحميل الصفحات عند الحاجة فقط.

المسارات وروابط التنقل

تربط مصفوفة Routes مسارات مثل '' و'about' بالمكوّنات. ينشئ routerLink رابطًا دون إعادة تحميل الصفحة، ويضيف routerLinkActive صنفًا إلى رابط الصفحة الحالية. ويعالج المسار ** الروابط غير المعروفة.

المعاملات والتحميل الكسول

يمرر مسار مثل 'post/:id' قيمة المعرّف، وتقرؤها من ActivatedRoute. ويحمّل loadComponent مع الاستيراد الديناميكي شيفرة الصفحة فقط عندما يفتحها المستخدم، فيبقى التنزيل الأول صغيرًا.

الحراس والمحلّلات والتنقل في الشيفرة

يعيد الحارس CanActivate القيمة true أو UrlTree لإعادة التوجيه. ويحمّل المحلّل البيانات قبل فتح الصفحة. وينقل Router.navigate المستخدم من الشيفرة، ويستطيع تمرير معاملات الاستعلام.

مثال 62: المسارات

اربط مسارات الروابط بالمكوّنات.

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: '**', redirectTo: '' },
];

مثال 63: routerLink وrouter-outlet

اربط الصفحات ببعضها، واعرض الصفحة المطابقة.

<nav>
  <a routerLink="/" routerLinkActive="active">Home</a>
  <a routerLink="/about" routerLinkActive="active">About</a>
</nav>
<router-outlet></router-outlet>

مثال 64: معاملات المسار

اقرأ قيمة مثل المعرّف من الرابط.

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-post',
  standalone: true,
  template: '<p>Post id: {{ id }}</p>',
})
export class PostComponent {
  private route = inject(ActivatedRoute);
  id = this.route.snapshot.paramMap.get('id');
}

مثال 65: مسار محمّل عند الطلب

حمّل شيفرة الصفحة فقط عندما يفتحها المستخدم.

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () =>
      import('./admin/admin.component').then((m) => m.AdminComponent),
  },
];

مثال 66: حارس CanActivate

اسمح بالمسار أو امنعه بناءً على فحص.

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  return auth.isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

مثال 67: المُحلِّل Resolver

حمّل البيانات قبل فتح الصفحة.

import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { PostService } from './post.service';

export const postResolver: ResolveFn<string> = (route) => {
  return inject(PostService).titleFor(route.paramMap.get('id') ?? '');
};

مثال 68: التنقل البرمجي

انتقل إلى صفحة أخرى من الشيفرة، مع معاملات الاستعلام.

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: '<button (click)="finish()">Finish</button>',
})
export class CheckoutComponent {
  private router = inject(Router);

  finish() {
    this.router.navigate(['/thanks'], { queryParams: { ref: 'cart' } });
  }
}

الموضوع 10: النماذج

لدى Angular أسلوبان للنماذج. تبني النماذج التفاعلية النموذج في الشيفرة، وهي الخيار المعتاد للنماذج الكبيرة. وتستخدم نماذج القالب ngModel في HTML، وتناسب النماذج الصغيرة.

FormControl وFormGroup وFormBuilder

يحتفظ FormControl بقيمة واحدة، ويجمع FormGroup عدة عناصر تحكم. يقلل FormBuilder كمية الشيفرة، وتُبقي nonNullable القيمة نصًا بدلًا من null بعد إعادة الضبط.

المدقّقات ورسائل الأخطاء

المدقّقات required وminLength وmin وmax وemail مدمجة. اعرض الخطأ فقط عندما يكون عنصر التحكم غير صالح وقد لمسه المستخدم، حتى لا يرى المستخدم الأخطاء قبل الكتابة. والمدقّق المخصص دالة تعيد كائن الخطأ أو null.

FormArray وvalueChanges

FormArray قائمة من عناصر التحكم يمكن إضافة عناصر إليها أو حذفها منها. وvalueChanges هو Observable يُصدر قيمة جديدة في كل مرة تتغير فيها القيمة، وهو مفيد للبحث الحي أو حفظ المسودات.

مثال 69: FormControl تفاعلي

حقل نموذج تُحفظ قيمته في الشيفرة.

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-email-input',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: '<input [formControl]="email">',
})
export class EmailInputComponent {
  email = new FormControl('', { nonNullable: true });
}

مثال 70: FormGroup مع FormBuilder

اجمع عدة حقول في نموذج تفاعلي واحد.

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-signup',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: '<form [formGroup]="form"></form>',
})
export class SignupComponent {
  private fb = inject(FormBuilder);
  form = this.fb.group({
    name: [''],
    email: [''],
  });
}

مثال 71: المُدقِّقات المدمجة

فحوص required وminLength وmin وmax وemail.

import { FormBuilder, Validators } from '@angular/forms';
import { inject } from '@angular/core';

const fb = inject(FormBuilder);
const form = fb.nonNullable.group({
  name: ['', [Validators.required, Validators.minLength(2)]],
  age: [18, [Validators.min(13), Validators.max(120)]],
  email: ['', Validators.email],
});

مثال 72: قالب النموذج التفاعلي

اربط FormGroup بـ HTML واعرض الأخطاء.

<form [formGroup]="form" (ngSubmit)="save()">
  <input formControlName="name" placeholder="Name">
  @if (form.controls.name.invalid && form.controls.name.touched) {
    <p class="error">Name is required.</p>
  }
  <button [disabled]="form.invalid">Save</button>
</form>

مثال 73: FormArray

قائمة من عناصر التحكم في النموذج تكبر وتصغر.

import { FormArray, FormControl } from '@angular/forms';

const tags = new FormArray([
  new FormControl('angular'),
  new FormControl('typescript'),
]);

tags.push(new FormControl('rxjs'));
console.log(tags.length);

مثال 74: مُدقِّق مخصص

اكتب قاعدتك الخاصة، مثل منع المسافات.

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function noSpaces(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = String(control.value ?? '');
    return value.includes(' ') ? { noSpaces: true } : null;
  };
}

مثال 75: نموذج مدفوع بالقالب

أنشئ النموذج باستخدام ngModel داخل القالب مباشرة.

<form #form="ngForm" (ngSubmit)="submit(form.value)">
  <input name="title" ngModel required>
  <button [disabled]="form.invalid">Post</button>
</form>

مثال 76: valueChanges

استجب في كل مرة تتغير فيها قيمة النموذج.

import { FormControl } from '@angular/forms';

const search = new FormControl('');

search.valueChanges.subscribe((text) => {
  console.log('Searching for', text);
});

الموضوع 11: HTTP وRxJS والبيانات غير المتزامنة

يرسل HttpClient الطلبات ويعيد Observable. تحوّل عوامل RxJS هذه التدفقات، ويعرض async النتائج دون اشتراكات يدوية.

إعداد HttpClient

استدعِ provideHttpClient() في إعدادات التطبيق. تعيد الدالتان get وpost في HttpClient Observable لا يعمل حتى يشترك أحد فيه، لذلك استخدم أنبوب async أو toSignal في القالب.

العوامل map وfilter وswitchMap

يغيّر map كل قيمة، ويُبقي filter القيم التي تريدها فقط. ويلغي switchMap الطلب السابق عند وصول قيمة جديدة، وهذا مناسب للبحث أثناء كتابة المستخدم. أضف debounceTime لانتظار توقف الكتابة.

الأخطاء وإعادة المحاولة

يعالج catchError الطلب الفاشل ويستطيع إعادة قيمة آمنة. ويعيد retry المحاولة قبل أن يفشل الطلب نهائيًا. قرّر دائمًا ما الذي سيراه المستخدم عند الخطأ، لأن الشاشة الفارغة تربك المستخدم.

مثال 77: provideHttpClient

فعّل HttpClient في إعدادات التطبيق.

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()],
};

مثال 78: جلب البيانات مع async

اعرض قائمة من HTTP دون اشتراكات يدوية.

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-users',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @for (user of users$ | async; track user.id) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UsersComponent {
  private http = inject(HttpClient);
  users$: Observable<{ id: number; name: string }[]> =
    this.http.get<{ id: number; name: string }[]>('/api/users');
}

مثال 79: map وfilter من RxJS

حوّل القيم من المجرى واحتفظ بما تحتاجه فقط.

import { map, filter } from 'rxjs';

const prices$ = of(120, 35, 80);
prices$
  .pipe(
    filter((p) => p > 50),
    map((p) => p * 1.07),
  )
  .subscribe((total) => console.log(total.toFixed(2)));

مثال 80: switchMap للبحث الحي

انتظر توقف الكتابة، ثم ألغِ الطلب القديم.

import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';

@Component({
  selector: 'app-live-search',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: '<input [formControl]="query">',
})
export class LiveSearchComponent {
  private http = inject(HttpClient);
  query = new FormControl('', { nonNullable: true });

  results$ = this.query.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((q) => this.http.get<string[]>('/api/search', { params: { q } })),
  );
}

مثال 81: catchError وretry

أعد محاولة الطلب الفاشل، ثم استخدم قيمة احتياطية آمنة.

import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, of, retry } from 'rxjs';

const http = inject(HttpClient);
const safeList$ = http.get<string[]>('/api/tags').pipe(
  retry(2),
  catchError(() => of([] as string[])),
);

مثال 82: طلب HTTP من نوع POST

أرسل البيانات إلى الخادم في جسم الطلب.

import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export class PostService {
  private http = inject(HttpClient);

  createPost(title: string) {
    return this.http.post<{ id: number }>('/api/posts', { title });
  }
}

الموضوع 12: التوجيهات

يغيّر التوجيه طريقة عمل العنصر أو ظهوره. توجيهات السمات تغيّر العنصر، أما التوجيهات الهيكلية فتضيف العنصر أو تحذفه.

توجيهات السمات

يستطيع توجيه له محدد داخل أقواس مربعة، مثل [appHighlight]، أن يستخدم HostListener للاستجابة لأحداث الفأرة وتغيير أنماط العنصر. أبقِ المنطق صغيرًا وضع السلوك المعقد في مكوّن.

التوجيهات الهيكلية وTemplateRef

يحقن التوجيه الهيكلي TemplateRef وViewContainerRef. وعندما يكون الشرط صحيحًا ينشئ العرض بالدالة createEmbeddedView، وعندما يكون خاطئًا يمسح العرض. ويعمل *ngIf و@if في Angular بالطريقة نفسها.

مُعيّنات المدخلات في التوجيهات

يعمل المُعيّن على المدخل في كل مرة يمرر فيها الأب قيمة جديدة. استخدمه للاستجابة للقيمة مباشرة، مثل إعادة بناء العرض، دون كتابة ngOnChanges.

مثال 83: توجيه السمة

توجيه يغيّر مظهر العنصر.

import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  color = input('yellow', { alias: 'appHighlight' });
  private el = inject<ElementRef<HTMLElement>>(ElementRef);

  @HostListener('mouseenter') onEnter() {
    this.el.nativeElement.style.backgroundColor = this.color();
  }

  @HostListener('mouseleave') onLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

مثال 84: التوجيه الهيكلي

توجيه يضيف عناصر إلى الصفحة أو يحذفها.

import { Directive, inject, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]',
  standalone: true,
})
export class UnlessDirective {
  private template = inject<TemplateRef<unknown>>(TemplateRef);
  private view = inject(ViewContainerRef);

  @Input() set appUnless(condition: boolean) {
    this.view.clear();
    if (!condition) {
      this.view.createEmbeddedView(this.template);
    }
  }
}

مثال 85: مُعيّن input

نفّذ شيفرة كلما مرّر الأب قيمة جديدة.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-tag-list',
  standalone: true,
  template: '<p>{{ tags.join(", ") }}</p>',
})
export class TagListComponent {
  private _tags: string[] = [];

  @Input()
  set tags(value: string[]) {
    this._tags = value.map((t) => t.trim());
  }
  get tags(): string[] {
    return this._tags;
  }
}

الموضوع 13: الرسم والإشارات واكتشاف التغييرات

يقرر Angular ما يعيد رسمه عبر اكتشاف التغييرات. تتيح لك الإشارات وOnPush التحكم في ذلك، فتبقى الصفحات الكبيرة سريعة.

القوائم مع الفهارس والقيم المحسوبة

داخل @for يمكنك استخدام $index و$first و$last لتنسيق كل صف. تستطيع الإشارة computed تصفية القائمة، فتتحدث النتيجة المصفاة كلما تغيّر نص البحث أو القائمة.

اكتشاف التغييرات بالنمط OnPush

مع ChangeDetectionStrategy.OnPush يفحص Angular المكوّن فقط عندما يتغير مدخل فيه، أو يقع حدث داخله، أو تتغير إشارة يقرؤها. وهذا يحسّن السرعة كثيرًا في الصفحات التي تحتوي على مكوّنات كثيرة.

بدون Zone وtoSignal

تعمل التطبيقات بدون Zone دون zone.js، فيتحدث العرض بسبب الإشارات والأحداث، لا بسبب كل حدث في المتصفح. ويحوّل toSignal الـ Observable إلى إشارة يمكن قراءتها في القالب دون اشتراك.

مثال 86: @for مع $index و$first

استخدم موضع كل عنصر داخل الحلقة.

@for (step of steps; track step; let i = $index, first = $first, last = $last) {
  <li [class.first]="first" [class.last]="last">{{ i + 1 }}. {{ step }}</li>
}

مثال 87: تصفية بـ computed

اعرض قائمة مصفاة تتحدث أثناء كتابة المستخدم.

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-fruit-filter',
  standalone: true,
  template: `
    <input (input)="query.set($any($event.target).value)">
    @for (fruit of shown(); track fruit) {
      <p>{{ fruit }}</p>
    }
  `,
})
export class FruitFilterComponent {
  fruits = ['apple', 'banana', 'cherry'];
  query = signal('');
  shown = computed(() =>
    this.fruits.filter((f) => f.includes(this.query().toLowerCase())),
  );
}

مثال 88: اكتشاف التغييرات OnPush

افحص المكوّن فقط عندما تتغير مدخلاته.

import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  selector: 'app-price-tag',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: '<p>{{ price }} THB</p>',
})
export class PriceTagComponent {
  @Input() price = 0;
}

مثال 89: اكتشاف التغييرات بدون Zone

شغّل Angular دون zone.js، واستخدم الإشارات لتحديث العرض.

import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [provideZonelessChangeDetection()],
};

مثال 90: toSignal

حوّل Observable إلى إشارة تُقرأ في القالب.

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';

@Component({
  selector: 'app-seconds',
  standalone: true,
  template: '<p>Seconds: {{ seconds() }}</p>',
})
export class SecondsComponent {
  seconds = toSignal(interval(1000).pipe(map((n) => n + 1)), { initialValue: 0 });
}

الموضوع 14: الاختبار والحركات

تفحص الاختبارات المكوّنات والخدمات كلًا على حدة. وتغيّر الحركات العنصر بسلاسة عندما تتغير حالته.

TestBed واختبارات المكوّنات

ينشئ TestBed المكوّن في وحدة اختبار، وينفّذ detectChanges اكتشاف التغييرات، ويفحص الاختبار نص nativeElement. اجعل كل اختبار يتحقق من سلوك واحد، حتى يشير الفشل إلى سبب واحد.

HttpTestingController وfakeAsync

يستبدل provideHttpClientTesting الشبكة الحقيقية بشبكة وهمية، فتعطي expectOne وflush الاختبار استجابة. وتقدّم fakeAsync مع tick الزمن، فيمكن اختبار عملية مؤجلة دون انتظار.

مشغّلات الحركات

يحدد مشغّل الحركة حالات، مثل open وclosed، والانتقالات بينها مع التوقيت. اربط المشغّل بقيمة في القالب، فيشغّل Angular الانتقال عندما تتغير القيمة.

مثال 91: اختبار مكوّن بـ TestBed

أنشئ المكوّن في الاختبار وتحقق من نصه.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({ imports: [CounterComponent] }).compileComponents();
    fixture = TestBed.createComponent(CounterComponent);
  });

  it('starts at zero', () => {
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('0');
  });
});

مثال 92: HttpTestingController

اختبر خدمة HTTP دون خادم حقيقي.

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  it('loads users', () => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    const service = TestBed.inject(UserService);
    const http = TestBed.inject(HttpTestingController);

    service.users().subscribe((list) => expect(list.length).toBe(2));
    http.expectOne('/api/users').flush([{ id: 1 }, { id: 2 }]);
    http.verify();
  });
});

مثال 93: fakeAsync وtick

قدّم الوقت في الاختبار لتتحقق من عملية مؤجلة.

import { fakeAsync, tick } from '@angular/core/testing';

it('runs a delayed action', fakeAsync(() => {
  let done = false;
  setTimeout(() => (done = true), 500);
  tick(500);
  expect(done).toBeTrue();
}));

مثال 94: مشغّلات الحركات

حرّك العنصر عندما تتغير حالته.

import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'app-drawer',
  standalone: true,
  animations: [
    trigger('slide', [
      state('closed', style({ height: '0px' })),
      state('open', style({ height: '200px' })),
      transition('closed <=> open', animate('300ms ease-in')),
    ]),
  ],
  template: '<div [@slide]="isOpen ? \'open\' : \'closed\'">Content</div>',
})
export class DrawerComponent {
  isOpen = false;
}

الموضوع 15: الإعدادات والأخطاء والنشر

تحتاج التطبيقات الحقيقية إلى إعدادات لكل بيئة، وخطة للأخطاء غير المتوقعة، وبناء للإنتاج جاهز للنشر.

ملفات البيئة والمُهيّئات

احفظ الإعدادات مثل عنوان API في ملف البيئة، بقيم مختلفة للتطوير والإنتاج. ينفّذ provideAppInitializer شيفرة، مثل تحميل الإعدادات، قبل رسم الصفحة الأولى.

الأخطاء وErrorHandler

يرسل Angular الأخطاء غير الملتقطة إلى ErrorHandler. وفّر صنفك الخاص لتسجيلها أو إرسالها إلى خدمة تقارير، فيكون هناك مكان واحد يقرر كيف تُعالج الأخطاء غير المتوقعة.

البناء والاختبار والنشر

ينشئ الأمر ng build --configuration production ملفات محسّنة في المجلد dist/. ويشغّل ng test --watch=false اختبارات الوحدة مرة واحدة في خط البناء. استضف الملفات الثابتة على أي خادم ويب أو خدمة استضافة ثابتة تدعم تطبيقات الصفحة الواحدة.

مثال 95: إعدادات البيئة

احتفظ بإعدادات مثل عنوان API في ملف واحد.

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
};

مثال 96: ErrorHandler مخصص

أبلغ عن الأخطاء غير المتوقعة في مكان واحد.

import { ErrorHandler, Injectable } from '@angular/core';

@Injectable()
export class ReportingErrorHandler implements ErrorHandler {
  handleError(error: unknown): void {
    console.error('Unexpected error:', error);
  }
}

مثال 97: provideAppInitializer

حمّل الإعدادات قبل بدء التطبيق.

import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { ConfigService } from './config.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => inject(ConfigService).load()),
  ],
};

مثال 98: الاستيرادات في مكوّن مستقل

اذكر التوجيهات والأنابيب التي يستخدمها المكوّن.

import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
import { HighlightDirective } from './highlight.directive';
import { ShortenPipe } from './shorten.pipe';

@Component({
  selector: 'app-note',
  standalone: true,
  imports: [NgClass, HighlightDirective, ShortenPipe],
  template: '<p [ngClass]="{ bold: true }" appHighlight="pink">{{ text | shorten: 10 }}</p>',
})
export class NoteComponent {
  text = 'A long note about Angular standalone components';
}

مثال 99: @defer عند التفاعل

حمّل جزءًا من الصفحة بعد أن ينقر المستخدم فقط.

<button #openBtn>Show comments</button>

@defer (on interaction(openBtn)) {
  <app-comments />
} @placeholder {
  <p>Comments will load after you click.</p>
}

مثال 100: أوامر البناء والاختبار

أهم أوامر Angular CLI في العمل اليومي.

# development server
ng serve
# production build (outputs to dist/)
ng build --configuration production
# unit tests
ng test --watch=false

مشروع تدريبي: قائمة مهام

ابنِ تطبيقًا صغيرًا واحدًا يستخدم أفكار كل قسم. اكتبه بنفسك أولًا، ثم قارنه بالأمثلة.

1. أنشئ مشروعًا مستقلًا، ثم أنشئ مكوّن TaskList يحتفظ بمصفوفة المهام في إشارة.

2. أضف نموذجًا فيه حقل نص إلزامي، وأضف كل مهمة جديدة إلى القائمة بطريقة مرتبطة بـ (ngSubmit).

3. اعرض المهام باستخدام @for مع track، واعرض رسالة @empty عندما تكون القائمة فارغة.

4. أضف إشارة computed تحسب المهام غير المكتملة، واعرضها في القالب.

5. أضف مسارًا لصفحة تفاصيل المهمة تقرأ المعرّف من الرابط باستخدام ActivatedRoute.

6. اكتب اختبار مكوّن واحدًا يتحقق من رسالة القائمة الفارغة، ثم شغّل ng test --watch=false.

الخطوات التالية

بعد ذلك، تعمّق في الموجّه، وأدِر الحالة بالإشارات أو NgRx، واستخدم Angular SSR للعرض من جهة الخادم، وتعرّف على مكتبات المكوّنات مثل Angular Material. الوثائق الرسمية على angular.dev هي أفضل مرجع. وتتبع الأمثلة في هذه المقالة الصياغة الموصى بها في Angular 17 وما بعدها.

الخلاصة

يبني Angular التطبيقات من المكوّنات. تربط القوالب البيانات والأحداث، وتعرض كتل التحكم في التدفق المحتوى أو تكرره، وتنسّق الأنابيب القيم، وتربط المدخلات والمخرجات الأب بالابن. تحتفظ المكوّنات بحالتها في خصائص عادية وقيم للقراءة فقط والـ getters والإشارات. تُشارَك الخدمات عبر حقن التبعيات، ويربط الموجّه العناوين بالصفحات، وتتحقق النماذج من المدخلات، ويحمّل HttpClient مع RxJS البيانات. ويكتمل سير العمل بالاختبار والإعدادات والبناء للإنتاج. أفضل طريقة لتذكر الأمثلة المئة كلها أن تبني مشروع قائمة المهام، وتغيّر شيئًا واحدًا في كل مرة.