Shadow DOM 101

ভূমিকা

ওয়েব কম্পোনেন্ট হল অত্যাধুনিক স্ট্যান্ডার্ডের সেট যা:

  1. উইজেট তৈরি করা সম্ভব করে তুলুন
  2. …যা নির্ভরযোগ্যভাবে আবার ব্যবহার করা যেতে পারে
  3. …এবং কম্পোনেন্টের পরবর্তী ভার্সন ইন্টার্নাল ইমপ্লিমেন্টেশন বিবরণ পরিবর্তন করলে যা পৃষ্ঠাগুলিকে ভাঙবে না।

Does this mean you have to decide when to use HTML/JavaScript, and when to use Web Components? না! HTML এবং জাভাস্ক্রিপ্ট ইন্টার‌্যাক্টিভ ভিজ্যুয়াল কন্টেন্ট তৈরি করতে পারে। উইজেট হল ইন্টার‍্যাক্টিভ ভিজ্যুয়াল জিনিস। উইজেট তৈরি করার সময় আপনার HTML ও JavaScript দক্ষতা কাজে লাগালে সুবিধা হবে। আপনাকে সাহায্য করার জন্য ওয়েব কম্পোনেন্ট স্ট্যান্ডার্ড ডিজাইন করা হয়েছে।

কিন্তু HTML ও JavaScript দিয়ে তৈরি উইজেট ব্যবহার করার ক্ষেত্রে একটি মৌলিক সমস্যা আছে: উইজেটের মধ্যে থাকা DOM ট্রি পৃষ্ঠার বাকি অংশ থেকে এনক্যাপসুলেট করা থাকে না। এনক্যাপসুলেশন না থাকার অর্থ হল, আপনার ডকুমেন্ট স্টাইলশিট ভুলবশত উইজেটের ভিতরের অংশে প্রযোজ্য হতে পারে; আপনার জাভাস্ক্রিপ্ট ভুলবশত উইজেটের ভিতরের অংশ পরিবর্তন করতে পারে; আপনার আইডি উইজেটের ভিতরের আইডির সাথে ওভারল্যাপ করতে পারে; এবং আরও অনেক কিছু।

ওয়েব কম্পোনেন্ট তিনটি অংশ নিয়ে গঠিত:

  1. টেমপ্লেট
  2. Shadow DOM
  3. কাস্টম এলিমেন্ট

Shadow DOM, DOM ট্রি এনক্যাপসুলেশন সংক্রান্ত সমস্যার সমাধান করে। ওয়েব কম্পোনেন্টের চারটি অংশ একসাথে কাজ করার জন্য ডিজাইন করা হয়েছে, তবে আপনি ওয়েব কম্পোনেন্টের কোন অংশ ব্যবহার করবেন তা বেছে নিতে পারেন। এই টিউটোরিয়াল থেকে আপনি জানতে পারবেন কীভাবে Shadow DOM ব্যবহার করতে হয়।

হ্যালো, শ্যাডো ওয়ার্ল্ড

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>

শুধু তাই নয়, পৃষ্ঠায় থাকা জাভাস্ক্রিপ্ট যদি বোতামের textContent কী তা জানতে চায়, তাহলে এটি “こんにちは、影の世界!” পাবে না, বরং “Hello, world!” পাবে কারণ শ্যাডো রুটের অধীনে থাকা DOM সাবট্রি এনক্যাপসুলেট করা থাকে।

প্রেজেন্টেশন থেকে কন্টেন্ট আলাদা করা হচ্ছে

এখন আমরা প্রেজেন্টেশন থেকে কন্টেন্ট আলাদা করার জন্য 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>

DOM ট্রিতে এনক্যাপসুলেশন না থাকার কারণে, নেম ট্যাগের সম্পূর্ণ স্ট্রাকচার ডকুমেন্টে দেখা যায়। পৃষ্ঠার অন্যান্য এলিমেন্ট যদি স্টাইলিং বা স্ক্রিপ্টিংয়ের জন্য ভুলবশত একই ক্লাস নাম ব্যবহার করে, তাহলে আমাদের সমস্যায় পড়তে হবে।

আমরা খারাপ সময় এড়াতে পারি।

ধাপ ১: প্রেজেন্টেশনের বিবরণ লুকান

শব্দার্থগতভাবে আমরা সম্ভবত শুধুমাত্র এই বিষয়ে যত্নশীল:

  • এটি একটি নামের ট্যাগ।
  • নাম হল “বীণা”।

প্রথমে, আমরা এমন মার্কআপ লিখি যা আমাদের কাঙ্ক্ষিত প্রকৃত শব্দার্থের কাছাকাছি:

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

এই মুহূর্তে ‘Bob’ হল একমাত্র জিনিস যা রেন্ডার করা হয়েছে। আমরা প্রেজেন্টেশনাল DOM এলিমেন্টগুলি <template> এলিমেন্টের ভিতরে সরিয়ে নিয়েছি, তাই সেগুলি রেন্ডার করা হয় না, তবে জাভাস্ক্রিপ্ট থেকে সেগুলি অ্যাক্সেস করা যেতে পারে। শ্যাডো রুট পপুলেট করার জন্য আমরা এখন এটি করি:

<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 ব্যবহার করে, আমরা ডকুমেন্ট থেকে নাম ট্যাগের প্রেজেন্টেশন সংক্রান্ত বিবরণ লুকিয়ে রেখেছি। The presentation details are encapsulated in the Shadow DOM.

ধাপ ২: প্রেজেন্টেশন থেকে কন্টেন্ট আলাদা করা

আমাদের নাম ট্যাগ এখন পৃষ্ঠা থেকে উপস্থাপনার বিবরণ লুকায়, কিন্তু এটি আসলে কন্টেন্ট থেকে উপস্থাপনাকে আলাদা করে না, কারণ যদিও কন্টেন্ট (নাম “Bob”) পৃষ্ঠায় আছে, যে নামটি রেন্ডার করা হয়েছে সেটি আমরা শ্যাডো রুটে কপি করেছি। আমরা যদি নেম ট্যাগে নাম পরিবর্তন করতে চাই, তাহলে আমাদের দুটি জায়গায় এটি করতে হবে এবং সেগুলি সিঙ্ক নাও হতে পারে।

HTML এলিমেন্ট কম্পোজিশনাল — যেমন, আপনি টেবিলের মধ্যে বোতাম যোগ করতে পারেন। এখানে আমাদের কম্পোজিশন প্রয়োজন: নেম ট্যাগে অবশ্যই লাল ব্যাকগ্রাউন্ড, “Hi!” টেক্সট এবং নেম ট্যাগে থাকা কন্টেন্টের কম্পোজিশন থাকতে হবে।

কম্পোনেন্ট লেখক হিসেবে আপনি <content> নামক নতুন এলিমেন্ট ব্যবহার করে আপনার উইজেটের সাথে কম্পোজিশন কীভাবে কাজ করে তা নির্ধারণ করেন। এটি উইজেটের প্রেজেন্টেশনে একটি ইনসার্শন পয়েন্ট তৈরি করে এবং ইনসার্শন পয়েন্টটি সেই পয়েন্টে প্রেজেন্ট করার জন্য শ্যাডো হোস্ট থেকে কন্টেন্ট চেরি-পিক করে।

আমরা যদি Shadow 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">

এখন আমরা কন্টেন্ট ও প্রেজেন্টেশন আলাদা করতে পেরেছি। কন্টেন্টটি ডকুমেন্টে আছে; প্রেজেন্টেশনটি শ্যাডো 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>

এটি বর্তমানে ওয়েবে থাকা পরিস্থিতির তুলনায় একটি বড় উন্নতি, কারণ আপনার নাম আপডেট কোড কম্পোনেন্ট-এর কাঠামোর উপর নির্ভর করতে পারে যা সহজ এবং সামঞ্জস্যপূর্ণ। আপনার নাম আপডেট কোডের রেন্ডারিংয়ের জন্য ব্যবহৃত স্ট্রাকচার জানার প্রয়োজন নেই। আমরা যদি বিবেচনা করি যে কী রেন্ডার করা হয়েছে, তাহলে নামটি ইংরেজিতে দ্বিতীয় স্থানে দেখা যাবে ("Hi!" এর পরে)। আমার নাম হল"), কিন্তু জাপানি ভাষায় প্রথমে ("と申します" এর আগে)। যে নাম দেখানো হচ্ছে তা আপডেট করার ক্ষেত্রে এই পার্থক্যটি অর্থগতভাবে অর্থহীন, তাই নাম আপডেট করার কোডকে সেই বিবরণ সম্পর্কে জানতে হবে না।

অতিরিক্ত ক্রেডিট: উন্নত প্রোজেকশন

উপরের উদাহরণে, <content> এলিমেন্ট শ্যাডো হোস্ট থেকে সব কন্টেন্ট চেরি-পিক করে। select অ্যাট্রিবিউট ব্যবহার করে, আপনি কোনও কন্টেন্ট এলিমেন্ট কী প্রজেক্ট করে তা নিয়ন্ত্রণ করতে পারবেন। এছাড়াও, আপনি একাধিক কন্টেন্ট এলিমেন্ট ব্যবহার করতে পারবেন।

যেমন, আপনার কাছে যদি এমন কোনও ডকুমেন্ট থাকে যাতে এটি রয়েছে:

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

এবং একটি শ্যাডো রুট যা নির্দিষ্ট কন্টেন্ট বেছে নিতে CSS সিলেক্টর ব্যবহার করে:

<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"> এলিমেন্ট দুটির সাথেই মিলে যায়। ববের ইমেল আইডি কতবার দেখা যাচ্ছে এবং কোন রঙে?

উত্তর হল, ববের ইমেল আইডি একবারই দেখা যাচ্ছে এবং সেটি হলুদ রঙের।

কারণ হল, যারা শ্যাডো DOM হ্যাক করেন তারা জানেন যে, স্ক্রিনে আসলে যা রেন্ডার করা হয় তার ট্রি তৈরি করা একটি বিশাল পার্টির মতো। কন্টেন্ট এলিমেন্ট হল আমন্ত্রণ যা ব্যাকস্টেজ শ্যাডো DOM রেন্ডারিং পার্টিতে ডকুমেন্ট থেকে কন্টেন্টকে অনুমতি দেয়। এইসব আমন্ত্রণ ক্রমানুসারে পাঠানো হয়; কে আমন্ত্রণ পাবেন তা নির্ভর করে কাকে এটি পাঠানো হয়েছে তার উপর (অর্থাৎ, select অ্যাট্রিবিউট।) কন্টেন্ট, একবার আমন্ত্রণ জানানো হলে, সবসময় আমন্ত্রণ গ্রহণ করে (কে করবে না?!) এবং এটি চলে যায়। যদি সেই ঠিকানায় আবার আমন্ত্রণ পাঠানো হয়, তাহলে কেউ বাড়িতে নেই এবং আপনার পার্টিতে কেউ আসবে না।

উপরের উদাহরণে, <div class="email"> div সিলেক্টর এবং .email সিলেক্টর উভয়ের সাথেই ম্যাচ করে, কিন্তু div সিলেক্টর সহ কন্টেন্ট এলিমেন্ট ডকুমেন্টে আগে আসে বলে, <div class="email"> হলুদ পার্টিতে যায় এবং নীল পার্টিতে যাওয়ার জন্য কেউ উপলভ্য থাকে না। (কেন এটি এত নীল, যদিও দুঃখের সঙ্গী অনেক, তাই আপনি কখনও জানতে পারবেন না।)

কোনও কিছুকে কোনও পার্টিতে আমন্ত্রণ জানানো না হলে, সেটি একেবারেই রেন্ডার করা হয় না। প্রথম উদাহরণে “Hello, world” টেক্সটের ক্ষেত্রে ঠিক এটাই ঘটেছে। আপনি যখন সম্পূর্ণ আলাদা রেন্ডারিং পেতে চান তখন এটি কার্যকর: ডকুমেন্টে সিম্যান্টিক মডেল লিখুন, যা পৃষ্ঠার স্ক্রিপ্টে অ্যাক্সেসযোগ্য, তবে রেন্ডারিংয়ের উদ্দেশ্যে এটি লুকান এবং জাভাস্ক্রিপ্ট ব্যবহার করে শ্যাডো DOM-এ সম্পূর্ণ আলাদা রেন্ডারিং মডেলের সাথে এটি কানেক্ট করুন।

যেমন, HTML-এ তারিখ বেছে নেওয়ার একটি ভালো বিকল্প আছে। আপনি <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>

কিন্তু Shadow 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>

আপনি Shadow DOM 101 পাস করেছেন

এগুলিই হল Shadow DOM-এর মূল বিষয় — আপনি Shadow DOM 101 পাস করেছেন! আপনি Shadow DOM-এর সাহায্যে আরও অনেক কিছু করতে পারবেন, যেমন, আপনি একটি শ্যাডো হোস্টে একাধিক শ্যাডো ব্যবহার করতে পারবেন, অথবা এনক্যাপসুলেশনের জন্য নেস্টেড শ্যাডো, অথবা মডেল-চালিত ভিউ (MDV) এবং Shadow DOM ব্যবহার করে আপনার পৃষ্ঠা ডিজাইন করতে পারবেন। আর ওয়েব কম্পোনেন্ট শুধু Shadow DOM-এর চেয়েও বেশি কিছু।

আমরা পরবর্তী পোস্টে সেগুলি ব্যাখ্যা করি।