| معيار المقارنة البرمجية | الربط ثنائي الاتجاه التقليدي (@Input و @Output) | الربط الحديث المعتمد على الإشارات (model API) |
|---|---|---|
| طريقة التعريف في صنف المكون | @Input() count: number; و @Output() countChange = new EventEmitter<number>(); | count = model<number>(0); |
| تحديث القيمة وإرسالها للأب | this.count = 5; this.countChange.emit(5); | this.count.set(5); (تقوم بتحديث القيمة وإرسالها تلقائياً) |
| طبيعة المتغير داخل المكون | متغير عادي يحتاج لإدارة يدوية للتحديثات | إشارة قابلة للكتابة (WritableSignal) تتكامل مع تأثيرات الإشارات الأخرى |
| حجم الكود وسهولة الصيانة | يتطلب كتابة مزدوجة وإدارة دقيقة لأسماء المخرجات | كود مختصر، آمن كلياً من حيث الأنواع (Type-safe)، وتحديث تلقائي |
نصل في هذا الدرس الثامن من الفصل السادس إلى جوهرة التاج في الربط المتقدم للبيانات في إصدار Angular 22.1، ألا وهي تمرير المراجع والبيانات باستخدام model في الإشارات. تعتبر دالة model() البديل الحديث والذكي لنمط الربط ثنائي الاتجاه (Two-Way Data Binding) القديم الذي كان يتطلب استخدام @Input مع @Output بشكل مزدوج. باستخدام الـ model، تحصل على إشارة قابلة للكتابة (WritableSignal) تعمل كمدخل للبيانات ومخرج للتحديثات في نفس الوقت وبسطر كود واحد، مما يقلل الحجم البرمجي ويزيد من نظافة الكود وتماسكه.
عندما تقوم بتعريف خاصية باستخدام model()، مثل count = model(0)، يقوم محرك أنجولر تلقائياً بإنشاء مخرج (Output) مخفي يحمل نفس اسم المتغير مضافاً إليه كلمة Change (أي countChange). هذا يسمح للمكون الأب باستخدام صيغة الربط ثنائي الاتجاه الشهيرة [(count)] بكل سلاسة وبدون أي إعدادات إضافية.
على عكس دالة input() التي ترجع إشارة للقراءة فقط (Read-only Signal)، فإن دالة model() ترجع إشارة قابلة للكتابة. هذا يعني أنه يمكنك استخدام الدوال .set() أو .update() لتعديل قيمتها من داخل المكون الفرعي، وبمجرد تعديلها، يتم إرسال القيمة الجديدة للمكون الأب تلقائياً، وتحديث كافة الواجهات المعتمدة عليها.
| 1 | import { Component, model } from '@angular/core'; |
| 2 | |
| 3 | @Component({ |
| 4 | selector: 'app-number-stepper', |
| 5 | standalone: true, |
| 6 | templateUrl: './number-stepper.component.html', |
| 7 | styleUrl: './number-stepper.component.css' |
| 8 | }) |
| 9 | export class NumberStepperComponent { |
| 10 | // استخدام model لإنشاء ربط ثنائي الاتجاه مع المكون الأب |
| 11 | // تعمل كإشارة يمكن قراءتها وتعديلها وإرسال تحديثاتها تلقائياً |
| 12 | stepValue = model<number>(0); |
| 13 | |
| 14 | increment() { |
| 15 | // تحديث القيمة داخلياً، وسيتم إخطار المكون الأب فوراً |
| 16 | this.stepValue.update(current => current + 1); |
| 17 | } |
| 18 | |
| 19 | decrement() { |
| 20 | this.stepValue.update(current => current - 1); |
| 21 | } |
| 22 | } |
| 1 | <div class="stepper-container"> |
| 2 | <button class="btn-step" (click)="decrement()">-</button> |
| 3 | |
| 4 | <!-- قراءة قيمة الـ model كدالة إشارة --> |
| 5 | <span class="step-display">{{ stepValue() }}</span> |
| 6 | |
| 7 | <button class="btn-step" (click)="increment()">+</button> |
| 8 | </div> |
| 1 | .stepper-container { |
| 2 | display: inline-flex; |
| 3 | align-items: center; |
| 4 | background-color: #f1f5f9; |
| 5 | border-radius: 8px; |
| 6 | padding: 4px; |
| 7 | border: 1px solid #cbd5e1; |
| 8 | } |
| 9 | |
| 10 | .btn-step { |
| 11 | background-color: #3b82f6; |
| 12 | color: white; |
| 13 | border: none; |
| 14 | border-radius: 4px; |
| 15 | width: 36px; |
| 16 | height: 36px; |
| 17 | font-size: 1.2rem; |
| 18 | font-weight: bold; |
| 19 | cursor: pointer; |
| 20 | display: flex; |
| 21 | justify-content: center; |
| 22 | align-items: center; |
| 23 | transition: background-color 0.2s; |
| 24 | } |
| 25 | |
| 26 | .btn-step:hover { |
| 27 | background-color: #2563eb; |
| 28 | } |
| 29 | |
| 30 | .step-display { |
| 31 | min-width: 50px; |
| 32 | text-align: center; |
| 33 | font-size: 1.2rem; |
| 34 | font-weight: bold; |
| 35 | color: #1e293b; |
| 36 | margin: 0 12px; |
| 37 | } |
| 1 | import { Component, signal } from '@angular/core'; |
| 2 | import { NumberStepperComponent } from './number-stepper.component'; |
| 3 | |
| 4 | @Component({ |
| 5 | selector: 'app-cart-manager', |
| 6 | standalone: true, |
| 7 | imports: [NumberStepperComponent], |
| 8 | template: ` |
| 9 | <div class="parent-dashboard"> |
| 10 | <h3>إدارة عربة التسوق (المكون الأب)</h3> |
| 11 | <p>الكمية المطلوبة حالياً: <strong>{{ productQuantity() }}</strong></p> |
| 12 | |
| 13 | <!-- استخدام صيغة [(property)] المألوفة مع الـ model الحديث --> |
| 14 | <app-number-stepper [(stepValue)]="productQuantity"></app-number-stepper> |
| 15 | |
| 16 | <div class="actions"> |
| 17 | <button (click)="resetQuantity()">إعادة ضبط الكمية من الأب</button> |
| 18 | </div> |
| 19 | </div> |
| 20 | ` |
| 21 | }) |
| 22 | export class CartManagerComponent { |
| 23 | // إشارة في المكون الأب للتحكم في الكمية |
| 24 | productQuantity = signal<number>(5); |
| 25 | |
| 26 | resetQuantity() { |
| 27 | // تحديث الإشارة في الأب سينعكس فوراً على المكون الفرعي |
| 28 | this.productQuantity.set(1); |
| 29 | } |
| 30 | } |