| معيار المقارنة البرمجية | الربط ثنائي الاتجاه التقليدي (@Input و @Output) | الربط الحديث المعتمد على الإشارات (model API) |
|---|---|---|
| طريقة التعريف في صنف المكون | @Input() val: T; و @Output() valChange = new EventEmitter<T>(); | val = model<T>(defaultValue); |
| تحديث القيمة وإرسالها للأب | this.val = newVal; this.valChange.emit(newVal); | this.val.set(newVal); (تحديث وإرسال تلقائي فوري) |
| طبيعة المتغير داخل المكون | متغير عادي يحتاج لإدارة يدوية وإضافية للتغييرات | إشارة قابلة للكتابة (WritableSignal) تتكامل كلياً مع نظام الإشارات |
| حجم الكود وسهولة الصيانة | يتطلب كتابة مزدوجة وإدارة دقيقة لأسماء المخرجات | كود مختصر للغاية، آمن من حيث الأنواع، وتحديث تلقائي للواجهة |
نصل في هذا الدرس السادس من الفصل السابع إلى جوهرة التاج في التواصل المتقدم بين المكونات في إصدار Angular 22.1، ألا وهو الربط ثنائي الاتجاه (Two-Way Binding). باستخدام دالة model() الحديثة، يمكنك إنشاء قنوات اتصال ذكية ومزدوجة الاتجاه بين المكون الأب والمكون الفرعي بكل سهولة وسلاسة، حيث تعمل القيمة كمدخل ومخرج في آنٍ واحد دون الحاجة لتعقيدات البرمجة القديمة. ومثل الدروس السابقة، سنحافظ على تنوع الأكواد لتشمل ملفات الـ TypeScript، وقوالب الـ HTML، وملفات التنسيق CSS.
عندما تقوم بتعريف خاصية باستخدام model()، يقوم محرك أنجولر تلقائياً بإنشاء مخرج مخفي يحمل نفس اسم المتغير مضافاً إليه كلمة Change. هذا يسمح للمكون الأب باستخدام صيغة الربط ثنائي الاتجاه الشهيرة [(property)] بكل سلاسة.
على عكس دالة input() التي ترجع إشارة للقراءة فقط، فإن model() ترجع إشارة قابلة للكتابة. هذا يتيح للمكون الفرعي تعديل القيمة محلياً عبر .set() أو .update(), ليتم مزامنتها وإرسالها للمكون الأب تلقائياً فوراً.
| 1 | import { Component, model } from '@angular/core'; |
| 2 | |
| 3 | @Component({ |
| 4 | selector: 'app-counter-stepper', |
| 5 | standalone: true, |
| 6 | templateUrl: './counter-stepper.component.html', |
| 7 | styleUrl: './counter-stepper.component.css' |
| 8 | }) |
| 9 | export class CounterStepperComponent { |
| 10 | // تعريف متغير ربط ثنائي الاتجاه يعتمد على الإشارات |
| 11 | currentValue = model<number>(0); |
| 12 | |
| 13 | increment() { |
| 14 | this.currentValue.update(val => val + 1); |
| 15 | } |
| 16 | |
| 17 | decrement() { |
| 18 | this.currentValue.update(val => val - 1); |
| 19 | } |
| 20 | } |
| 1 | <div class="stepper-box"> |
| 2 | <button (click)="decrement()">-</button> |
| 3 | <span class="value-display">{{ currentValue() }}</span> |
| 4 | <button (click)="increment()">+</button> |
| 5 | </div> |
| 1 | .stepper-box { |
| 2 | display: inline-flex; |
| 3 | align-items: center; |
| 4 | background: #1e293b; |
| 5 | padding: 8px; |
| 6 | border-radius: 8px; |
| 7 | border: 1px solid #475569; |
| 8 | margin-top: 10px; |
| 9 | } |
| 10 | |
| 11 | .stepper-box button { |
| 12 | background: #3b82f6; |
| 13 | color: white; |
| 14 | border: none; |
| 15 | width: 32px; |
| 16 | height: 32px; |
| 17 | border-radius: 4px; |
| 18 | font-weight: bold; |
| 19 | cursor: pointer; |
| 20 | } |
| 21 | |
| 22 | .stepper-box button:hover { |
| 23 | background: #2563eb; |
| 24 | } |
| 25 | |
| 26 | .value-display { |
| 27 | color: #f8fafc; |
| 28 | font-size: 1.1rem; |
| 29 | font-weight: bold; |
| 30 | margin: 0 16px; |
| 31 | min-width: 30px; |
| 32 | text-align: center; |
| 33 | } |
| 1 | import { Component, signal } from '@angular/core'; |
| 2 | import { CounterStepperComponent } from './counter-stepper.component'; |
| 3 | |
| 4 | @Component({ |
| 5 | selector: 'app-shop-cart-summary', |
| 6 | standalone: true, |
| 7 | imports: [CounterStepperComponent], |
| 8 | template: ` |
| 9 | <div class="cart-summary"> |
| 10 | <h3>ملخص عربة التسوق (الأب)</h3> |
| 11 | <p>الكمية المختارة حالياً في الأب: <strong>{{ totalItems() }}</strong></p> |
| 12 | |
| 13 | <!-- استخدام الربط ثنائي الاتجاه الحديث مع المكون الفرعي --> |
| 14 | <app-counter-stepper [(currentValue)]="totalItems"></app-counter-stepper> |
| 15 | </div> |
| 16 | ` |
| 17 | }) |
| 18 | export class ShopCartSummaryComponent { |
| 19 | totalItems = signal<number>(3); |
| 20 | } |