शैडो DOM 101

परिचय

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

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

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

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

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

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

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

नमस्ते, Shadow World

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

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

<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>

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

<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">, पीले रंग के हिस्से में जाता है और नीले रंग के हिस्से में कोई भी नहीं जाता. (शायद इसी वजह से यह इतना नीला है. हालांकि, उदास मन को किसी साथी की ज़रूरत होती है, इसलिए कभी-कभी ऐसा भी हो सकता है.)

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

अगर लेबल रेंडर नहीं किए जाएंगे, तो मैंने दस्तावेज़ में लेबल क्यों शामिल किए? इसकी वजह यह है कि अगर कोई उपयोगकर्ता ऐसे ब्राउज़र से फ़ॉर्म देखता है जो शैडो 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 का इस्तेमाल करके, अपने पेज को बेहतर बनाया जा सकता है. वेब कॉम्पोनेंट, सिर्फ़ शैडो डीओएम नहीं होते.

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