शैडो DOM 101

परिचय

वेब कॉम्पोनेंट, आधुनिक स्टैंडर्ड का एक सेट है जो:

  1. विजेट बनाना संभव होता है
  2. …जिनका भरोसेमंद तरीके से फिर से इस्तेमाल किया जा सकता है
  3. …और अगर कॉम्पोनेंट के अगले वर्शन में, लागू करने के तरीके की अंदरूनी जानकारी में बदलाव होता है, तो पेजों पर कोई असर नहीं पड़ेगा.

क्या इसका मतलब है कि आपको यह तय करना होगा कि एचटीएमएल/JavaScript का इस्तेमाल कब करना है और वेब कॉम्पोनेंट का इस्तेमाल कब करना है? नहीं! एचटीएमएल और JavaScript की मदद से, इंटरैक्टिव विज़ुअल बनाए जा सकते हैं. विजेट, इंटरैक्टिव विज़ुअल प्रॉडक्ट होते हैं. विजेट बनाते समय, एचटीएमएल और JavaScript की अपनी स्किल का इस्तेमाल करना सही रहता है. वेब कॉम्पोनेंट के स्टैंडर्ड, आपको ऐसा करने में मदद करने के लिए डिज़ाइन किए गए हैं.

हालांकि, एक बुनियादी समस्या की वजह से एचटीएमएल और JavaScript से बनाए गए विजेट का इस्तेमाल करना मुश्किल हो जाता है: विजेट के अंदर मौजूद डीओएम ट्री, पेज के बाकी हिस्से से अलग नहीं होता. एन्कैप्सलेशन की कमी का मतलब है कि आपके दस्तावेज़ की स्टाइलशीट, गलती से विजेट के हिस्सों पर लागू हो सकती है. आपका JavaScript, गलती से विजेट के हिस्सों में बदलाव कर सकता है. आपके आईडी, विजेट के आईडी से ओवरलैप हो सकते हैं वगैरह.

वेब कॉम्पोनेंट में तीन हिस्से होते हैं:

  1. टेंप्लेट
  2. शैडो DOM
  3. कस्टम एलिमेंट

शैडो DOM, DOM ट्री एनकैप्सुलेशन की समस्या को हल करता है. वेब कॉम्पोनेंट के चार हिस्सों को एक साथ काम करने के लिए डिज़ाइन किया गया है. हालांकि, आपके पास यह चुनने का विकल्प भी होता है कि वेब कॉम्पोनेंट के कौनसे हिस्सों का इस्तेमाल करना है. इस ट्यूटोरियल में, शैडो DOM का इस्तेमाल करने का तरीका बताया गया है.

हेलो, शैडो वर्ल्ड

शैडो डीओएम की मदद से, एलिमेंट को उनसे जुड़े नए तरह का नोड मिल सकता है. इस नए तरह के नोड को शैडो रूट कहा जाता है. जिस एलिमेंट से कोई शैडो रूट जुड़ा होता है उसे शैडो होस्ट कहा जाता है. शैडो होस्ट का कॉन्टेंट रेंडर नहीं किया गया है. इसके बजाय, शैडो रूट का कॉन्टेंट रेंडर किया जाता है.

उदाहरण के लिए, अगर आपका मार्कअप इस तरह का था:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

इसके बजाय

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

आपका पेज इस तरह दिखता है

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

इतना ही नहीं, अगर पेज पर मौजूद JavaScript पूछता है कि बटन का textContent क्या है, तो उसे “こんにちは、影の世界!” के बजाय “Hello, world!” दिखेगा. इसकी वजह यह है कि शैडो रूट के नीचे मौजूद डीओएम सबट्री को कंटेनप्लेट किया गया है.

कॉन्टेंट को प्रज़ेंटेशन से अलग करना

अब हम कॉन्टेंट को प्रेज़ेंटेशन से अलग करने के लिए, शैडो DOM का इस्तेमाल करेंगे. मान लें कि हमारे पास यह नेम टैग है:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

मार्कअप यहां दिया गया है. आज आपको यही लिखना है. इसमें Shadow DOM का इस्तेमाल नहीं किया जाता है:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

डीओएम ट्री में एन्कैप्सुलेट करने की सुविधा नहीं होती, इसलिए नाम टैग का पूरा स्ट्रक्चर दस्तावेज़ में दिखता है. अगर पेज पर मौजूद अन्य एलिमेंट, स्टाइल या स्क्रिप्टिंग के लिए गलती से एक ही क्लास के नाम का इस्तेमाल करते हैं, तो हमें परेशानी होगी.

हम बुरा समय बिताने से बच सकते हैं.

पहला चरण: प्रज़ेंटेशन की जानकारी छिपाना

सेमेटिक के हिसाब से, शायद हमें सिर्फ़ यह ध्यान रखना है कि:

  • यह नाम टैग है.
  • नाम “बॉब” है.

सबसे पहले, हम मार्कअप लिखते हैं, जो हमारे वांछित सेमेटिक्स के करीब होता है:

<div id="nameTag">Bob</div>

इसके बाद, हम प्रज़ेंटेशन के लिए इस्तेमाल की गई सभी स्टाइल और div को <template> एलिमेंट में डाल देते हैं:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

इस समय, सिर्फ़ 'बॉब' रेंडर किया गया है. हमने प्रज़ेंटेशन के लिए इस्तेमाल होने वाले DOM एलिमेंट को <template> एलिमेंट में ले जाया है, इसलिए उन्हें रेंडर नहीं किया जाता. हालांकि, इन्हें JavaScript से ऐक्सेस किया जा सकता है. हम अब ऐसा, शेडो रूट में डेटा भरने के लिए करते हैं:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

अब जब हमने शैडो रूट सेट अप कर लिया है, तो नाम टैग फिर से रेंडर हो जाता है. अगर नाम टैग पर दायां क्लिक करके एलिमेंट की जांच की जाती है, तो आपको पता चलता है कि यह एक बेहतरीन, सेमैंटिक मार्कअप है:

<div id="nameTag">Bob</div>

इससे पता चलता है कि हमने शैडो DOM का इस्तेमाल करके, दस्तावेज़ से नाम टैग की प्रज़ेंटेशन की जानकारी छिपाई है. प्रज़ेंटेशन की जानकारी को शैडो DOM में शामिल किया जाता है.

दूसरा चरण: कॉन्टेंट को प्रज़ेंटेशन से अलग करना

हमारा नाम टैग, अब प्रज़ेंटेशन की जानकारी को पेज से छिपा देता है. हालांकि, असल में यह प्रज़ेंटेशन को कॉन्टेंट से अलग नहीं करता. ऐसा इसलिए है, क्योंकि पेज पर कॉन्टेंट (“बॉब”) नाम मौजूद है, लेकिन रेंडर किया गया नाम ही शैडो रूट में कॉपी किया गया है. अगर हमें नेम टैग पर मौजूद नाम बदलना है, तो हमें दो जगहों पर बदलाव करना होगा. ऐसा करने पर, हो सकता है कि वे सिंक न रहें.

एचटीएमएल एलिमेंट, कॉम्पोज़िशनल होते हैं — उदाहरण के लिए, टेबल के अंदर बटन डाला जा सकता है. यहां हमें कॉम्पोज़िशन की ज़रूरत है: नेम टैग, लाल रंग के बैकग्राउंड, “नमस्ते!” टेक्स्ट, और नेम टैग पर मौजूद कॉन्टेंट का कॉम्पोज़िशन होना चाहिए.

कॉम्पोनेंट के लेखक के तौर पर, आपको यह तय करना होता है कि <content> नाम के नए एलिमेंट का इस्तेमाल करके, आपके विजेट के साथ कॉम्पोज़िशन कैसे काम करता है. इससे विजेट के प्रज़ेंटेशन में एक इंसर्शन पॉइंट बन जाता है. साथ ही, इंसर्शन पॉइंट उस जगह पर दिखाने के लिए, शैडो होस्ट से कॉन्टेंट चुनता है.

अगर हम शैडो DOM में मार्कअप को इस तरह बदलते हैं:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

नेम टैग रेंडर होने पर, शैडो होस्ट का कॉन्टेंट उस जगह पर प्रोजेक्ट किया जाता है जहां <content> एलिमेंट दिखता है.

अब दस्तावेज़ का स्ट्रक्चर आसान हो गया है, क्योंकि नाम सिर्फ़ एक जगह पर है — दस्तावेज़ में. अगर आपके पेज पर उपयोगकर्ता का नाम अपडेट करना है, तो बस यह लिखें:

document.querySelector('#nameTag').textContent = 'Shellie';

बस इतना ही. ब्राउज़र, नेम टैग को अपने-आप रेंडर करता है, क्योंकि हम <content> की मदद से नेम टैग के कॉन्टेंट को प्रोजेक्ट कर रहे हैं.

<div id="ex2b">

अब हमने कॉन्टेंट और प्रज़ेंटेशन को अलग कर दिया है. कॉन्टेंट, दस्तावेज़ में होता है और प्रज़ेंटेशन, शैडो डीओएम में होता है. जब कुछ रेंडर करना होता है, तो ब्राउज़र उन्हें अपने-आप सिंक रखता है.

तीसरा चरण: मुनाफ़ा

कॉन्टेंट और प्रज़ेंटेशन को अलग करके, कॉन्टेंट में बदलाव करने वाले कोड को आसान बनाया जा सकता है. नाम टैग के उदाहरण में, उस कोड को सिर्फ़ एक आसान स्ट्रक्चर से निपटना होगा, जिसमें कई के बजाय एक <div> होगा.

अब अगर हमें अपना प्रज़ेंटेशन बदलना है, तो हमें कोड में कोई बदलाव करने की ज़रूरत नहीं है!

उदाहरण के लिए, मान लें कि हमें अपने नाम वाले टैग को स्थानीय भाषा में दिखाना है. यह अब भी नाम टैग है, इसलिए दस्तावेज़ में सेमैनटिक कॉन्टेंट नहीं बदलता:

<div id="nameTag">Bob</div>

शैडो रूट सेटअप कोड में कोई बदलाव नहीं होता. शैडो रूट में सिर्फ़ ये बदलाव किए जाते हैं:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

यह वेब पर मौजूद मौजूदा स्थिति से काफ़ी बेहतर है, क्योंकि नाम अपडेट करने का कोड, कॉम्पोनेंट के स्ट्रक्चर पर निर्भर कर सकता है. यह स्ट्रक्चर आसान और एक जैसा होता है. आपके नाम के अपडेट कोड को रेंडर करने के लिए इस्तेमाल किए गए स्ट्रक्चर के बारे में जानने की ज़रूरत नहीं है. अगर हम रेंडर किए गए कॉन्टेंट पर विचार करें, तो नाम अंग्रेज़ी में दूसरे स्थान पर दिखता है. मेरा नाम है”), लेकिन पहले जैपनीज़ में ( “と申します” से पहले). दिख रहे नाम को अपडेट करने के लिहाज़ से, इस फ़र्क़ का कोई मतलब नहीं है, इसलिए नाम अपडेट करने वाले कोड को इस जानकारी की ज़रूरत नहीं है.

अतिरिक्त क्रेडिट: बेहतर प्रोजेक्शन

ऊपर दिए गए उदाहरण में, <content> एलिमेंट, शैडो होस्ट से सभी कॉन्टेंट को चुनता है. select एट्रिब्यूट का इस्तेमाल करके, यह कंट्रोल किया जा सकता है कि कॉन्टेंट एलिमेंट क्या प्रोजेक्ट करता है. एक से ज़्यादा कॉन्टेंट एलिमेंट का भी इस्तेमाल किया जा सकता है.

उदाहरण के लिए, अगर आपके पास ऐसा दस्तावेज़ है जिसमें यह जानकारी शामिल है:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

और एक शैडो रूट जो खास कॉन्टेंट को चुनने के लिए सीएसएस सिलेक्टर का इस्तेमाल करता है:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

<div class="email"> एलिमेंट का मिलान <content select="div"> और <content select=".email">, दोनों एलिमेंट से किया जाता है. बॉब का ईमेल पता कितनी बार और किन रंगों में दिखता है?

इसका जवाब है कि बॉब का ईमेल पता एक बार दिखता है और वह पीले रंग में दिखता है.

इसकी वजह यह है कि शैडो डीओएम को हैक करने वाले लोगों को पता है कि स्क्रीन पर असल में रेंडर किए गए कॉन्टेंट का ट्री बनाना, एक बड़ी पार्टी की तरह है. कॉन्टेंट एलिमेंट एक न्योता है, जो दस्तावेज़ के कॉन्टेंट को बैकस्टेज शैडो डीओएम रेंडरिंग पार्टी में भेजता है. ये न्योते क्रम से डिलीवर किए जाते हैं. यह इस बात पर निर्भर करता है कि न्योता किसको भेजा गया है (यानी, select एट्रिब्यूट). कॉन्टेंट को न्योता भेजने के बाद, वह हमेशा न्योता स्वीकार करता है (कौन नहीं करेगा?!) और फिर वह चलता है. अगर उस पते पर फिर से न्योता भेजा जाता है, तो कोई भी घर पर नहीं होता और न्योता आपकी पार्टी में नहीं आता.

ऊपर दिए गए उदाहरण में, <div class="email">, div सिलेक्टर और .email सिलेक्टर, दोनों से मैच करता है. हालांकि, div सिलेक्टर वाला कॉन्टेंट एलिमेंट दस्तावेज़ में पहले आता है, इसलिए <div class="email">, पीले रंग के हिस्से में जाता है और नीले रंग के हिस्से में कोई भी नहीं जाता. (यह क्यों हो सकता है कि यह इतना नीला है, हालांकि दुख वाले लोग साथ में होते हैं, इसलिए आपको कभी पता नहीं चलता.)

अगर किसी आइटम को किसी पार्टी में शामिल नहीं किया जाता है, तो उसे रेंडर नहीं किया जाता. यही बात, सबसे पहले उदाहरण में दिए गए “नमस्ते, दुनिया” टेक्स्ट के साथ हुई है. यह तब फ़ायदेमंद होता है, जब आपको पूरी तरह से अलग रेंडरिंग चाहिए: दस्तावेज़ में सेमैनटिक मॉडल लिखें, जो पेज में स्क्रिप्ट के लिए ऐक्सेस किया जा सकता है. हालांकि, इसे रेंडरिंग के मकसद से छिपाएं और JavaScript का इस्तेमाल करके, इसे शैडो DOM में किसी बहुत अलग रेंडरिंग मॉडल से कनेक्ट करें.

उदाहरण के लिए, एचटीएमएल में तारीख चुनने वाला अच्छा टूल है. <input type="date"> लिखने पर, आपको एक पॉप-अप कैलेंडर दिखेगा. लेकिन अगर आपको उपयोगकर्ता को द्वीप पर डेज़र्ट के लिए तारीखों की सीमा चुनने की अनुमति देनी है, तो क्या होगा? अपना दस्तावेज़ इस तरह से सेट अप करें:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

लेकिन एक ऐसा शैडो DOM बनाएं जो टेबल का इस्तेमाल करके, एक शानदार कैलेंडर बनाए. यह कैलेंडर, तारीख की सीमा वगैरह को हाइलाइट करता है. जब उपयोगकर्ता कैलेंडर में मौजूद दिनों पर क्लिक करता है, तो कॉम्पोनेंट, startDate और endDate इनपुट में स्टेटस अपडेट करता है. जब उपयोगकर्ता फ़ॉर्म सबमिट करता है, तो उन इनपुट एलिमेंट की वैल्यू सबमिट हो जाती हैं.

अगर लेबल रेंडर नहीं किए जाएंगे, तो मैंने उन्हें दस्तावेज़ में क्यों शामिल किया? इसकी वजह यह है कि अगर कोई उपयोगकर्ता ऐसे ब्राउज़र पर फ़ॉर्म देखता है जिस पर Shadow DOM काम नहीं करता, तो भी फ़ॉर्म इस्तेमाल किया जा सकता है, न कि उतना सुंदर. उपयोगकर्ता को कुछ ऐसा दिखता है:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

आपने शैडो डीओएम के बारे में बुनियादी जानकारी हासिल कर ली है

ये शैडो डीओएम के बारे में बुनियादी बातें हैं — शैडो डीओएम के बारे में आपकी बुनियादी जानकारी हो गई है! शैडो DOM की मदद से, ज़्यादा काम किए जा सकते हैं. उदाहरण के लिए, एक शैडो होस्ट पर कई शैडो का इस्तेमाल किया जा सकता है. इसके अलावा, एन्कैप्सुलेट करने के लिए नेस्ट की गई शैडो का इस्तेमाल किया जा सकता है. इसके अलावा, मॉडल-ड्रिवन व्यू (एमडीवी) और शैडो DOM का इस्तेमाल करके, अपने पेज को बेहतर बनाया जा सकता है. वेब कॉम्पोनेंट, सिर्फ़ शैडो डीओएम नहीं होते.

हम इनके बारे में बाद की पोस्ट में बताएंगे.