צל DOM 101

Dominic Cooney
Dominic Cooney

מבוא

רכיבי אינטרנט הם קבוצה של תקנים מתקדמים ש:

  1. אפשרות ליצור ווידג'טים
  2. …שאפשר להשתמש בהם שוב ושוב באופן מהימן
  3. …ולא ישברו דפים אם הגרסה הבאה של הרכיב תשנה את פרטי ההטמעה הפנימית.

האם זה אומר שצריך להחליט מתי להשתמש ב-HTML/JavaScript ומתי להשתמש ברכיבי אינטרנט? לא! HTML ו-JavaScript יכולים ליצור דברים חזותיים אינטראקטיביים. ווידג'טים הם רכיבים חזותיים אינטראקטיביים. כדאי לנצל את המיומנויות שלכם ב-HTML וב-JavaScript כשאתם מפתחים ווידג'ט. התקנים של רכיבי אינטרנט נועדו לעזור לכם לעשות את זה.

אבל יש בעיה בסיסית שמקשה על השימוש בווידג'טים שנוצרו באמצעות HTML ו-JavaScript: עץ ה-DOM בתוך הווידג'ט לא מופרד משאר הדף. היעדר ההכלה הזה אומר שגיליון הסגנונות של המסמך עלול לחול בטעות על חלקים בתוך הווידג'ט, שקוד ה-JavaScript עלול לשנות בטעות חלקים בתוך הווידג'ט, שהמזהים עלולים לחפוף למזהים בתוך הווידג'ט וכן הלאה.

רכיבי אינטרנט מורכבים משלושה חלקים:

  1. תבניות
  2. Shadow DOM
  3. רכיבים מותאמים אישית

Shadow DOM פותר את בעיית ההכלה של עץ ה-DOM. ארבעת החלקים של רכיבי האינטרנט נועדו לפעול יחד, אבל אתם יכולים גם לבחור באילו חלקים של רכיבי האינטרנט להשתמש. במדריך הזה מוסבר איך להשתמש ב-Shadow DOM.

שלום, עולם הצללים

באמצעות Shadow DOM, רכיבים יכולים לקבל סוג חדש של צומת שמשויך אליהם. הסוג החדש הזה של צומת נקרא שורש צל. רכיב שמשויך אליו שורש צל נקרא מארח צל. התוכן של מארח ה-shadow לא מוצג, ובמקומו מוצג התוכן של שורש ה-shadow.

לדוגמה, אם יש לכם תגי עיצוב כאלה:

<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 מתחת לשורש ה-Shadow מקופסל.

הפרדת התוכן מהמצגת

עכשיו נראה איך משתמשים ב-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 אין אנקפסולציה, כל המבנה של תג השם חשוף למסמך. אם רכיבים אחרים בדף משתמשים בטעות באותם שמות של מחלקות לעיצוב או לסקריפטים, תהיה לנו בעיה.

אנחנו יכולים להימנע מחוויות לא נעימות.

שלב 1: מסתירים את פרטי המצגת

מבחינה סמנטית, כנראה שרק הדברים הבאים חשובים לנו:

  • זה תג שם.
  • השם הוא "Bob".

קודם כל, אנחנו כותבים תגי עיצוב שקרובים יותר לסמנטיקה האמיתית שאנחנו רוצים:

<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>, הם לא מוצגים, אבל אפשר לגשת אליהם מ-JavaScript. עכשיו נעשה את זה כדי לאכלס את שורש הצל:

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

עכשיו, אחרי שהגדרנו shadow root, תג השם מוצג שוב. אם תלחצו לחיצה ימנית על תג השם ותבדקו את הרכיב, תראו שזהו תג עיצוב סמנטי נחמד:

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

זה מראה שבאמצעות Shadow DOM, הסתרנו מהמסמך את פרטי התצוגה של תג השם. פרטי המצגת מופיעים ב-Shadow DOM.

‫שלב 2: הפרדת התוכן מהמצגת

תג השם שלנו מסתיר עכשיו את פרטי התצוגה מהדף, אבל הוא לא באמת מפריד בין התצוגה לבין התוכן, כי למרות שהתוכן (השם "Bob") נמצא בדף, השם שמוצג הוא השם שהעתקנו ל-shadow root. אם נרצה לשנות את השם בתג השם, נצטרך לעשות את זה בשני מקומות, והם עלולים לצאת מסנכרון.

רכיבי HTML הם קומפוזיציוניים – לדוגמה, אפשר לשים כפתור בתוך טבלה. כאן אנחנו צריכים קומפוזיציה: תג השם חייב להיות קומפוזיציה של הרקע האדום, הטקסט "Hi!‎" והתוכן שמופיע בתג השם.

אתם, יוצרי הרכיב, מגדירים איך הקומפוזיציה פועלת עם הווידג'ט באמצעות רכיב חדש שנקרא <content>. כך נוצרת נקודת הוספה בתצוגה של הווידג'ט, ונקודת ההוספה משתמשת בקוד מהסתעפות אחרת (Cherry-picking) של תוכן מהמארח של ה-shadow DOM כדי להציג אותו בנקודה הזו.

אם נשנה את תגי העיצוב ב-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>

כשתג השם מוצג, התוכן של המארח של ה-shadow DOM מוקרן במקום שבו מופיע הרכיב <content>.

עכשיו מבנה המסמך פשוט יותר כי השם מופיע רק במקום אחד – במסמך. אם צריך לעדכן את שם המשתמש בדף, פשוט כותבים:

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

וזהו. העיבוד של תג השם מתעדכן באופן אוטומטי על ידי הדפדפן, כי אנחנו מקרינים את התוכן של תג השם למקום באמצעות <content>.

<div id="ex2b">

עכשיו הפרדנו בין התוכן לבין התצוגה. התוכן נמצא במסמך, והתצוגה נמצאת ב-Shadow DOM. הדפדפן מסנכרן אותם באופן אוטומטי כשמגיע הזמן לעבד משהו.

שלב 3: רווחים

ההפרדה בין התוכן לתצוגה מאפשרת לנו לפשט את הקוד שמטפל בתוכן. בדוגמה של תג השם, הקוד הזה צריך לטפל רק במבנה פשוט שמכיל <div> אחד במקום כמה.

עכשיו, אם נשנה את המצגת, לא נצטרך לשנות את הקוד.

לדוגמה, נניח שאנחנו רוצים להתאים לשוק המקומי את תג השם שלנו. זה עדיין תג שם, כך שהתוכן הסמנטי במסמך לא משתנה:

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

קוד ההגדרה של שורש הצל נשאר ללא שינוי. רק מה שמוגדר ב-shadow root משתנה:

<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> בוחר את כל התוכן מהמארח של ה-shadow DOM. באמצעות המאפיין select, אתם יכולים לקבוע מה רכיב התוכן יציג. אפשר גם להשתמש בכמה רכיבי תוכן.

לדוגמה, אם יש לכם מסמך שמכיל את הטקסט הבא:

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

ו-shadow root שמשתמש בסלקטורים ב-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">. כמה פעמים כתובת האימייל של בוב מופיעה, ובאילו צבעים?

התשובה היא שכתובת האימייל של דני מופיעה פעם אחת, והיא צהובה.

הסיבה לכך היא שכמו שאנשים שמתעסקים עם Shadow DOM יודעים, בניית העץ של מה שבאמת מוצג על המסך היא כמו מסיבה ענקית. רכיב התוכן הוא ההזמנה שמאפשרת לתוכן מהמסמך להיכנס למסיבת הרינדור של Shadow DOM מאחורי הקלעים. ההזמנות האלה נשלחות לפי הסדר. מי שמקבל הזמנה תלוי בכתובת שאליה היא נשלחת (כלומר, המאפיין select). אחרי שהתוכן מוזמן, הוא תמיד מקבל את ההזמנה (מי לא היה מקבל?!) ויוצא לדרך. אם תישלח הזמנה נוספת לכתובת הזו, אף אחד לא יהיה בבית וההזמנה לא תגיע למסיבה.

בדוגמה שלמעלה, <div class="email"> תואם גם לסלקטור div וגם לסלקטור .email, אבל בגלל שרכיב התוכן עם הסלקטור div מופיע מוקדם יותר במסמך, <div class="email"> הולך למסיבה הצהובה, ואף אחד לא זמין להגיע למסיבה הכחולה. (אולי בגלל זה הוא כל כך עצוב, אבל צרת רבים חצי נחמה, אז אי אפשר לדעת).

אם משהו לא מוזמן לאף מסיבה, הוא לא מוצג בכלל. זה מה שקרה לטקסט "Hello, world" בדוגמה הראשונה. זה שימושי כשרוצים להשיג עיבוד שונה באופן קיצוני: כותבים את המודל הסמנטי במסמך, שאליו הסקריפטים בדף יכולים לגשת, אבל מסתירים אותו למטרות עיבוד ומחברים אותו למודל עיבוד שונה מאוד ב-Shadow DOM באמצעות JavaScript.

לדוגמה, ב-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, למשל, להשתמש בכמה צללים במארח צללים אחד, או בצללים מקוננים לצורך אנקפסולציה, או לתכנן את הדף באמצעות תצוגות מונחות-מודל (MDV) ו-Shadow DOM. ורכיבי אינטרנט הם לא רק Shadow DOM.

אנחנו מסבירים עליהם בפוסטים הבאים.