فرمهای Angular به زیبایی با HTML استاندارد کار میکنند. input ،textarea و select همگی بدون تلاش اضافی در Reactive Forms جای میگیرند. این فریمورک رویدادها، مقادیر و وضعیتهای آنها را درک میکند.
اما اپلیکیشنهای مدرن بهندرت تنها با عناصر استاندارد به کار خود ادامه میدهند. ممکن است به یک ویجت امتیازدهی ستارهای، یک انتخابگر تاریخ ترکیبی یا یک انتخابگر رنگ سفارشی نیاز داشته باشید. اگر یکی از اینها را در یک form group قرار دهید، Angular با آن مانند یک HTML مرده برخورد میکند. patchValue هیچ کاری انجام نمیدهد. اعتبارسنجها (Validators) آن را نادیده میگیرند. فرم هیچ ایدهای ندارد که کاربر چه زمانی با کنترل تعامل برقرار میکند و form.disable() ویجت سفارشی را کاملاً تعاملی باقی میگذارد.
این همان مشکلی است که ControlValueAccessor برای حل آن وجود دارد.
ControlValueAccessor در واقع چه کاری انجام میدهد
ControlValueAccessor قراردادی است که یک کامپوننت سفارشی را به یک عضو اصلی و سطحبالا در فرم تبدیل میکند. این رابط مانند یک مترجم بین Angular Forms API و رابط کاربری (UI) شما عمل میکند. زمانی که آن را به درستی پیادهسازی کنید، کامپوننت شما از دیدگاه فرم، از یک input بومی غیرقابل تشخیص خواهد بود. این کامپوننت میتواند مقادیر را دریافت کند، تغییرات را منتشر کند، وضعیتهای لمس شده (touches) را گزارش دهد و مانند یک عنصر داخلی، به وضعیتهای غیرفعال (disabled) احترام بگذارد.
این اینترفیس به چهار متد خاص نیاز دارد. هر کدام از آنها یک جهت مشخص از ارتباط را مدیریت میکنند.
writeValue: از فرم به کامپوننت
writeValue(obj) مسیر ورودی است. هر زمان که مدل فرم بهروزرسانی شود و نیاز باشد مقدار جدیدی را به UI شما تزریق کند، Angular این متد را فراخوانی میکند. اگر patchValue({ rating: 4 }) را روی یک form group اجرا کنید، آن مقدار 4 از طریق writeValue به داخل کامپوننت شما میرسد. اگر فرم را ریست کنید، writeValue مقدار اولیه جدید یا null را دریافت میکند. وظیفه شما در این متد این است که آن دادههای ورودی را بگیرید و آنها را به وضعیت داخلی کامپوننت خود نگاشت (map) کنید. اگر در حال ساخت یک انتخابگر رنگ هستید، writeValue یک رشته هگز مانند #ff4400 دریافت میکند و شما باید view خود را بهروز کنید تا آن رنگ را به عنوان رنگ انتخابشده نشان دهد.
در اینجا یک نکته عملی وجود دارد. Angular میتواند writeValue را قبل از اینکه view شما کاملاً مقداردهی اولیه شود فراخوانی کند، بهویژه در کامپوننتهایی که به صورت پویا رندر میشوند، دیالوگها یا رابطهای کاربری تببندی شده. اگر کامپوننت شما سعی کند خیلی زود به DOM یا کامپوننتهای فرزند دسترسی پیدا کند، ممکن است با خطاهای زمان اجرا (runtime errors) مواجه شوید. یک الگوی مطمئن این است که مقدار را در یک ویژگی (property) محلی ذخیره کنید و پس از مقداردهی اولیه view آن را اعمال کنید، یا در برابر ارجاعات تعریفنشده به فرزندان محافظت کنید. هرگز فرض نکنید که writeValue فقط زمانی اجرا میشود که template شما پایدار است.
registerOnChange: از کامپوننت به فرم
registerOnChange(fn) مسیر خروجی را تنظیم میکند. Angular یک تابع callback به شما میدهد و شما باید ارجاعی به آن نگه دارید. هر بار که کاربر مقدار داخل کامپوننت شما را تغییر میدهد، شما آن تابع را با مقدار جدید فراخوانی میکنید. در یک کامپوننت امتیازدهی ستارهای، وقتی کاربر روی ستاره سوم کلیک میکند، شما callback ذخیرهشده را با مقدار 3 فراخوانی میکنید. آن فراخوانی به FormControl بازمیگردد، مدل را بهروز میکند، هرگونه اشتراک (subscription) در valueChanges را تحریک میکند و اعتبارسنجها را دوباره اجرا میکند.
نادیده گرفتن این مرحله، رایجترین راه برای خراب کردن بیصدای یک فرم است. ویجت ممکن است فعال به نظر برسد؛ کاربر میبیند که ستارهها روشن میشوند، رنگها تغییر میکنند یا تاریخها پر میشوند، اما مدل فرم هرگز بهروز نمیشود. اعتبارسنجها همچنان دادههای قدیمی (stale) را ارزیابی میکنند. هندلرهای ارسال (Submit handlers) مقادیر قدیمی را میفرستند. کامپوننت به نظر کار میکند، اما فرم در واقع کور است. اگر کنترل سفارشی شما ورودی کاربر را میپذیرد اما فرمِ اطراف آن هرگز متوجه آن نمیشود، تقریباً همیشه مقصر همین مرحله است.
registerOnTouched: گزارش تعامل
فرمها فقط مقادیر را دنبال نمیکنند؛ آنها دنبال این هستند که آیا کاربر با یک فیلد تعامل داشته است یا خیر. Angular از وضعیت touched استفاده میکند تا تصمیم بگیرد چه زمانی نمایش خطاهای اعتبارسنجی مناسب است. یک ورودی متنیِ اجباری (required) نباید بلافاصله پس از بارگذاری صفحه قرمز شود؛ بلکه باید منتظر بماند تا کاربر با کلید Tab از آن خارج شود یا جای دیگری کلیک کند.
ورودیهای بومی این کار را بهطور خودکار از طریق رویدادهای blur انجام میدهند. کامپوننتهای سفارشی اینگونه نیستند. شما باید از registerOnTouched(fn) استفاده کنید تا خودتان این تعاملات را گزارش دهید. Angular یک callback دیگر به شما میدهد؛ شما زمانی آن را فراخوانی میکنید که تصمیم بگیرید کاربر به شکلی معنادار با کنترل تعامل برقرار کرده است.
زمانبندی دقیق به کامپوننت شما بستگی دارد. برای یک ورودی سفارشی شبیه متن، ممکن است آن را هنگام blur فراخوانی کنید. برای امتیازدهی ستارهای، احتمالاً اولین کلیک لحظه مناسبی است. برای یک انتخابگر رنگ که یک popover باز میکند، ممکن است تا بسته شدن پالت رنگ صبر کنید. نکته کلیدی، ثبات است. اگر هرگز callback مربوط به touched را فراخ
setDisabledState: احترام به دستورات فرم
فرمهای پویا مدام فیلدها را بر اساس منطق تجاری (business logic) فعال یا غیرفعال میکنند. وقتی متد .disable() را روی یک FormControl فراخوانی میکنید، انگولار نیاز دارد که کامپوننت سفارشی شما به آن پاسخ دهد. متد setDisabledState(isDisabled) یک مقدار بولین دریافت میکند. وقتی این مقدار true باشد، شما باید رابط کاربری (UI) خود را قفل کنید.
این موضوع چیزی فراتر از نادیده گرفتن کلیکهاست. شما باید دکمههای داخلی را غیرفعال کنید، حالتهای قابل فوکوس (focusable) را حذف کنید و تغییرات ظاهری مانند کاهش شفافیت (opacity) یا pointer-events: none را اعمال کنید. اگر این متد را نادیده بگیرید، کامپوننت شما کاملاً تعاملی باقی میماند در حالی که مدلِ فرم اصرار دارد که غیرفعال است. این کار باعث ایجاد باگهای سختعیبیابی میشود. کاربران میتوانند مقادیری را تغییر دهند که فرم قاعدتاً باید آنها را رد کند. دکمههای ذخیره ممکن است بر اساس وضعیتهای نامعتبر فعال شوند. در واقع، گروه فرم (form group) و رابط کاربری از هم فاصله میگیرند (ناهماهنگ میشوند).
یک کنترل سفارشیِ خوشساخت، با setDisabledState به عنوان یک الزام اصلی برخورد میکند، نه یک موضوع فرعی که بعداً به آن فکر شود.
اشتباهاتی که زمان عیبیابی شما را تلف میکنند
چندین اشتباه تکراری باعث سردرگمی توسعهدهندگانی میشود که با این اینترفیس (interface) تازه آشنا شدهاند.
فراموش کردن فراخوانی کالبکِ تغییر (change callback). کامپوننت شما وضعیت داخلی خود را بهروز میکند، اما فرم هرگز از آن مطلع نمیشود. اعتبارسنجها (Validators) متوقف میشوند و فرمهای والد، دادههای قدیمی (stale) را ارسال میکنند. همیشه به محض اینکه کاربر مقدار جدیدی را ثبت کرد، آن تابع ذخیرهشدهی onChange را اجرا کنید.
نادیده گرفتن کالبکِ لمسشده (touched callback). بدون آن، Angular هرگز کنترل را به عنوان touched علامتگذاری نمیکند. پیامهای خطایی که به وضعیتهای touched یا dirty وابسته هستند، نمایش داده نمیشوند. کاربران به فرمی خیره میشوند که درست به نظر میرسد اما ارسال نمیشود، بدون اینکه هیچ نشانه ظاهری از مشکل وجود داشته باشد.
بیتوجهی به وضعیت غیرفعال (disabled state). یک کنترل که از نظر ظاهری فعال است اما فرم فکر میکند غیرفعال است، مرز اعتماد را میشکند. کاربر میتواند به تایپ کردن یا کلیک کردن ادامه دهد، اما مدل ورودیهای او را نادیده میگیرد. یا بدتر از آن، مدل در طول چرخههای همگامسازی (sync cycles)، به صورت پراکنده ورودیهای کاربر را بازنویسی میکند.
حذف NG_VALUE_ACCESSOR از پرووایدرها. این یک قاتل خاموش است. اگر چهار متد را پیادهسازی کنید اما فراموش کنید NG_VALUE_ACCESSOR را به آرایه providers کامپوننت خود اضافه کنید، Angular هرگز کامپوننت شما را به عنوان یک value accessor ثبت نمیکند. کد کامپایل میشود، ویو (view) رندر میشود، اما هیچ چیزی Bind نمیشود. هیچ پیام خطایی دریافت نمیکنید، فقط کامپوننتی دارید که کاملاً خارج از فرم شناور است. همیشه آن را در متادیتای دکوراتور (decorator metadata) بگنجانید.
Signals، Validators و Angular مدرن
ControlValueAccessor یک سطح API قدیمی (legacy) نیست؛ بلکه به خوبی با توسعه مدرن در Angular سازگار است. چه وضعیت داخلی را با Signals، ویژگیهای ساده (plain properties) یا RxJS subjects مدیریت کنید، آن چهار متد همچنان قرارداد عمومی (public contract) شما با ماژول فرمها باقی میمانند. شما مقادیر را در writeValue مصرف میکنید، Signals یا وضعیت خود را تغییر میدهید و از طریق کالبکهایی که Angular فراهم میکند، آنها را منتشر (emit) میکنید.
اعتبارسنجهای استاندارد بدون تغییر کار میکنند. Validators.required ،Validators.min ،Validators.pattern و اعتبارسنجهای سفارشیِ بینفیلدی (cross-field)، همگی کامپوننت مبتنی بر CVA شما را دقیقاً همانطور ارزیابی میکنند که یک input بومی را ارزیابی میکنند. کنترلِ فرم، یک مقدار و یک وضعیت را میبیند و اهمیتی نمیدهد که آن مقدار از یک جعبه متن (text box) آمده باشد یا از یک انتخابگر ماه (month-picker) که خودتان ساختهاید.
همین قابلیت جابهجایی (portability) دلیل اهمیت CVA برای سیستمهای طراحی (design systems) و کتابخانههای UI مشترک است. یک تیم یک ورودی شماره تلفن قدرتمند یا یک ویجت آپلود فایل میسازد. آنها این اینترفیس را فقط یک بار پیادهسازی میکنند. تمام تیمهای دیگر در سازمان میتوانند آن را بدون هیچ تنظیمات اضافی در Reactive Forms خود قرار دهند. کامپوننت به شکلی قابل پیشبینی رفتار میکند، به طور یکسان اعتبارسنجی میشود و در تمام ماژولهای ویژگی (feature modules)، به طور منسجم غیرفعال میشود.
نکته اصلی
ControlValueAccessor فقط یک اینترفیس دیگر برای حفظ کردن جهت سوالات مصاحبه نیست. این پلی است که به کامپوننتهای سفارشی شما اجازه میدهد به عنوان همتراز با عناصر بومی HTML، در اکوسیستم فرم Angular مشارکت کنند. تسلط بر آن به معنای درک گفتگوی کامل بین ویجت شما و فرم است: دریافت مقادیر، گزارش تغییرات، اعلام لمس شدن (touches) و احترام به وضعیتهای غیرفعال. اگر این چهار بخش را درست انجام دهید، میتوانید کنترلهای فرم پیچیده و قابل استفاده مجددی بسازید که برای توسعهدهندگانی که از آنها استفاده میکنند، کاملاً نامحسوس (invisible) باشند. این نشانهی یک کامپوننت حرفهای در Angular است.
