צל DOM 101

Dominic Cooney
Dominic Cooney

מבוא

Web Components (רכיבי אינטרנט) הם קבוצה של סטנדרטים חדשניים שעומדים בדרישות הבאות:

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

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

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

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

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

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

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

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

עכשיו נבחן את השימוש ב-DOM של Shadow כדי להפריד בין תוכן לבין המצגת. נניח שיש לנו את תג השם הבא:

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

הנה תגי העיצוב. זה מה תכתוב היום. לא נעשה שימוש ב-DOM של Shadow:

<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: הסתרת פרטי המצגת

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

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

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

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

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

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

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

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

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

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

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

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

שלב 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>

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

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

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

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

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

לדוגמה, ל-HTML יש בוחר תאריכים מוצלח. אם כותבים <input type="date">, נוצר יומן קופץ ונעים. אבל מה קורה אם רוצים לאפשר למשתמש לבחור טווח תאריכים לחופשה שלו בקינוח? (כלומר, עם ערסלים מתוצרת Red Vines). כך מגדירים את המסמך:

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

You Pass Shadow DOM 101

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

נסביר על כך בפוסטים מאוחרים יותר.