Angular forms স্ট্যান্ডার্ড HTML-এর সাথে চমৎকারভাবে কাজ করে। input, textarea, এবং select কোনো অতিরিক্ত প্রচেষ্টা ছাড়াই Reactive Forms-এ যুক্ত হয়ে যায়। ফ্রেমওয়ার্কটি এদের ইভেন্ট (events), ভ্যালু (values) এবং স্টেট (states) বুঝতে পারে।
কিন্তু আধুনিক অ্যাপ্লিকেশনগুলো কেবল স্ট্যান্ডার্ড এলিমেন্ট দিয়ে খুব কমই চলে। আপনার একটি স্টার রেটিং উইজেট (star rating widget), একটি কম্পোজিট ডেট সিলেক্টর (composite date selector), অথবা একটি কাস্টম কালার পিকার (custom color picker)-এর প্রয়োজন হতে পারে। এগুলোর মধ্যে যেকোনো একটিকে একটি form group-এ ব্যবহার করলে Angular সেটিকে একটি 'dead HTML' হিসেবে গণ্য করে। patchValue কিছুই করতে পারে না। Validators এগুলোকে উপেক্ষা করে। ব্যবহারকারী কখন কন্ট্রোলের সাথে ইন্টারঅ্যাক্ট করছেন তা ফর্ম বুঝতে পারে না, এবং form.disable() করার পরেও কাস্টম উইজেটটি পুরোপুরি ইন্টারঅ্যাক্টিভ থেকে যায়।
ControlValueAccessor ঠিক এই সমস্যাটি সমাধানের জন্যই তৈরি করা হয়েছে।
ControlValueAccessor আসলে কী করে
ControlValueAccessor হলো এমন একটি কন্ট্রাক্ট (contract) যা একটি কাস্টম কম্পোনেন্টকে ফর্মের একটি প্রথম শ্রেণীর সদস্য (first-class citizen) হিসেবে রূপান্তরিত করে। এটি Angular Forms API এবং আপনার নিজস্ব UI-এর মধ্যে একটি অনুবাদক (translator) হিসেবে কাজ করে। একবার আপনি এটি সঠিকভাবে ইমপ্লিমেন্ট করলে, ফর্মের দৃষ্টিকোণ থেকে আপনার কম্পোনেন্টটি একটি নেটিভ ইনপুটের (native input) থেকে আলাদা করা যাবে না। এটি একটি বিল্ট-ইন এলিমেন্টের মতোই ভ্যালু গ্রহণ করতে পারে, পরিবর্তনগুলো প্রকাশ (emit) করতে পারে, টাচ (touch) রিপোর্ট করতে পারে এবং ডিজেবল (disabled) স্টেট মেনে চলতে পারে।
এই ইন্টারফেসের জন্য চারটি নির্দিষ্ট মেথড প্রয়োজন। প্রতিটি মেথড যোগাযোগের একটি ভিন্ন দিক পরিচালনা করে।
writeValue: Form থেকে Component
writeValue(obj) হলো ইনবাউন্ড লেন (inbound lane)। যখনই ফর্ম মডেল আপডেট হয় এবং আপনার UI-তে একটি নতুন ভ্যালু পাঠাতে হয়, Angular এই মেথডটি কল করে। আপনি যদি একটি form group-এ patchValue({ rating: 4 }) কল করেন, তবে সেই 4 ভ্যালুটি writeValue-এর মাধ্যমে আপনার কম্পোনেন্টের ভেতরে চলে আসবে। আপনি যদি ফর্মটি রিসেট করেন, তবে writeValue নতুন প্রাথমিক ভ্যালু অথবা null গ্রহণ করবে। এই মেথডের ভেতরে আপনার কাজ হলো সেই ইনকামিং ডেটা গ্রহণ করা এবং সেটিকে আপনার কম্পোনেন্টের ইন্টারনাল স্টেটের (internal state) সাথে ম্যাপ করা। আপনি যদি একটি কালার পিকার তৈরি করেন, তবে writeValue একটি হেক্স স্ট্রিং (hex string) যেমন #ff4400 গ্রহণ করবে, এবং আপনাকে সেই রঙটি সিলেক্টেড হিসেবে দেখানোর জন্য আপনার ভিউ আপডেট করতে হবে।
এখানে একটি ব্যবহারিক জটিলতা রয়েছে। বিশেষ করে ডায়নামিকভাবে রেন্ডার করা কম্পোনেন্ট, ডায়ালগ বা ট্যাবেড ইন্টারফেসের ক্ষেত্রে, আপনার ভিউ পুরোপুরি ইনিশিয়ালাইজ হওয়ার আগেই Angular writeValue কল করতে পারে। যদি আপনার কম্পোনেন্ট খুব দ্রুত DOM বা চাইল্ড কম্পোনেন্ট স্পর্শ করার চেষ্টা করে, তবে আপনি রানটাইম এরর (runtime error) পেতে পারেন। একটি কার্যকর পদ্ধতি হলো ভ্যালুটি একটি লোকাল প্রপার্টিতে (local property) সংরক্ষণ করা এবং ভিউ ইনিশিয়ালাইজ হওয়ার পরে তা প্রয়োগ করা, অথবা আনডিফাইন্ড চাইল্ড রেফারেন্সের (undefined child references) বিরুদ্ধে সুরক্ষা নিশ্চিত করা। কখনোই ধরে নেবেন না যে writeValue শুধুমাত্র তখনই কল হবে যখন আপনার টেমপ্লেটটি স্থিতিশীল (stable) হবে।
registerOnChange: Component থেকে Form
registerOnChange(fn) হলো আউটবাউন্ড লেন (outbound lane) সেটআপ করে। Angular আপনাকে একটি কলব্যাক ফাংশন (callback function) প্রদান করে এবং আপনাকে সেটির একটি রেফারেন্স রাখতে হয়। প্রতিবার যখন ব্যবহারকারী আপনার কম্পোনেন্টের ভেতরে ভ্যালু পরিবর্তন করেন, আপনি সেই ফাংশনটিকে নতুন ভ্যালুসহ কল করবেন। একটি স্টার রেটিং কম্পোনেন্টে, যখন ব্যবহারকারী তৃতীয় স্টাটিতে ক্লিক করেন, আপনি সংরক্ষিত কলব্যাকটিকে 3 দিয়ে ইনভোক (invoke) করবেন। সেই কলটি FormControl-এ ফিরে যায়, মডেল আপডেট করে, যেকোনো valueChanges সাবস্ক্রিপশন ট্রিগার করে এবং ভ্যালিডেটরগুলোকে পুনরায় রান করে।
এই ধাপটি বাদ দেওয়া একটি ফর্মকে নিঃশব্দে নষ্ট করার সবচেয়ে সাধারণ উপায়। উইজেটটি সচল মনে হতে পারে। ব্যবহারকারী দেখবেন স্টারগুলো জ্বলছে, রঙ পরিবর্তন হচ্ছে বা ডেট ফিলআপ হচ্ছে। কিন্তু ফর্ম মডেল কখনোই আপডেট হয় না। Validators পুরনো ডেটা (stale data) মূল্যায়ন করতে থাকে। সাবমিট হ্যান্ডলারগুলো পুরনো ভ্যালু পাঠায়। কম্পোনেন্টটি কাজ করছে বলে মনে হলেও ফর্মটি কার্যত অন্ধ থাকে। যদি আপনার কাস্টম কন্ট্রোল ব্যবহারকারীর ইনপুট গ্রহণ করে কিন্তু আশেপাশের ফর্ম তা বুঝতে না পারে, তবে এর কারণ প্রায় সবসময় এটিই হয়।
registerOnTouched: ইন্টারঅ্যাকশন রিপোর্ট করা
ফর্মগুলো কেবল ভ্যালু ট্র্যাক করে না। তারা এটিও ট্র্যাক করে যে ব্যবহারকারী কোনো ফিল্ডের সাথে ইন্টারঅ্যাক্ট করেছেন কি না। কখন ভ্যালিডেশন এরর (validation errors) দেখানো উপযুক্ত তা সিদ্ধান্ত নিতে Angular 'touched state' ব্যবহার করে। একটি রিকয়ার্ড (required) টেক্সট ইনপুট পেজ লোড হওয়ার সাথে সাথেই লাল হয়ে যাওয়া উচিত নয়। ব্যবহারকারী ট্যাব পরিবর্তন করা বা অন্য কোথাও ক্লিক করা পর্যন্ত এটি অপেক্ষা করা উচিত।
নেটিভ ইনপুটগুলো ব্লার ইভেন্টের (blur events) মাধ্যমে এটি স্বয়ংক্রিয়ভাবে পরিচালনা করে। কাস্টম কম্পোনেন্টগুলো তা করে না। এই ইন্টারঅ্যাকশনগুলো রিপোর্ট করার জন্য আপনাকে অবশ্যই registerOnTouched(fn) ব্যবহার করতে হবে। Angular আপনাকে আরেকটি কলব্যাক দেয়; ব্যবহারকারী কন্ট্রোলের সাথে অর্থপূর্ণভাবে যুক্ত হয়েছেন বলে আপনি যখন সিদ্ধান্ত নেবেন, তখন আপনি এটি কল করবেন।
সঠিক সময়টি আপনার কম্পোনেন্টের ওপর নির্ভর করে। টেক্সট-এর মতো কাস্টম ইনপুটের জন্য, আপনি ব্লার (blur) হওয়ার সময় এটি কল করতে পারেন। স্টার রেটিংয়ের জন্য, প্রথম ক্লিকটি সম্ভবত সঠিক মুহূর্ত। একটি কালার পিকার যা পপওভার (popover) ওপেন করে, তার জন্য আপনি প্যালেটটি বন্ধ হওয়া পর্যন্ত অপেক্ষা করতে পারেন। মূল বিষয়টি হলো ধারাবাহিকতা (consistency)। আপনি যদি কখনোই 'touched callback' কল না করেন, তবে Angular কন্ট্রোলটিকে pristine হিসেবে চিহ্নিত রাখতে থাকবে। ব্যবহারকারী স্পষ্টভাবে এডিটিং শেষ করার পরেও ভ্যালিডেশন এররগুলো লুকিয়ে থাকবে। এটি বিভ্রান্তি এবং খারাপ ইউজার এক্সপেরিয়েন্সের (user experience) দিকে নিয়ে যায়।
setDisabledState: ফর্ম কমান্ড মেনে চলা
ডায়নামিক ফর্মগুলো বিজনেস লজিকের ওপর ভিত্তি করে ক্রমাগত ফিল্ডগুলো enable এবং disable করে। যখন আপনি একটি FormControl-এ .disable() কল করেন, তখন Angular-এর প্রয়োজন হয় আপনার কাস্টম কম্পোনেন্টটি যেন তার প্রতি সাড়া দেয়। setDisabledState(isDisabled) একটি boolean গ্রহণ করে। যখন এটি true হয়, তখন আপনার UI লক করে দেওয়া উচিত।
এর মানে শুধু ক্লিক ইগনোর করা নয়। আপনার ইন্টারনাল বাটনগুলো disable করা উচিত, focusable state সরিয়ে ফেলা উচিত এবং reduced opacity বা pointer-events: none-এর মতো ভিজ্যুয়াল পরিবর্তন প্রয়োগ করা উচিত। আপনি যদি এই মেথডটি ইগনোর করেন, তবে ফর্ম মডেল এটিকে disabled হিসেবে গণ্য করলেও আপনার কম্পোনেন্টটি পুরোপুরি ইন্টারঅ্যাক্টিভ থেকে যাবে। এটি এমন কিছু বাগ (bug) তৈরি করে যা খুঁজে বের করা কঠিন। ব্যবহারকারীরা এমন ভ্যালু পরিবর্তন করতে পারেন যা ফর্মটি মূলত রিজেক্ট করার কথা। সেভ বাটনগুলো ইনভ্যালিড স্টেটের ওপর ভিত্তি করে enable হয়ে যেতে পারে। ফলে ফর্ম গ্রুপ এবং UI-এর মধ্যে অমিল তৈরি হয়।
একটি সুগঠিত কাস্টম কন্ট্রোল setDisabledState-কে একটি প্রাথমিক প্রয়োজনীয়তা হিসেবে বিবেচনা করে, কোনো afterthought হিসেবে নয়।
ভুল যা আপনার ডিবাগিংয়ের সময় নষ্ট করবে
এই ইন্টারফেসের সাথে নতুন ডেভেলপাররা বেশ কিছু পুনরাবৃত্তিমূলক ভুলের সম্মুখীন হন।
change callback কল করতে ভুলে যাওয়া। আপনার কম্পোনেন্ট তার ইন্টারনাল স্টেট আপডেট করে, কিন্তু ফর্মটি তা জানতে পারে না। এর ফলে Validators কাজ করা বন্ধ করে দেয় এবং প্যারেন্ট ফর্মগুলো stale data সাবমিট করে। ব্যবহারকারী যখনই নতুন কোনো ভ্যালু প্রদান করেন, তখনই সেই সংরক্ষিত onChange ফাংশনটি কল করুন।
touched callback বাদ দেওয়া। এটি ছাড়া Angular কন্ট্রোলটিকে কখনোই touched হিসেবে চিহ্নিত করতে পারে না। touched বা dirty স্টেটের সাথে যুক্ত এরর মেসেজগুলো দেখায় না। ব্যবহারকারীরা এমন একটি ফর্মের দিকে তাকিয়ে থাকেন যা দেখতে সঠিক মনে হলেও সাবমিট হচ্ছে না, অথচ কী ভুল হয়েছে তার কোনো দৃশ্যমান ইঙ্গিত নেই।
disabled state অবহেলা করা। একটি কন্ট্রোল যা দেখতে enable অবস্থায় আছে কিন্তু ফর্ম মনে করছে সেটি disabled, তা একটি ত্রুটিপূর্ণ trust boundary তৈরি করে। ব্যবহারকারী টাইপ করা বা ক্লিক করা চালিয়ে যেতে পারেন, কিন্তু মডেল তাদের ইনপুট ইগনোর করে। অথবা আরও খারাপভাবে, সিঙ্ক সাইকেলের সময় মডেলটি মাঝে মাঝে তাদের ইনপুট ওভাররাইট করে দিতে পারে।
NG_VALUE_ACCESSOR provider বাদ দেওয়া। এটি একটি নিঃশব্দ ঘাতক। আপনি যদি চারটি মেথড ইমপ্লিমেন্ট করেন কিন্তু আপনার কম্পোনেন্টের providers অ্যারেতে NG_VALUE_ACCESSOR যোগ করতে ভুলে যান, তবে Angular আপনার কম্পোনেন্টকে কখনোই value accessor হিসেবে রেজিস্টার করবে না। কোড কম্পাইল হবে। ভিউ রেন্ডার হবে। কিন্তু কোনো বাইন্ডিং হবে না। কোনো এরর মেসেজ আসবে না, শুধু একটি কম্পোনেন্ট থাকবে যা সম্পূর্ণভাবে ফর্মের বাইরে ভাসতে থাকবে। সবসময় এটি ডেকোরেটর মেটাডেটাতে অন্তর্ভুক্ত করুন।
Signals, Validators, এবং Modern Angular
ControlValueAccessor কোনো লেগাসি (legacy) API নয়। এটি আধুনিক Angular ডেভেলপমেন্টের সাথে চমৎকারভাবে মানিয়ে যায়। আপনি আপনার ইন্টারনাল স্টেট Signals, সাধারণ প্রপার্টি বা RxJS subjects দিয়ে ম্যানেজ করুন না কেন, এই চারটি মেথডই ফর্ম মডিউলের সাথে আপনার পাবলিক কন্ট্রাক্ট হিসেবে কাজ করে। আপনি writeValue-তে ভ্যালু গ্রহণ করেন, আপনার Signals বা স্টেট পরিবর্তন করেন এবং Angular-এর দেওয়া কলব্যাকগুলোর মাধ্যমে তা emit করেন।
স্ট্যান্ডার্ড ভ্যালিডেটরগুলো কোনো পরিবর্তন ছাড়াই কাজ করে। Validators.required, Validators.min, Validators.pattern, এবং কাস্টম ক্রস-ফিল্ড ভ্যালিডেটরগুলো আপনার CVA-ব্যাকড কম্পোনেন্টকে ঠিক একটি নেটিভ ইনপুটের মতোই মূল্যায়ন করে। ফর্ম কন্ট্রোল একটি ভ্যালু এবং একটি স্টেট দেখে। সেই ভ্যালুটি একটি টেক্সট বক্স থেকে এসেছে নাকি হাতে তৈরি কোনো মান্থ-পিকার (month-picker) থেকে এসেছে, তাতে তার কিছু যায় আসে না।
এই পোর্টেবিলিটির কারণেই ডিজাইন সিস্টেম এবং শেয়ারড UI লাইব্রেরির জন্য CVA গুরুত্বপূর্ণ। একটি টিম একটি শক্তিশালী ফোন-নম্বর ইনপুট বা ফাইল আপলোড উইজেট তৈরি করে। তারা একবার ইন্টারফেসটি ইমপ্লিমেন্ট করে। এরপর প্রতিষ্ঠানের অন্য প্রতিটি টিম কোনো অতিরিক্ত ওয়্যারিং ছাড়াই তাদের Reactive Forms-এ এটি ব্যবহার করতে পারে। কম্পোনেন্টটি প্রতিটি ফিচার মডিউলে একইভাবে কাজ করে, একইভাবে ভ্যালিডেট করে এবং একইভাবে ডিজেবল হয়।
আসল শিক্ষা
ControlValueAccessor কেবল ইন্টারভিউয়ের প্রশ্নের জন্য মুখস্থ করার মতো কোনো ইন্টারফেস নয়। এটি একটি সেতু যা আপনার কাস্টম কম্পোনেন্টগুলোকে নেটিভ HTML এলিমেন্টের সমান হিসেবে Angular-এর ফর্ম ইকোসিস্টেমে অংশগ্রহণের সুযোগ দেয়। এটি আয়ত্ত করার অর্থ হলো আপনার উইজেট এবং ফর্মের মধ্যকার সম্পূর্ণ কথোপকথনটি বোঝা: ভ্যালু গ্রহণ করা, পরিবর্তন রিপোর্ট করা, টাচ (touch) জানানো এবং disabled স্টেট মেনে চলা। এই চারটি বিষয় সঠিকভাবে আয়ত্ত করতে পারলে আপনি এমন জটিল এবং পুনরায় ব্যবহারযোগ্য (reusable) ফর্ম কন্ট্রোল তৈরি করতে পারবেন যা ব্যবহারকারী ডেভেলপারদের কাছে একদম সাবলীল মনে হবে। এটাই একটি প্রফেশনাল Angular কম্পোনেন্টের বৈশিষ্ট্য।
