কাস্টম এলিমেন্ট হল এমন একটি ক্লাস যা customElements.define()-এর মাধ্যমে ট্যাগ নামের বিপরীতে রেজিস্টার করা হয়। রেজিস্ট্রেশন ও প্রাথমিক লাইফসাইকেল ভালভাবে বোঝা গেছে; নিচের নিয়মগুলি সেইসব অংশকে টার্গেট করে যেগুলি রুটিন অনুযায়ী ভুলভাবে প্রয়োগ করা হয়েছে।
লাইফসাইকেল: যে অংশগুলি ভুলভাবে প্রয়োগ করা হয়েছে
কলব্যাকগুলি নিজেরাই ভালভাবে ডকুমেন্ট করা হয়; এগুলি হল ভুল ধারণা যা বাগ সৃষ্টি করে:
connectedCallbackএকাধিকবার রান করতে পারে। এটি প্রতিটি ইনসার্শনে ফায়ার করে, তাই এলিমেন্টটি সরালে এটি আবার রান করে। সেট-আপ আইডেমপোটেন্ট করুন — প্রথমবার রান করার পরেই কাজ হয়ে যাবে বলে ধরে নেবেন না।connectedMoveCallbackচাল দেওয়ার সময় ডিসকানেক্ট/রিকানেক্ট পেয়ার এড়িয়ে যায়।Node.moveBefore()-এর মাধ্যমে কোনও এলিমেন্ট রিলোকেট করা হলে, এই কলব্যাকdisconnectedCallback→connectedCallback-এর পরিবর্তে ফায়ার করে, তাই স্টেট, ফোকাস এবং iframe/মিডিয়া প্লেব্যাক মুভ করার পরেও থেকে যায়। আপনার সেট-আপ বা টিয়ারডাউন ব্যয়বহুল হলে বা লক্ষণীয়ভাবে আবার রান করলে এটি নির্ধারণ করুন।disconnectedCallback-এ টিয়ার ডাউন। গ্লোবাল লিসেনার, টাইমার ও অবজারভার সরিয়ে দিন বা লিক করুন; তবে এলিমেন্ট আবার যোগ করা হতে পারে, তাই টিয়ারডাউনকে স্থায়ী হিসেবে বিবেচনা করবেন না।- কনস্ট্রাক্টরে প্রথমে
super()কল করুন এবং সম্পূর্ণ DOM উপস্থিত থাকার উপর নির্ভর করবেন না।thisস্পর্শ করার পরেsuper()কল করলেReferenceErrorথ্রো করে। অ্যাট্রিবিউট সাধারণত (কিন্তু সবসময় নয়) থাকে; চাইল্ডের দীর্ঘ তালিকা পার্স করা শেষ নাও হতে পারে। এক-শট ইনিশিয়ালাইজেশনের জন্যconnectedCallback, সরাসরি চাইল্ড মনিটর করার জন্যslotchangeএবং অ্যাট্রিবিউট মনিটর করার জন্যattributeChangedCallbackব্যবহার করুন। সব চিলড্রেন পার্স করার আগেconnectedCallbackএখনও ফায়ার করতে পারে, তাই পরবর্তীslotchange/attributeChangedCallbackকল উপেক্ষা করবেন না। attributeChangedCallbackশুধুমাত্রobservedAttributes-এর জন্য ফায়ার করে, এবং আউটপুটে প্রভাব ফেলে না এমন অ্যাট্রিবিউটের জন্য সস্তা থাকা উচিত।- রেজিস্ট্রেশনের সময় লাইফসাইকেল হুক ও
observedAttributes-এর স্ন্যাপশট নেওয়া হয়।customElements.define()দুটিই একবার করে পড়ে; কল করার পরে ক্লাসে হুক যোগ করা বাobservedAttributesএক্সটেন্ড করাকে নীরবে উপেক্ষা করা হয়। বিশেষ করে,attributeChangedCallbackকখনই ফায়ার করে না যদি নাobservedAttributesআগে থেকেইdefine()সময়ে উপস্থিত থাকে।
আপগ্রেড করার সময় এবং :defined
কোনও এলিমেন্ট DOM-এ তার ক্লাস রেজিস্টার হওয়ার আগে থাকতে পারে (সার্ভার-রেন্ডার করা HTML-এ অথবা স্ক্রিপ্ট দেরিতে লোড হলে)। এটি সংজ্ঞায়িত না করা পর্যন্ত, এটি একটি নিষ্ক্রিয় HTMLElement যার কোনও আচরণ নেই; যখন customElements.define() রান করে, তখন ব্রাউজার ডকুমেন্টে আগে থেকেই থাকা প্রতিটি ম্যাচিং এলিমেন্ট আপগ্রেড করে, এর কনস্ট্রাক্টর এবং তারপর connectedCallback রান করে।
দুটি ফলাফল মডেল সাধারণত মিস করে:
- আপগ্রেড করার সময় আপনার কনস্ট্রাক্টর/
connectedCallbackরান করলে, অ্যাট্রিবিউট ও চিলড্রেন আগে থেকেই উপস্থিত থাকতে পারে। প্রাথমিক অবস্থা সিঙ্ক করতেconnectedCallback-এ আগে থেকে থাকা অ্যাট্রিবিউট পড়ুন এবং কখনই ধরে নেবেন না যে এলিমেন্টটি ফাঁকা শুরু হয়েছে। connectedCallback-এও চাইল্ড এখনও মিসিং থাকতে পারে — HTML পার্সার দীর্ঘ চাইল্ড তালিকার মিড-পার্স তৈরি করতে পারে, তাই কখনও কখনও প্রতিটি চাইল্ড অ্যাটাচ করার আগে কলব্যাক রান করে। প্যাটার্নের (slotchange+attributeChangedCallback) জন্য উপরে লাইফসাইকেল-এ কনস্ট্রাক্টর বুলেট দেখুন।
স্টাইল করা হয়নি এমন কন্টেন্ট যাতে হঠাৎ করে দেখা না যায়, তার জন্য নির্দিষ্ট করা হয়নি এমন এলিমেন্ট লুকান — তবে ফেল ওপেন, যাতে স্ক্রিপ্ট কখনও লোড না হলেও কন্টেন্ট অ্যাক্সেস করা যায়। visibility: hidden-এর মাধ্যমে হার্ড-হাইড করার পরিবর্তে, দেরি করে opacity অ্যানিমেট করুন, যাতে টাইমআউটের পরে এলিমেন্টটি নিজেকে প্রকাশ করে, এমনকি এটি কখনও আপগ্রেড না হলেও:
my-element:not(:defined) {
opacity: 0;
/* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }
এইসব গার্ডকে সেইসব এলিমেন্টে স্কোপ করুন যেগুলিতে এগুলি প্রয়োজন — অথবা :has(:not(:defined))-এর মাধ্যমে নির্দিষ্ট সাবট্রি — সম্পূর্ণ body-এ নয়। একটি স্লো, অপ্রয়োজনীয় উইজেটের (যেমন, শেয়ার বোতাম) জন্য পুরো পৃষ্ঠার পেইন্ট প্রসেস যেন স্লো না হয়।
অ্যাট্রিবিউট ⇄ প্রপার্টি রিফ্লেকশন
HTML অ্যাট্রিবিউট এবং JS প্রপার্টি সিঙ্ক করে রাখুন যাতে এলিমেন্টটি মার্কআপ এবং স্ক্রিপ্ট, দুটির মাধ্যমেই ব্যবহার করা যায়। অ্যাট্রিবিউট ভ্যালু সবসময় স্ট্রিং হয়; সেগুলি জোর করে প্রয়োগ করুন।
class MyToggle extends HTMLElement {
static observedAttributes = ['label', 'count', 'disabled'];
// String attribute reflected via getter/setter.
get label() { return this.getAttribute('label') ?? ''; }
set label(v) {
v ? this.setAttribute('label', v) : this.removeAttribute('label');
}
// Numeric attribute: coerce from string on read, serialize on write.
get count() { return Number(this.getAttribute('count')) || 0; }
set count(v) { this.setAttribute('count', String(v)); }
// Boolean attribute: present = true, absent = false. Never set it to "false".
get disabled() { return this.hasAttribute('disabled'); }
set disabled(on) { this.toggleAttribute('disabled', !!on); }
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
}
যেখানে ভ্যালু থাকে
উপরের উদাহরণে DOM-কে সোর্স অফ ট্রুথ হিসেবে রাখা হয়েছে — প্রতিটি গেটার getAttribute পড়ে। এটি হল কয়েকটি সহজ, স্ট্রিং-এর মতো অ্যাট্রিবিউটের জন্য সঠিক ডিফল্ট: স্ট্যাটাস শুধুমাত্র একটি জায়গাতেই থাকতে পারে, তাই এটি ডিসিঙ্ক হতে পারে না।
আরও সমৃদ্ধ কম্পোনেন্টের জন্য, JS ফিল্ডে সোর্স অফ ট্রুথ রাখুন এবং শুধুমাত্র তখনই অ্যাট্রিবিউটে আউটওয়ার্ড দেখান যখন অন্য কিছুকে এটি দেখতেই হবে — CSS টার্গেটিং বা কোনও গ্রাহক এটি পড়ছেন। প্রতিটি অ্যাক্সেসে অ্যাট্রিবিউট পার্স ও জোর করার চেয়ে ফিল্ড পড়া অনেক সস্তা এবং নন-স্ট্রিং ভ্যালু কখনও সিরিয়ালাইজেশনের মাধ্যমে রাউন্ড-ট্রিপ করে না। এই কারণেই Lit-এর মতো লাইব্রেরি JS-এ স্টেট ধরে রাখে এবং অ্যাট্রিবিউটকে স্টোরেজ হিসেবে বিবেচনা করার পরিবর্তে বেছে বেছে দেখায়: শুধুমাত্র বাইরের বিশ্বকে যা পর্যবেক্ষণ করতে হবে তা দেখায়, প্রতিটি প্রপার্টি নয়।
সমস্যা:
observedAttributesডিসিপ্লিন: শুধুমাত্র সেইসব অ্যাট্রিবিউটের তালিকা তৈরি করুন যেগুলি পরিবর্তন করলে অবশ্যই রি-রেন্ডার করতে হবে বা সাইড এফেক্ট ট্রিগার করতে হবে। সবকিছু পর্যবেক্ষণ করলে, কোনও সুবিধা ছাড়াই প্রতিটি মিউটেশনে একটি কলব্যাক যোগ করা হয়।- রিফ্লেকশন লুপ: অ্যাট্রিবিউট লেখে এমন সেটার
attributeChangedCallback-এ আবার প্রবেশ করবে। উপরে যেমন দেখানো হয়েছে,oldVal !== newValচেক দিয়ে সুরক্ষিত করুন। - শুধুমাত্র স্টাইল স্টেট: ইন্টার্নাল স্টেটের জন্য যা শুধুমাত্র স্টাইলিং (লোডিং, অ্যাক্টিভ, এক্সপ্যান্ডেড) চালানোর জন্য বিদ্যমান, সিন্থেটিক অ্যাট্রিবিউট প্রতিফলিত করার পরিবর্তে কাস্টম স্টেট পছন্দ করুন — নিচে কাস্টম স্টেট দেখুন। গ্রাহকরা মার্কআপ থেকে স্ট্যাটাস সেট করতে পারলে তবেই অ্যাট্রিবিউটে প্রতিফলিত করুন।
কাস্টম স্টেট
কাস্টম স্টেট হল বুলিয়ান ফ্ল্যাগ যা কোনও এলিমেন্ট নিজের সম্পর্কে প্রকাশ করে যাতে CSS টার্গেট করতে পারে, ElementInternals-এর মাধ্যমে সেট করা হয় এবং :state() ছদ্ম-ক্লাসের সাথে ম্যাচ করে। সিন্থেটিক অ্যাট্রিবিউট দেখানোর পরিবর্তে ইন্টার্নাল, রানটাইম UI স্টেট — busy, expanded, invalid — এর জন্য এগুলি ব্যবহার করুন: এগুলি গ্রাহকের অ্যাট্রিবিউটের সাথে ওভারল্যাপ করতে পারে না, DOM-এ কখনও দেখা যায় না এবং বাইরে থেকে শুধু পড়া যায় (গ্রাহক স্টাইল :state() করতে পারেন কিন্তু জাল করতে পারেন না)।
class AsyncButton extends HTMLElement {
#internals;
#action = async () => {};
constructor() {
super();
this.#internals = this.attachInternals();
}
set action(fn) { this.#action = fn; } // host supplies the work to run
async run() {
if (this.#internals.states.has('busy')) return; // guard re-entry
this.#internals.states.add('busy');
this.#internals.states.delete('success');
try {
await this.#action();
this.#internals.states.add('success');
} catch {
this.#internals.states.add('error');
} finally {
this.#internals.states.delete('busy');
}
}
}
CustomStateSet হল একটি সেট, তাই স্বাধীন স্টেটগুলি একটি সম্মিলিত data-status enum আবিষ্কার না করেই কম্পোজ (busy প্লাস error) করে।
কাস্টম স্টেট শুধুমাত্র রানটাইম। এটি DOM-এ সিরিয়ালাইজ করা হয় না, তাই এটি রিলোড, এলিমেন্ট আবার তৈরি করা বা সার্ভার রেন্ডারিংয়ের পরেও থেকে যায় না। যদি স্টেটটি অবশ্যই মার্কআপে ঘোষণামূলকভাবে সেট করা থাকে বা লোড জুড়ে থাকে, তাহলে সেটি একটি অ্যাট্রিবিউট-এর কাজ (ভ্যালু কোথায় থাকে দেখুন) — আপনার সোর্স অফ ট্রুথ থেকে এটি connectedCallback-এ রিস্টোর করুন। সাধারণ নিয়ম: রানটাইমে কম্পোনেন্ট যা থেকে উদ্ভূত হয় এবং যার মালিকানা থাকে এবং শুধুমাত্র স্টাইল করার প্রয়োজন হয় তার জন্য কাস্টম স্টেট; মার্কআপ বা সিরিয়ালাইজড থেকে যা সেট করতে হবে তার জন্য একটি অ্যাট্রিবিউট।
এইসব স্টেট স্টাইল করার জন্য — কম্পোনেন্টের ভিতরে এবং গ্রাহকের স্টাইলশিট থেকে — ওয়েব কম্পোনেন্ট স্টাইল করা দেখুন।
নামকরণ কনভেনশন
- True-private state: native
#fields (#count) for hard encapsulation; a_prefix only when subclasses must still reach it. মনে রাখবেন,#ফিল্ড ইনহেরিট করা হয় না — সাবক্লাস থেকে মূল ক্লাসের ব্যক্তিগত ফিল্ড দেখা যায় না। - সিম্বল-কীড প্রপার্টি হল মধ্যবর্তী অবস্থা: সাধারণ গণনা থেকে লুকানো, কিন্তু সিম্বল ধারণকারী যেকোনও কোডের মাধ্যমে অ্যাক্সেসযোগ্য (এটি এক্সপোর্ট করুন বা ক্লাসে
staticফিল্ড হিসেবে প্রকাশ করুন)। যেহেতু প্রতিটি প্রতীক অনন্য, তাই একই বিবরণ শেয়ার করা দুটি প্রতীক কখনই একে অপরের সাথে ওভারল্যাপ করে না — এটি তখন কাজে লাগে যখন কোনও সাবক্লাস বা সিবলিংয়ের জন্য নিয়ন্ত্রিত অ্যাক্সেসের প্রয়োজন হয় যা#ফিল্ড প্রদান করতে পারে না। - আপনার নিজস্ব প্রপার্টি দিয়ে গ্লোবাল অ্যাট্রিবিউট (
style,class,id,slot,part,title,lang,dir) শ্যাডো করবেন না; এটি করলে তাদের বিল্ট-ইন আচরণ ভেঙে যায়। মনে রাখবেন,disabledশুধুমাত্র ফর্ম কন্ট্রোলের ক্ষেত্রে গ্লোবাল; অন্যান্য এলিমেন্টের ক্ষেত্রে আপনাকে নিজে এর এফেক্ট প্রয়োগ করতে হবে।
নতুন API সারফেসের নাম দেওয়ার সময়, প্ল্যাটফর্মের নিজস্ব কনভেনশন অনুসরণ করুন: কাস্টম এলিমেন্ট ট্যাগ নামের জন্য অন্তত একটি হাইফেন সহ ছোট হাতের কাবাব-কেস ব্যবহার করুন (যেমন my-toggle), প্রপার্টি ও পদ্ধতির জন্য lowerCamelCase এবং সংশ্লিষ্ট প্রপার্টির সাথে সিঙ্ক করা ছোট হাতের অ্যাট্রিবিউট নাম ব্যবহার করুন।