कस्टम एलिमेंट

वेब की सुविधाएं: ऑटोनॉमस कस्टम एलिमेंट

कस्टम एलिमेंट एक क्लास है, जिसे customElements.define() के ज़रिए टैग के नाम के साथ रजिस्टर किया जाता है. रजिस्ट्रेशन और बुनियादी लाइफ़साइकल को अच्छी तरह से समझा गया है. नीचे दिए गए नियम, उन हिस्सों को टारगेट करते हैं जिन्हें आम तौर पर गलत तरीके से लागू किया जाता है.

लाइफ़साइकल: वे हिस्से जिन्हें गलत तरीके से लागू किया जाता है

कॉलबैक के बारे में अच्छी तरह से बताया गया है. हालांकि, कुछ गलतफ़हमियों की वजह से गड़बड़ियां होती हैं:

  • connectedCallback को एक से ज़्यादा बार चलाया जा सकता है. यह हर बार एलिमेंट को शामिल करने पर ट्रिगर होता है. इसलिए, एलिमेंट को मूव करने पर यह फिर से ट्रिगर होता है. सेटअप को आइडेंपोटेंट बनाएं — यह न मानें कि सेटअप सिर्फ़ एक बार चलेगा.
  • connectedMoveCallback, किसी मूव के दौरान डिसकनेक्ट/रीकनेक्ट पेयर को स्किप कर देता है. जब किसी एलिमेंट को Node.moveBefore() की मदद से दूसरी जगह ले जाया जाता है, तो यह कॉलबैक disconnectedCallback → connectedCallback के बजाय ट्रिगर होता है. इससे एलिमेंट की स्थिति, फ़ोकस, और iframe/मीडिया प्लेबैक की जानकारी बनी रहती है. इसे तब तय करें, जब आपका सेटअप या टियरडाउन महंगा हो या फिर से चलाया जा रहा हो.
  • disconnectedCallback में टियर डाउन. ग्लोबल लिसनर, टाइमर, और ऑब्ज़र्वर को हटाएं या लीक करें. हालांकि, एलिमेंट को फिर से शामिल किया जा सकता है. इसलिए, टियरडाउन को स्थायी न मानें.
  • कंस्ट्रक्टर में सबसे पहले super() को कॉल करें. साथ ही, पूरे डीओएम के मौजूद होने पर निर्भर न रहें. this को छूने के बाद, super() को कॉल करने पर ReferenceError थ्रो होता है. आम तौर पर, एट्रिब्यूट मौजूद होते हैं, लेकिन हमेशा ऐसा नहीं होता. ऐसा हो सकता है कि चाइल्ड एलिमेंट की लंबी सूचियों को पार्स करने की प्रोसेस पूरी न हुई हो. एक बार में शुरू करने के लिए connectedCallback का इस्तेमाल करें. डायरेक्ट चाइल्ड को मॉनिटर करने के लिए slotchange और एट्रिब्यूट को मॉनिटर करने के लिए attributeChangedCallback का इस्तेमाल करें. सभी चाइल्ड एलिमेंट को पार्स करने से पहले, connectedCallback अब भी ट्रिगर हो सकता है. इसलिए, इसके बाद आने वाले slotchange / attributeChangedCallback कॉल को अनदेखा न करें.
  • attributeChangedCallback सिर्फ़ observedAttributes के लिए ट्रिगर होता है, और आउटपुट पर असर न डालने वाले एट्रिब्यूट के लिए, इसकी कीमत कम होनी चाहिए.
  • रजिस्ट्रेशन के समय, लाइफ़साइकल हुक और observedAttributes का स्नैपशॉट लिया जाता है. customElements.define() दोनों को एक बार पढ़ता है; कॉल के बाद क्लास में हुक जोड़ने या observedAttributes को बढ़ाने की प्रोसेस को अनदेखा कर दिया जाता है. खास तौर पर, attributeChangedCallback तब तक ट्रिगर नहीं होता, जब तक observedAttributes पहले से ही define() समय पर मौजूद न हो.

अपग्रेड करने का समय और :defined

कोई एलिमेंट, अपनी क्लास के रजिस्टर होने से पहले डीओएम में मौजूद हो सकता है. ऐसा सर्वर से रेंडर किए गए एचटीएमएल में या स्क्रिप्ट के देर से लोड होने पर होता है. जब तक इसे तय नहीं किया जाता, तब तक यह एक इनर्ट HTMLElement होता है, जिसमें आपका कोई व्यवहार नहीं होता. जब customElements.define() चलता है, तो ब्राउज़र दस्तावेज़ में पहले से मौजूद हर मैचिंग एलिमेंट को अपग्रेड करता है. साथ ही, इसके कंस्ट्रक्टर और connectedCallback को चलाता है.

मॉडल अक्सर दो चीज़ों को ध्यान में नहीं रखते:

  1. जब आपका कंस्ट्रक्टर/connectedCallback अपग्रेड पर चलता है, तब एट्रिब्यूट और चाइल्ड पहले से मौजूद हो सकते हैं. शुरुआती स्थिति को सिंक करने के लिए, connectedCallback में मौजूद एट्रिब्यूट पढ़ें. साथ ही, कभी भी यह न मानें कि एलिमेंट खाली है.
  2. ऐसा हो सकता है कि connectedCallback में भी चाइल्ड एलिमेंट मौजूद न हों — एचटीएमएल पार्सर, चाइल्ड एलिमेंट की लंबी सूचियों को पार्स करने के दौरान ही नतीजे दे सकता है. इसलिए, कभी-कभी हर चाइल्ड एलिमेंट के अटैच होने से पहले ही कॉलबैक चलता है. (slotchange + attributeChangedCallback) पैटर्न के लिए, ऊपर लाइफ़साइकल में कंस्ट्रक्टर बुलेट देखें.

बिना स्टाइल वाले कॉन्टेंट को फ़्लैश होने से रोकने के लिए, तय नहीं किए गए एलिमेंट छिपाएं. हालांकि, फ़ेल ओपन करें, ताकि स्क्रिप्ट लोड न होने पर भी कॉन्टेंट को ऐक्सेस किया जा सके. opacity को visibility: hidden के साथ छिपाने के बजाय, देरी से ऐनिमेट करें. इससे एलिमेंट, टाइम आउट के बाद खुद ही दिखने लगता है, भले ही उसे कभी अपग्रेड न किया गया हो:

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 पर लागू न करें. धीमी गति से काम करने वाले किसी ऐसे विजेट की वजह से पूरे पेज को रेंडर होने में देरी नहीं होनी चाहिए जो ज़रूरी न हो. जैसे, शेयर बटन.

एट्रिब्यूट ⇄ प्रॉपर्टी रिफ़्लेक्शन

एचटीएमएल एट्रिब्यूट और 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();
  }
}

वैल्यू कहां मौजूद होती है

ऊपर दिए गए उदाहरण में, डीओएम को सोर्स ऑफ़ ट्रुथ के तौर पर रखा गया है — हर गेटर getAttribute को पढ़ता है. यह कुछ आसान, स्ट्रिंग जैसे एट्रिब्यूट के लिए सही डिफ़ॉल्ट है: स्टेट सिर्फ़ एक जगह पर मौजूद हो सकता है. इसलिए, यह सिंक से बाहर नहीं हो सकता.

ज़्यादा बेहतर कॉम्पोनेंट के लिए, सोर्स ऑफ़ ट्रुथ को JS फ़ील्ड में रखें. साथ ही, इसे किसी एट्रिब्यूट में बाहर की ओर सिर्फ़ तब दिखाएं, जब किसी और को इसे देखना हो — सीएसएस टारगेटिंग या इसे पढ़ने वाला कोई उपभोक्ता. किसी फ़ील्ड को पढ़ना, हर बार ऐक्सेस करने पर किसी विशेषता को पार्स करने और उसका डेटा टाइप बदलने से कहीं ज़्यादा आसान है. साथ ही, नॉन-स्ट्रिंग वैल्यू कभी भी सीरियलाइज़ेशन के ज़रिए दोतरफ़ा यात्रा नहीं करती हैं. यही वजह है कि Lit जैसी लाइब्रेरी, JS में स्टेट को सेव रखती हैं और चुनिंदा तरीके से रिफ़्लेक्ट करती हैं. वे एट्रिब्यूट को स्टोरेज के तौर पर इस्तेमाल नहीं करतीं: सिर्फ़ वही रिफ़्लेक्ट करें जो बाहरी दुनिया को देखना चाहिए, हर प्रॉपर्टी को नहीं.

नुकसान:

  • observedAttributes अनुशासन: सिर्फ़ उन एट्रिब्यूट की सूची बनाएं जिनमें बदलाव होने पर, फिर से रेंडर करना या कोई खराब असर ट्रिगर करना ज़रूरी हो. हर चीज़ को मॉनिटर करने से, हर म्यूटेशन में एक कॉलबैक जुड़ जाता है. इससे कोई फ़ायदा नहीं होता.
  • रिफ़्लेक्शन लूप: एट्रिब्यूट की वैल्यू सेट करने वाला सेटर, attributeChangedCallback को फिर से कॉल करेगा. ऊपर बताए गए तरीके से, oldVal !== newVal की जांच करें.
  • सिर्फ़ स्टाइल वाली स्थिति: ऐसी इंटरनल स्थिति के लिए जो सिर्फ़ स्टाइल (लोड हो रहा है, चालू है, बड़ा किया गया है) को दिखाने के लिए मौजूद है, सिंथेटिक एट्रिब्यूट दिखाने के बजाय कस्टम स्थितियों को प्राथमिकता दें — नीचे कस्टम स्थितियां देखें. किसी एट्रिब्यूट को सिर्फ़ तब दिखाएं, जब उपभोक्ताओं को मार्कअप से स्थिति सेट करने की अनुमति हो.

कस्टम स्टेट

कस्टम स्टेट, बूलियन फ़्लैग होते हैं. ये किसी एलिमेंट के बारे में जानकारी देते हैं, ताकि सीएसएस उसे टारगेट कर सके. इन्हें ElementInternals की मदद से सेट किया जाता है और :state() स्यूडो-क्लास से मैच किया जाता है. सिंथेटिक एट्रिब्यूट दिखाने के बजाय, इंटरनल और रनटाइम यूज़र इंटरफ़ेस (यूआई) स्टेट के लिए इनका इस्तेमाल करें — busy, expanded, invalid — ये उपभोक्ता के एट्रिब्यूट से नहीं टकरा सकते, डीओएम में कभी नहीं दिखते, और बाहर से सिर्फ़ रीड-ओनली होते हैं (उपभोक्ता :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 एक सेट है. इसलिए, अलग-अलग स्टेटस (busy और error) को मिलाकर एक data-status enum बनाने की ज़रूरत नहीं होती.

कस्टम स्टेट सिर्फ़ रनटाइम के लिए है. इसे डीओएम में सीरियलाइज़ नहीं किया जाता. इसलिए, यह पेज को फिर से लोड करने, एलिमेंट को फिर से बनाने या सर्वर रेंडरिंग के बाद नहीं बचता. अगर स्थिति को मार्कअप में साफ़ तौर पर सेट करना ज़रूरी है या उसे लोड के दौरान बनाए रखना है, तो यह काम एट्रिब्यूट का है (वैल्यू कहां मौजूद है देखें) — इसे अपने सोर्स ऑफ़ ट्रुथ से 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 सिर्फ़ फ़ॉर्म कंट्रोल पर ग्लोबल होता है. अन्य एलिमेंट पर, आपको इसका असर खुद लागू करना होगा.

किसी नए एपीआई प्लैटफ़ॉर्म का नाम तय करते समय, प्लैटफ़ॉर्म के अपने कन्वेंशन का पालन करें: कस्टम एलिमेंट टैग के नामों के लिए, कम से कम एक हाइफ़न के साथ लोअरकेस केबाब-केस का इस्तेमाल करें (उदाहरण के लिए my-toggle), प्रॉपर्टी और तरीकों के लिए lowerCamelCase का इस्तेमाल करें, और लोअरकेस एट्रिब्यूट के नामों को उनकी संबंधित प्रॉपर्टी के साथ सिंक करें.