| مستوى التفاعل البرمجي | الهدف الهندسي للتنفيذ | طريقة التطبيق المعماري في Angular |
|---|---|---|
| التفاعل مع حالة الصلاحية (`statusChanges`) | مراقبة انتقال النموذج بالكامل من حالة الخطأ إلى الصلاحية لتعديل واجهة المستخدم | متابعة تدفق `form.statusChanges` لتفعيل أو تعطيل زر الإرسال الرئيسي ديناميكياً |
| التفاعل الشرطي بين الحقول (`Cross-Field Reactivity`) | تغيير متطلبات الحقليْن بناءً على اختيار المستخدم (مثل إجبار إدخال رقم الهوية عند اختيار نوع مستخدم معين) | مراقبة `valueChanges` لحقل المفتاح واستخدام `setValidators` و `updateValueAndValidity` على الحقل التابع |
| الحفظ التلقائي للبيانات (`Auto-Save Architecture`) | حفظ حالة النموذج مؤخراً في التخزين المحلي دون إزعاج المستخدم بزر حفظ يدوي | ربط `valueChanges` لـ `FormGroup` بالكامل مع عامل `debounceTime` لحفظ البيانات في الـ LocalStorage بانتظام |
لا تقتصر قوة النماذج التفاعلية على جمع البيانات وإرسالها فقط، بل تمتد لتوفير قدرة فائقة على التفاعل الفوري مع تغييرات الحقول (Real-time Reactivity). بفضل الاعتماد المباشر على تدفقات RxJS من خلال خصائص مثل valueChanges و statusChanges، يتيح لك النظام مراقبة كل حرف يكتبه المستخدم أو كل تعديل في حالة التحقق لحظياً، لتنفيذ مهام متقدمة مثل البحث التلقائي، تغيير واجهة العرض، أو حساب القيم التراكمية ديناميكياً.
كل FormControl أو FormGroup يمتلك تدفقاً (Observable) يسمى valueChanges. يمكنك ربط هذا التدفق بمعالجات RxJS مثل debounceTime لتأخير البحث و distinctUntilChanged لمنع التكرار، مما يضمن أداءً خارقاً وخالياً من الاستدعاءات الزائدة.
يتيح لك statusChanges معرفة متى ينتقل الحقل أو النموذج من حالة الخطأ إلى الصحة، مما يسهل إدارة تفعيل الأزرار أو تنبيه المستخدم بصرياً دون الحاجة لكتابة أحداث DOM معقدة.
| 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; |
| 2 | import { CommonModule } from '@angular/common'; |
| 3 | import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; |
| 4 | import { Subject, takeUntil, debounceTime, distinctUntilChanged } from 'rxjs'; |
| 5 | |
| 6 | @Component({ |
| 7 | selector: 'app-real-time-reactivity', |
| 8 | standalone: true, |
| 9 | imports: [CommonModule, ReactiveFormsModule], |
| 10 | template: ` |
| 11 | <div class="reactivity-card-demo"> |
| 12 | <h3>التفاعل الفوري ومراقبة تغييرات الحقول برمجياً</h3> |
| 13 | |
| 14 | <form [formGroup]="searchFilterForm" class="form-container"> |
| 15 | |
| 16 | <!-- حقل البحث الفوري --> |
| 17 | <div class="field-block"> |
| 18 | <label>البحث الفوري عن المنتجات:</label> |
| 19 | <input |
| 20 | type="text" |
| 21 | formControlName="searchQuery" |
| 22 | placeholder="اكتب للبحث لحظياً..." |
| 23 | class="styled-input-field" |
| 24 | /> |
| 25 | </div> |
| 26 | |
| 27 | <!-- عرض النتيجة الفورية للتفاعل --> |
| 28 | <div class="live-feedback-box"> |
| 29 | <p><strong>النص المُدخل حالياً:</strong> {{ liveTypedValue || 'لا توجد مدخلات بعد...' }}</p> |
| 30 | <p><strong>حالة النموذج العامة:</strong> <span [class.valid-text]="searchFilterForm.valid" [class.invalid-text]="searchFilterForm.invalid">{{ searchFilterForm.status }}</span></p> |
| 31 | </div> |
| 32 | </form> |
| 33 | </div> |
| 34 | `, |
| 35 | styles: [` |
| 36 | .reactivity-card-demo { background: #090d16; color: #fff; padding: 24px; border-radius: 12px; border: 1px solid #1e293b; } |
| 37 | .form-container { display: flex; flex-direction: column; gap: 16px; } |
| 38 | .field-block { display: flex; flex-direction: column; gap: 6px; } |
| 39 | .styled-input-field { background: #1e293b; border: 1px solid #334155; padding: 10px; border-radius: 6px; color: #fff; } |
| 40 | .live-feedback-box { background: #1e293b; border: 1px solid #334155; padding: 16px; border-radius: 8px; font-size: 14px; } |
| 41 | .live-feedback-box p { margin: 6px 0; color: #94a3b8; } |
| 42 | .live-feedback-box strong { color: #38bdf8; } |
| 43 | .valid-text { color: #10b981; font-weight: bold; } |
| 44 | .invalid-text { color: #f43f5e; font-weight: bold; } |
| 45 | `] |
| 46 | }) |
| 47 | export class RealTimeReactivityComponent implements OnInit, OnDestroy { |
| 48 | searchFilterForm!: FormGroup; |
| 49 | liveTypedValue: string = ''; |
| 50 | private destroy$ = new Subject<void>(); |
| 51 | |
| 52 | constructor(private fb: FormBuilder) {} |
| 53 | |
| 54 | ngOnInit(): void { |
| 55 | // بناء نموذج البحث |
| 56 | this.searchFilterForm = this.fb.group({ |
| 57 | searchQuery: ['', [Validators.minLength(2)]] |
| 58 | }); |
| 59 | |
| 60 | // ربط مراقبة التغييرات الفورية مع تحسين الأداء عبر RxJS |
| 61 | this.searchFilterForm.get('searchQuery')?.valueChanges.pipe( |
| 62 | debounceTime(300), // الانتظار 300 جزء من الثانية بعد توقف الكتابة |
| 63 | distinctUntilChanged(), // عدم تنفيذ الحدث إلا إذا تغيرت القيمة فعلياً |
| 64 | takeUntil(this.destroy$) // إلغاء الاشتراكات تلقائياً عند تدمير المكون لمنع تسرب الذاكرة |
| 65 | ).subscribe(value => { |
| 66 | this.liveTypedValue = value; |
| 67 | console.log('تنفيذ عملية البحث الفوري بناءً على القيمة:', value); |
| 68 | }); |
| 69 | } |
| 70 | |
| 71 | ngOnDestroy(): void { |
| 72 | // إغلاق التدفقات لتنظيف الموارد بشكل هندسي سليم |
| 73 | this.destroy$.next(); |
| 74 | this.destroy$.complete(); |
| 75 | } |
| 76 | } |
⚠️ تحذير
لا تنسَ أبداً إلغاء الاشتراكات (Unsubscribe) عند استخدام valueChanges المخصصة! الاستمرار في مراقبة التدفقات دون ربطها بآلية تدمير المكون (takeUntil أو إشارات دورة الحياة الحديثة) يؤدي حتماً إلى تسرب الذاكرة وبطء التطبيق بمرور الوقت