צל DOM 101

Dominic Cooney
Dominic Cooney

מבוא

Web Components היא קבוצה של תקנים מתקדמים שמאפשרים:

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

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

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

Web Components מורכב משלושה חלקים:

  1. תבניות
  2. Shadow DOM
  3. אלמנטים מותאמים אישית

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

שלום, Shadow World

בעזרת 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 של הלחצן, הוא לא יקבל את הערך 'こんにちは、影の世界!‎' אלא את הערך '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>

זהו ה-Markup. זה מה שצריך לכתוב היום. היא לא משתמשת ב-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>

לאחר מכן, אנחנו מכניסים את כל הסגנונות וה-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>

בשלב הזה, '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);

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

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

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

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

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

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

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

אם משנים את ה-Markup ב-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">

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

שלב 3: רווח

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

ושורש צל שמשתמש בסלקטורים של 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>

אלא ליצור 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 למתחילים

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

נסביר עליהם בהמשך.