تعمل نماذج Angular بشكل رائع مع HTML القياسي. فجميع عناصر input وtextarea وselect تندمج في Reactive Forms دون أي مجهود إضافي. يفهم الإطار أحداثها، وقيمها، وحالاتها.

لكن التطبيقات الحديثة نادرًا ما تكتفي بالعناصر القياسية وحدها. قد تحتاج إلى أداة تقييم بالنجوم (star rating widget)، أو منتقي تاريخ مركب، أو منتقي ألوان مخصص. إذا وضعت أحد هذه العناصر داخل مجموعة نموذج (form group)، فسيعاملها Angular كأنها HTML ميت. لن تفعل patchValue أي شيء، وسيتجاهلها الـ Validators. لن يكون لدى النموذج أي فكرة عن وقت تفاعل المستخدم مع عنصر التحكم، كما أن form.disable() سيترك الأداة المخصصة تفاعلية بالكامل.

هذه هي المشكلة التي وُجد ControlValueAccessor لحلها.

ما الذي يفعله ControlValueAccessor فعليًا

يُعد ControlValueAccessor هو العقد (contract) الذي يحول المكون المخصص إلى عنصر نموذج من الدرجة الأولى. فهو يعمل كمترجم بين Angular Forms API وواجهة المستخدم الخاصة بك. بمجرد تنفيذه بشكل صحيح، يصبح مكونك غير قابل للتمييز عن أي عنصر إدخال أصلي (native input) من وجهة نظر النموذج. يمكنه استقبال القيم، وإرسال التغييرات، والإبلاغ عن حالات اللمس (touches)، واحترام حالات التعطيل تمامًا مثل العناصر المدمجة.

تتطلب الواجهة أربع طرق (methods) محددة، حيث تتعامل كل واحدة منها مع اتجاه مختلف من الاتصال.

writeValue: من النموذج إلى المكون

تُعد writeValue(obj) هي المسار الوارد. كلما تم تحديث نموذج البيانات (form model) واحتاج إلى دفع قيمة جديدة إلى واجهة المستخدم الخاصة بك، يقوم Angular باستدعاء هذه الطريقة. إذا قمت باستدعاء patchValue({ rating: 4 }) على مجموعة نموذج، فستصل تلك القيمة 4 إلى داخل مكونك عبر writeValue. وإذا قمت بإعادة ضبط النموذج، فستتلقى writeValue القيمة الأولية الجديدة أو null. مهمتك داخل هذه الطريقة هي أخذ تلك البيانات الواردة وربطها بالحالة الداخلية لمكونك. إذا كنت تبني منتقي ألوان، فستتلقى writeValue سلسلة hex مثل #ff4400 ، ويجب عليك تحديث العرض (view) الخاص بك لإظهار ذلك اللون كخيار محدد.

هناك عقبة عملية هنا؛ حيث يمكن لـ Angular استدعاء writeValue قبل تهيئة العرض (view) بالكامل، خاصة داخل المكونات التي يتم عرضها ديناميكيًا، أو في النوافذ الحوارية (dialogs)، أو واجهات التبويب. إذا حاول مكونك الوصول إلى الـ DOM أو المكونات الفرعية في وقت مبكر جدًا، فقد تواجه أخطاء وقت التشغيل (runtime errors). النمط السليم هو تخزين القيمة في خاصية محلية وتطبيقها بعد تهيئة العرض، أو الحماية من المراجع الفرعية غير المعرفة (undefined child references). لا تفترض أبدًا أن writeValue لا تعمل إلا عندما يكون القالب (template) مستقرًا.

registerOnChange: من المكون إلى النموذج

تقوم registerOnChange(fn) بإعداد المسار الصادر. حيث يمنحك Angular دالة استدعاء (callback function)، ويجب عليك الاحتفاظ بمرجع لها. في كل مرة يغير فيها المستخدم القيمة داخل مكونك، تقوم باستدعاء تلك الدالة بالقيمة الجديدة. في مكون التقييم بالنجوم، عندما ينقر المستخدم على النجمة الثالثة، تقوم باستدعاء الـ callback المخزن بالقيمة 3. يتدفق هذا الاستدعاء عائدًا إلى FormControl ، ويحدث النموذج، ويحفز أي اشتراكات في valueChanges ، ويعيد تشغيل الـ validators.

إن تخطي هذه الخطوة هو الطريقة الأكثر شيوعًا لتعطيل النموذج بصمت. قد تبدو الأداة وكأنها تعمل؛ حيث يرى المستخدم النجوم تضيء، أو الألوان تتغير، أو التواريخ تظهر. لكن نموذج البيانات لا يتحدث أبدًا. تستمر الـ Validators في تقييم بيانات قديمة، وترسل معالجات الإرسال (submit handlers) قيمًا قديمة. يبدو المكون وكأنه يعمل، ومع ذلك يكون النموذج "أعمى" فعليًا. إذا كان عنصر التحكم المخصص الخاص بك يقبل مدخلات المستخدم ولكن النموذج المحيط به لا يلاحظ ذلك أبدًا، فهذا هو السبب في الغالب.

registerOnTouched: الإبلاغ عن التفاعل

لا تكتفي النماذج بتتبع القيم فحسب، بل تتبع أيضًا ما إذا كان المستخدم قد تفاعل مع الحقل أم لا. يستخدم Angular حالة "اللمس" (touched state) لتحديد الوقت المناسب لإظهار أخطاء التحقق (validation errors). فلا ينبغي لمدخل نصي مطلوب (required text input) أن يومض باللون الأحمر بمجرد تحميل الصفحة، بل يجب أن ينتظر حتى ينتقل المستخدم إلى حقل آخر أو ينقر في مكان آخر.

تتعامل المدخلات الأصلية مع هذا تلقائيًا من خلال أحداث الـ blur. أما المكونات المخصصة فلا تفعل ذلك. يجب عليك استخدام registerOnTouched(fn) للإبلاغ عن هذه التفاعلات بنفسك. يمنحك Angular دالة استدعاء (callback) أخرى؛ تقوم باستدعائها عندما تقرر أن المستخدم قد تفاعل بشكل فعال مع عنصر التحكم.

يعتمد التوقيت الدقيق على مكونك. بالنسبة لمدخل مخصص يشبه النص، قد تستدعيها عند الـ blur. بالنسبة للتقييم بالنجوم، فمن المحتمل أن تكون النقرة الأولى هي اللحظة المناسبة. بالنسبة لمنتقي الألوان الذي يفتح نافذة منبثقة (popover)، قد تنتظر حتى تُغلق لوحة الألوان. المفتاح هو الاستمرارية؛ فإذا لم تستدعِ دالة الـ touched callback أبدًا، فسيستمر Angular في تمييز عنصر التحكم على أنه "نقي" (pristine). وستظل أخطاء التحقق مخفية حتى بعد أن ينتهي المستخدم بوضوح من التحرير، مما يؤدي إلى الارتباك وسوء تجربة المستخدم.

setDisabledState: احترام أوامر النموذج

تقوم النماذج الديناميكية بتفعيل وتعطيل الحقول باستمرار بناءً على منطق العمل (business logic). عندما تستدعي .disable() على FormControl ، يحتاج Angular من مكونك المخصص أن يستجيب. تستقبل الدالة setDisabledState(isDisabled) قيمة منطقية (boolean). عندما تكون القيمة true ، يجب عليك إغلاق واجهة المستخدم الخاصة بك.

هذا يعني أكثر من مجرد تجاهل النقرات. يجب عليك تعطيل الأزرار الداخلية، وإزالة حالات التركيز (focusable states)، وتطبيق تأثيرات بصرية مثل تقليل الشفافية أو pointer-events: none. إذا تجاهلت هذه الطريقة، فسيظل مكونك تفاعليًا بالكامل بينما يصر نموذج النموذج (form model) على أنه معطل. يؤدي ذلك إلى حدوث أخطاء يصعب تتبعها؛ حيث يمكن للمستخدمين تعديل القيم التي من المفترض أن يرفضها النموذج. قد يتم تفعيل أزرار الحفظ بناءً على حالات غير صالحة، مما يؤدي إلى انفصال مجموعة النموذج (form group) عن واجهة المستخدم.

يعامل عنصر التحكم المخصص المصمم جيدًا setDisabledState كمتطلب أساسي، وليس مجرد فكرة لاحقة.

أخطاء ستكلفك وقتًا في تصحيح الأخطاء (Debugging)

هناك العديد من الأخطاء المتكررة التي تضلل المطورين الجدد في هذه الواجهة.

نسيان استدعاء دالة التغيير (change callback). يقوم مكونك بتحديث حالته الداخلية، لكن النموذج لا يعلم بذلك أبدًا. تتوقف أدوات التحقق (Validators) عن العمل، وتقوم النماذج الأب بإرسال بيانات قديمة. احرص دائمًا على تشغيل دالة onChange المخزنة في اللحظة التي يثبت فيها المستخدم قيمة جديدة.

تخطي دالة اللمس (touched callback). بدونها، لن يقوم Angular أبدًا بتمييز عنصر التحكم على أنه "تم لمسه" (touched). وبالتالي لن تظهر رسائل الخطأ المرتبطة بحالات touched أو dirty. سيحدق المستخدمون في نموذج يبدو صحيحًا ولكنه لا يُرسل، دون أي إشارة مرئية لما هو الخطأ.

إهمال حالة التعطيل (disabled state). إن وجود عنصر تحكم مفعل بصريًا بينما يعتقد النموذج أنه معطل يؤدي إلى كسر حدود الثقة. يمكن للمستخدم الاستمرار في الكتابة أو النقر، لكن النموذج يتجاهله. أو والأسوأ من ذلك، قد يقوم النموذج بمسح مدخلاتهم بشكل متقطع أثناء دورات المزامنة.

إغفال مزود NG_VALUE_ACCESSOR. هذا هو القاتل الصامت. إذا قمت بتنفيذ الطرق الأربع ولكن نسيت إضافة NG_VALUE_ACCESSOR إلى مصفوفة providers الخاصة بمكونك، فلن يقوم Angular أبدًا بتسجيل مكونك كمزود قيم (value accessor). الكود سيعمل (compiles)، والواجهة ستظهر (renders)، ولكن لن يتم ربط أي شيء. لن تظهر رسالة خطأ، بل مجرد مكون يطفو خارج النموذج تمامًا. احرص دائمًا على تضمينه في بيانات الميتا (decorator metadata).

Signals و Validators و Angular الحديث

ControlValueAccessor ليست واجهة برمجة تطبيقات (API) قديمة. فهي تتناسب تمامًا مع تطوير Angular الحديث. سواء كنت تدير الحالة الداخلية باستخدام Signals أو الخصائص العادية أو RxJS subjects ، تظل الطرق الأربع هي عقدك العام مع وحدة النماذج (forms module). أنت تستهلك القيم في writeValue ، وتعدل Signals أو الحالة الخاصة بك، وتصدر القيم عبر دوال الاستدعاء (callbacks) التي يوفرها Angular.

تعمل أدوات التحقق (validators) القياسية دون تعديل. فكل من Validators.required و Validators.min و Validators.pattern وأدوات التحقق المخصصة عبر الحقول (cross-field validators) تقوم بتقييم مكونك المدعوم بـ CVA تمامًا كما تفعل مع المدخلات الأصلية (native input). يرى عنصر تحكم النموذج قيمة وحالة، ولا يهمه ما إذا كانت تلك القيمة قد جاءت من مربع نص أو من منتقي شهور (month-picker) مصمم يدويًا.

هذه القابلية للنقل هي السبب في أهمية CVA لأنظمة التصميم ومكتبات واجهة المستخدم المشتركة. يقوم فريق واحد ببناء مدخل رقم هاتف قوي أو أداة رفع ملفات، ويقومون بتنفيذ الواجهة مرة واحدة. ثم يقوم كل فريق آخر في المؤسسة بوضعها في نماذجهم التفاعلية (Reactive Forms) دون الحاجة إلى أي توصيلات إضافية. يتصرف المكون بشكل متوقع، ويتحقق من الصحة بشكل موحد، ويتم تعطيله باتساق عبر كل وحدة ميزة (feature module).

الخلاصة الحقيقية

ControlValueAccessor ليست مجرد واجهة أخرى يجب حفظها لأسئلة المقابلات. إنها الجسر الذي يسمح لمكوناتك المخصصة بالمشاركة في منظومة نماذج Angular كأنداد لعناصر HTML الأصلية. إتقانها يعني فهم المحادثة الكاملة بين الأداة (widget) والنموذج: استقبال القيم، والإبلاغ عن التغييرات، والإعلان عن اللمس، واحترام حالات التعطيل. إذا أتقنت هذه الأجزاء الأربعة، يمكنك بناء عناصر تحكم في النماذج معقدة وقابلة لإعادة الاستخدام تبدو غير مرئية للمطورين الذين يستخدمونها. هذه هي علامة مكون Angular الاحترافي.