डेक्लेरेटिव शैडो DOM, वेब प्लैटफ़ॉर्म की स्टैंडर्ड सुविधा है. यह सुविधा, Chrome के 90 वर्शन से काम करती है. ध्यान दें कि इस सुविधा की खास बातें 2023 में बदल गई हैं. इसमें shadowroot
को shadowrootmode
में बदलना भी शामिल है. साथ ही, इस सुविधा के सभी हिस्सों के सबसे अप-टू-डेट स्टैंडर्ड वर्शन, Chrome के 124 वर्शन में लॉन्च किए गए हैं.
शैडो डीओएम, वेब कॉम्पोनेंट के तीन स्टैंडर्ड में से एक है. बाकी दो स्टैंडर्ड एचटीएमएल टेंप्लेट और कस्टम एलिमेंट हैं. शैडो डीओएम की मदद से, सीएसएस स्टाइल को किसी खास डीओएम सबट्री तक सीमित किया जा सकता है. साथ ही, उस सबट्री को दस्तावेज़ के बाकी हिस्से से अलग किया जा सकता है. <slot>
एलिमेंट की मदद से, यह कंट्रोल किया जा सकता है कि कस्टम एलिमेंट के चाइल्ड एलिमेंट को उसके शैडो ट्री में कहां डाला जाए. इन सुविधाओं की मदद से, ऐसे सिस्टम बनाए जा सकते हैं जो अपने-आप काम करने वाले और फिर से इस्तेमाल किए जा सकने वाले कॉम्पोनेंट बनाते हैं. ये कॉम्पोनेंट, पहले से मौजूद ऐप्लिकेशन में आसानी से इंटिग्रेट हो जाते हैं, ठीक उसी तरह जैसे पहले से मौजूद एचटीएमएल एलिमेंट इंटिग्रेट होते हैं.
अब तक, शैडो DOM का इस्तेमाल करने का सिर्फ़ एक तरीका था. इसके लिए, JavaScript का इस्तेमाल करके शैडो रूट बनाया जाता था:
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
इस तरह का ज़रूरी एपीआई, क्लाइंट-साइड रेंडरिंग के लिए ठीक से काम करता है: हमारे कस्टम एलिमेंट तय करने वाले वही JavaScript मॉड्यूल, उनके शैडो रूट भी बनाते हैं और उनका कॉन्टेंट सेट करते हैं. हालांकि, कई वेब ऐप्लिकेशन को बिल्ड के समय, कॉन्टेंट को सर्वर साइड या स्टैटिक एचटीएमएल में रेंडर करना पड़ता है. यह उन वेबसाइट पर आने वाले लोगों को बेहतर अनुभव देने का एक अहम हिस्सा हो सकता है जो JavaScript को चलाने में सक्षम नहीं हैं.
सर्वर साइड रेंडरिंग (एसएसआर) के लिए, हर प्रोजेक्ट के हिसाब से अलग-अलग वजहें होती हैं. सुलभता से जुड़े दिशा-निर्देशों का पालन करने के लिए, कुछ वेबसाइटों को सर्वर से रेंडर किया गया एचटीएमएल उपलब्ध कराना ज़रूरी है. वहीं, कुछ वेबसाइटें बिना JavaScript के बेसलाइन अनुभव देने का विकल्प चुनती हैं, ताकि वे धीमे कनेक्शन या डिवाइसों पर अच्छी परफ़ॉर्मेंस को पक्का कर सकें.
पहले, सर्वर-साइड रेंडरिंग के साथ शैडो DOM का इस्तेमाल करना मुश्किल था. इसकी वजह यह थी कि सर्वर से जनरेट किए गए एचटीएमएल में, शैडो रूट को दिखाने का कोई तरीका पहले से मौजूद नहीं था. जिन डीओएम एलिमेंट को पहले से ही बिना शैडो रूट के रेंडर किया जा चुका है उनमें शैडो रूट जोड़ने पर भी परफ़ॉर्मेंस पर असर पड़ता है. इस वजह से, पेज लोड होने के बाद लेआउट में बदलाव हो सकता है. इसके अलावा, शैडो रूट की स्टाइलशीट लोड करते समय, कुछ समय के लिए स्टाइल न किए गए कॉन्टेंट ("FOUC") का फ़्लैश दिख सकता है.
एलान वाला शैडो डीओएम (डीएसडी), इस सीमा को हटा देता है. साथ ही, शैडो डीओएम को सर्वर पर ले जाता है.
डिक्लेरेटिव शैडो रूट बनाने का तरीका
डिक्लेरेटिव शैडो रूट, shadowrootmode
एट्रिब्यूट वाला <template>
एलिमेंट होता है:
<host-element>
<template shadowrootmode="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
एचटीएमएल पार्सर, shadowrootmode
एट्रिब्यूट वाले टेंप्लेट एलिमेंट का पता लगाता है और उसे अपने पैरंट एलिमेंट के शैडो रूट के तौर पर तुरंत लागू कर देता है. ऊपर दिए गए सैंपल से, सिर्फ़ एचटीएमएल मार्कअप लोड करने पर, यह डीओएम ट्री दिखता है:
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
यह कोड सैंपल, शैडो डीओएम कॉन्टेंट दिखाने के लिए, Chrome DevTools के एलिमेंट पैनल के नियमों का पालन कर रहा है. उदाहरण के लिए, ↳
वर्ण, स्लॉट किए गए लाइट डीओएम कॉन्टेंट को दिखाता है.
इससे हमें स्टैटिक एचटीएमएल में, शैडो DOM के कंटेनमेंट और स्लॉट प्रोजेक्शन के फ़ायदे मिलते हैं. पूरे ट्री को जनरेट करने के लिए, शैडो रूट के साथ-साथ JavaScript की ज़रूरत नहीं होती.
कस्टम एलिमेंट और मौजूदा शैडो रूट का पता लगाना
डिक्लेरेटिव शैडो DOM का इस्तेमाल, स्टाइल को शामिल करने या चाइल्ड एलिमेंट के प्लेसमेंट को पसंद के मुताबिक बनाने के लिए किया जा सकता है. हालांकि, कस्टम एलिमेंट के साथ इस्तेमाल करने पर, यह सबसे असरदार होता है. कस्टम एलिमेंट का इस्तेमाल करके बनाए गए कॉम्पोनेंट, स्टैटिक एचटीएमएल से अपने-आप अपग्रेड हो जाते हैं. डिक्लेरेटिव शैडो डीओएम के आने के बाद, अब कस्टम एलिमेंट को अपग्रेड होने से पहले शैडो रूट दिया जा सकता है.
कस्टम एलिमेंट का इस्तेमाल पिछले कुछ समय से किया जा रहा है. अब तक, attachShadow()
का इस्तेमाल करके नया शैडो रूट बनाने से पहले, मौजूदा शैडो रूट की जांच करने की ज़रूरत नहीं थी. डिक्लेरेटिव शैडो DOM में एक छोटा बदलाव किया गया है, जिसकी वजह से मौजूदा कॉम्पोनेंट काम करते रहेंगे: किसी एलिमेंट पर attachShadow()
मेथड को कॉल करने पर, मौजूदा डिक्लेरेटिव शैडो रूट से कोई गड़बड़ी नहीं होगी. इसके बजाय, डिक्लेरेटिव शैडो रूट को खाली कर दिया जाता है और उसे वापस लाया जाता है. इससे, ऐसे पुराने कॉम्पोनेंट काम करते रहेंगे जिन्हें डिक्लेरेटिव शैडो DOM के लिए नहीं बनाया गया है. ऐसा इसलिए होता है, क्योंकि डिक्लेरेटिव रूट तब तक सुरक्षित रखे जाते हैं, जब तक कोई ज़रूरी बदलाव नहीं किया जाता.
हाल ही में बनाए गए कस्टम एलिमेंट के लिए, ElementInternals.shadowRoot प्रॉपर्टी की मदद से, एलिमेंट के मौजूदा डेक्लेरेटिव शैडो रूट का रेफ़रंस पाया जा सकता है. यह रेफ़रंस, ओपन और क्लोज़्ड, दोनों तरह के शैडो रूट के लिए उपलब्ध होता है. इसका इस्तेमाल, किसी भी डिक्लेरेटिव शैडो रूट की जांच करने और उसका इस्तेमाल करने के लिए किया जा सकता है. साथ ही, जहां कोई शैडो रूट नहीं दिया गया है वहां attachShadow()
पर वापस आ जाता है.
कॉम्पोनेंट हाइड्रेशन
एचटीएमएल से अपग्रेड किए जा रहे कस्टम एलिमेंट में, पहले से ही एलिमेंट का शेडो रूट जुड़ा होगा. इसका मतलब है कि एलिमेंट के ElementInternals
के इंस्टैंशिएट होने पर, उसमें shadowRoot
प्रॉपर्टी पहले से मौजूद होगी. इसके लिए, आपके कोड को साफ़ तौर पर कोई प्रॉपर्टी बनाने की ज़रूरत नहीं होगी. अपने एलिमेंट के कन्स्ट्रक्टर में मौजूद किसी भी मौजूदा शैडो रूट के लिए, ElementInternals.shadowRoot
देखना सबसे अच्छा है. अगर पहले से कोई वैल्यू मौजूद है, तो इस कॉम्पोनेंट के एचटीएमएल में, डिक्लेरेटिव शैडो रूट शामिल होता है. अगर वैल्यू शून्य है, तो इसका मतलब है कि एचटीएमएल में कोई डिक्लेरेटिव शैडो रूट मौजूद नहीं है या ब्राउज़र, डिक्लेरेटिव शैडो DOM के साथ काम नहीं करता.
<menu-toggle>
<template shadowrootmode="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
<script>
class MenuToggle extends HTMLElement {
constructor() {
super();
const supportsDeclarative = HTMLElement.prototype.hasOwnProperty("attachInternals");
const internals = supportsDeclarative ? this.attachInternals() : undefined;
const toggle = () => {
console.log("menu toggled!");
};
// check for a Declarative Shadow Root.
let shadow = internals?.shadowRoot;
if (!shadow) {
// there wasn't one. create a new Shadow Root:
shadow = this.attachShadow({
mode: "open",
});
shadow.innerHTML = `<button><slot></slot></button>`;
}
// in either case, wire up our event listener:
shadow.firstElementChild.addEventListener("click", toggle);
}
}
customElements.define("menu-toggle", MenuToggle);
</script>
हर रूट के लिए एक शैडो
डिक्लेरेटिव शैडो रूट सिर्फ़ अपने पैरंट एलिमेंट से जुड़ा होता है. इसका मतलब है कि शैडो रूट हमेशा अपने एलिमेंट के साथ मौजूद होते हैं. डिज़ाइन के इस फ़ैसले से यह पक्का होता है कि शैडो रूट, एचटीएमएल दस्तावेज़ के बाकी हिस्सों की तरह ही स्ट्रीम किए जा सकते हैं. यह एलिमेंट बनाने और जनरेट करने के लिए भी सुविधाजनक है. ऐसा इसलिए है, क्योंकि किसी एलिमेंट में शैडो रूट जोड़ने के लिए, मौजूदा शैडो रूट की रजिस्ट्री को बनाए रखने की ज़रूरत नहीं होती.
शैडो रूट को उनके पैरंट एलिमेंट से जोड़ने का नुकसान यह है कि एक ही डिक्लेरेटिव शैडो रूट <template>
से कई एलिमेंट को शुरू नहीं किया जा सकता. हालांकि, ज़्यादातर मामलों में ऐसा होने की संभावना नहीं होती है, जहां डिक्लेरेटिव शैडो डीओएम का इस्तेमाल किया जाता है. ऐसा इसलिए, क्योंकि हर शैडो रूट का कॉन्टेंट शायद ही एक जैसा हो. सर्वर से रेंडर किए गए एचटीएमएल में, अक्सर एलिमेंट के दोहराए गए स्ट्रक्चर होते हैं. हालांकि, आम तौर पर उनका कॉन्टेंट अलग-अलग होता है. उदाहरण के लिए, टेक्स्ट या एट्रिब्यूट में थोड़े-बहुत अंतर. सीरियलाइज़ किए गए डिक्लेरेटिव शैडो रूट के कॉन्टेंट पूरी तरह से स्टैटिक होते हैं. इसलिए, एक ही डिक्लेरेटिव शैडो रूट से कई एलिमेंट को अपग्रेड करने की सुविधा सिर्फ़ तब काम करती है, जब एलिमेंट एक जैसे हों. आखिर में, कंप्रेस करने की वजह से, नेटवर्क ट्रांसफ़र के साइज़ पर, बार-बार एक जैसे शैडो रूट का असर काफ़ी कम होता है.
आने वाले समय में, शेयर किए गए शैडो रूट पर फिर से जाया जा सकता है. अगर DOM में टेंप्लेट बनाने की सुविधा काम करती है, तो एलिमेंट के लिए शैडो रूट बनाने के लिए, एलिमेंट के एलान वाले शैडो रूट को टेंप्लेट के तौर पर इस्तेमाल किया जा सकता है. फ़िलहाल, डेक्लेरेटिव शैडो डीओएम के डिज़ाइन में, शैडो रूट को सिर्फ़ एक एलिमेंट से जोड़ा जा सकता है. इससे, आने वाले समय में इस सुविधा को इस्तेमाल किया जा सकता है.
स्ट्रीमिंग करना अच्छा है
डिक्लेरेटिव शैडो रूट को सीधे उनके पैरंट एलिमेंट से जोड़ने पर, उन्हें अपग्रेड करने और उस एलिमेंट से अटैच करने की प्रोसेस आसान हो जाती है. एलान वाले शैडो रूट का पता, एचटीएमएल पार्स करने के दौरान चलता है. साथ ही, उनके ओपनिंग <template>
टैग का पता चलने पर, उन्हें तुरंत अटैच कर दिया जाता है. <template>
में पार्स किया गया एचटीएमएल, सीधे शैडो रूट में पार्स किया जाता है, ताकि उसे "स्ट्रीम" किया जा सके: इसे मिलने के साथ ही रेंडर किया जाता है.
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
सिर्फ़ पार्सर
एलान वाला शैडो डीओएम, एचटीएमएल पार्स करने वाले टूल की एक सुविधा है. इसका मतलब है कि एलान वाला शैडो रूट, सिर्फ़ उन <template>
टैग के लिए पार्स और अटैच किया जाएगा जिनमें shadowrootmode
एट्रिब्यूट मौजूद है और जो एचटीएमएल पार्स करने के दौरान मौजूद हैं. दूसरे शब्दों में, एचटीएमएल को शुरुआती तौर पर पार्स करते समय, एलान वाले शैडो रूट बनाए जा सकते हैं:
<some-element>
<template shadowrootmode="open">
shadow root content for some-element
</template>
</some-element>
<template>
एलिमेंट का shadowrootmode
एट्रिब्यूट सेट करने से कुछ नहीं होता और टेंप्लेट एक सामान्य टेंप्लेट एलिमेंट बना रहता है:
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null
सुरक्षा से जुड़ी कुछ अहम बातों से बचने के लिए, innerHTML
या insertAdjacentHTML()
जैसे फ़्रैगमेंट पार्स करने वाले एपीआई का इस्तेमाल करके, एलान वाले शैडो रूट भी नहीं बनाए जा सकते. एलान वाले शैडो रूट लागू होने पर, एचटीएमएल को पार्स करने का एक ही तरीका है. इसके लिए, setHTMLUnsafe()
या parseHTMLUnsafe()
का इस्तेमाल करें:
<script>
const html = `
<div>
<template shadowrootmode="open"></template>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html; // No shadow root here
div.setHTMLUnsafe(html); // Shadow roots included
const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>
स्टाइल के साथ सर्वर-रेंडरिंग
इनलाइन और बाहरी स्टाइलशीट, स्टैंडर्ड <style>
और <link>
टैग का इस्तेमाल करके, डिक्लेरेटिव शैडो रूट में पूरी तरह से काम करती हैं:
<nineties-button>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>
इस तरह से तय की गई स्टाइल भी काफ़ी ऑप्टिमाइज़ की जाती हैं: अगर एक ही स्टाइल शीट, कई डिक्लेरेटिव शैडो रूट में मौजूद है, तो उसे सिर्फ़ एक बार लोड और पार्स किया जाता है. ब्राउज़र, एक बैकिंग CSSStyleSheet
का इस्तेमाल करता है, जिसे सभी शैडो रूट शेयर करते हैं. इससे डुप्लीकेट मेमोरी ओवरहेड की समस्या खत्म हो जाती है.
बनाई जा सकने वाली स्टाइलशीट, डिक्लेरेटिव शैडो DOM में काम नहीं करती हैं. ऐसा इसलिए है, क्योंकि फ़िलहाल एचटीएमएल में, कॉन्स्ट्रक्ट की जा सकने वाली स्टाइलशीट को सीरियलाइज़ करने का कोई तरीका नहीं है. साथ ही, adoptedStyleSheets
को पॉप्युलेट करते समय उनका रेफ़रंस देने का कोई तरीका भी नहीं है.
स्टाइल न किए गए कॉन्टेंट को फ़्लैश होने से रोकने का तरीका
जिन ब्राउज़र में अब तक डिक्लेरेटिव शैडो डीओएम काम नहीं करता उनमें "बिना स्टाइल वाले कॉन्टेंट का फ़्लैश" (एफ़ओयूसी) से बचने की एक समस्या है. इसमें, उन कस्टम एलिमेंट के लिए रॉ कॉन्टेंट दिखाया जाता है जिन्हें अब तक अपग्रेड नहीं किया गया है. डिक्लेरेटिव शैडो DOM से पहले, फ़्रीज़ ऑन यूज़र ऐक्शन (एफ़ओयूसी) से बचने के लिए, एक आम तकनीक का इस्तेमाल किया जाता था. इसके तहत, उन कस्टम एलिमेंट पर display:none
स्टाइल नियम लागू किया जाता था जो अभी तक लोड नहीं हुए थे. ऐसा इसलिए किया जाता था, क्योंकि इनमें शैडो रूट अटैच और पॉप्युलेट नहीं किया गया था. इस तरह, कॉन्टेंट तब तक नहीं दिखाया जाता, जब तक वह "तैयार" नहीं हो जाता:
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
एलान वाले शैडो डीओएम के आने से, कस्टम एलिमेंट को एचटीएमएल में रेंडर या लिखा जा सकता है, ताकि क्लाइंट-साइड कॉम्पोनेंट लागू होने से पहले ही उनका शैडो कॉन्टेंट पहले से मौजूद और तैयार हो:
<x-foo>
<template shadowrootmode="open">
<style>h2 { color: blue; }</style>
<h2>shadow content</h2>
</template>
</x-foo>
इस मामले में, display:none
"FOUC" नियम, डिक्लेरेटिव शैडो रूट के कॉन्टेंट को दिखने से रोक देगा. हालांकि, उस नियम को हटाने पर, ऐसे ब्राउज़र में गलत या बिना स्टाइल वाला कॉन्टेंट दिखेगा जिनमें Declarative Shadow DOM की सुविधा काम नहीं करती. ऐसा तब तक होगा, जब तक Declarative Shadow DOM polyfill लोड नहीं हो जाता और शैडो रूट टेंप्लेट को असली शैडो रूट में बदल नहीं देता.
फ़िलहाल, सीएसएस में जाकर, फ़्लाश ऑन यूआरएल क्लिक (एफ़ओयूसी) स्टाइल नियम में बदलाव करके, इस समस्या को हल किया जा सकता है. जिन ब्राउज़र में डिक्लेरेटिव शैडो डीओएम की सुविधा काम करती है उनमें <template shadowrootmode>
एलिमेंट को तुरंत शैडो रूट में बदल दिया जाता है. इससे, डीओएम ट्री में कोई <template>
एलिमेंट नहीं बचता. जो ब्राउज़र, डिक्लेरेटिव शैडो डीओएम के साथ काम नहीं करते वे <template>
एलिमेंट को सेव रखते हैं. इसका इस्तेमाल, फ़्रीक्वेंटली अपडेट होने वाले कॉन्टेंट (एफ़ओयूसी) को रोकने के लिए किया जा सकता है:
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
बदले गए "एफ़ओयूसी" नियम में, अभी तक तय नहीं किए गए कस्टम एलिमेंट को छिपाने के बजाय, <template shadowrootmode>
एलिमेंट के बाद आने वाले चाइल्ड एलिमेंट को छिपाया जाता है. कस्टम एलिमेंट तय करने के बाद, नियम मैच नहीं होता. एलान वाले शैडो डीओएम के साथ काम करने वाले ब्राउज़र में, इस नियम को अनदेखा किया जाता है. ऐसा इसलिए होता है, क्योंकि एचटीएमएल पार्स करने के दौरान <template shadowrootmode>
चाइल्ड को हटा दिया जाता है.
सुविधा की पहचान और ब्राउज़र के साथ काम करने की सुविधा
एलान वाला शैडो DOM, Chrome 90 और Edge 91 से उपलब्ध है. हालांकि, इसमें स्टैंडर्ड shadowrootmode
एट्रिब्यूट के बजाय, shadowroot
नाम के पुराने नॉन-स्टैंडर्ड एट्रिब्यूट का इस्तेमाल किया गया था. नया shadowrootmode
एट्रिब्यूट और स्ट्रीमिंग का तरीका, Chrome 111 और Edge 111 में उपलब्ध है.
नए वेब प्लैटफ़ॉर्म एपीआई के तौर पर, Declarative Shadow DOM अभी सभी ब्राउज़र पर काम नहीं करता. HTMLTemplateElement
के प्रोटोटाइप पर shadowRootMode
प्रॉपर्टी मौजूद होने की जांच करके, ब्राउज़र के साथ काम करने की सुविधा का पता लगाया जा सकता है:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
Polyfill
डिक्लेरेटिव शैडो डीओएम के लिए आसान पॉलीफ़िल बनाना काफ़ी आसान है. ऐसा इसलिए है, क्योंकि पॉलीफ़िल को टाइमिंग सेमेटिक्स या सिर्फ़ पार्स करने वाले टूल की उन विशेषताओं को पूरी तरह से दोहराने की ज़रूरत नहीं होती जिनसे ब्राउज़र को काम करने में मदद मिलती है. Declarative शैडो डीओएम को पॉलीफ़िल करने के लिए, हम सभी <template shadowrootmode>
एलिमेंट ढूंढने के लिए डीओएम को स्कैन कर सकते हैं. इसके बाद, उन्हें अपने पैरंट एलिमेंट पर अटैच किए गए शैडो रूट में बदल सकते हैं. दस्तावेज़ तैयार होने के बाद या कस्टम एलिमेंट के लाइफ़साइकल जैसे खास इवेंट से, यह प्रोसेस ट्रिगर की जा सकती है.
(function attachShadowRoots(root) {
if (supportsDeclarativeShadowDOM()) {
// Declarative Shadow DOM is supported, no need to polyfill.
return;
}
root.querySelectorAll("template[shadowrootmode]").forEach(template => {
const mode = template.getAttribute("shadowrootmode");
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(document);