शैडो DOM 101

शुरुआती जानकारी

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

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

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

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

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

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

शैडो डीओएम, डीओएम ट्री एनकैप्सुलेशन से जुड़ी समस्या को हल करता है. वेब कॉम्पोनेंट के चार हिस्से, एक साथ काम करने के लिए डिज़ाइन किए गए हैं. हालांकि, आपके पास यह चुनने का विकल्प भी होता है कि वेब कॉम्पोनेंट के कौनसे हिस्से इस्तेमाल किए जाएं. इस ट्यूटोरियल में, Shadow 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 क्या है, तो यह नहीं दिखेगा कि “Yourんにちは、影会世界!”, बल्कि “नमस्ते, दुनिया!” ही दिखते हैं. ऐसा इसलिए होता है, क्योंकि शैडो रूट के नीचे मौजूद डीओएम सबट्री को एनकैप्सुलेट किया जाता है.

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

अब हम कॉन्टेंट को प्रज़ेंटेशन से अलग करने के लिए, Shadow 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>

इसके बाद हम प्रज़ेंटेशन के लिए इस्तेमाल की जाने वाली सभी स्टाइल और divs को एक <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>

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

इससे पता चलता है कि Shadow 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">

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

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

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

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

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

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

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

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

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

हमने बाद की पोस्ट में उनके बारे में बताया है.