צל DOM 301

מושגים מתקדמים וממשקי API של DOM

המאמר הזה מפרט עוד על הדברים המדהימים שאפשר לעשות עם Shadow DOM! הוא מבוסס על המושגים שמפורטים במאמרים Shadow DOM 101 ו-Shadow DOM 201.

שימוש בכמה שורשי צל

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

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

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

העיבוד של המודל הזה הוא Root 2 FTW, למרות שכבר צירפנו עץ צל. הסיבה לכך היא ש-tree האחרון של האופל שנוסף למארח מנצח. מבחינת העיבוד, זוהי מחסנית LIFO. בדיקה של DevTools מאשרת את ההתנהגות הזו.

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

נקודות להוספת אזורים כהים

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

ככל שמתעמקים יותר בנושא, הדברים נעשים מורכבים יותר. לכן, המפרט ברור מאוד מה קורה כשמספר רכיבי <shadow> פעילים:

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

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

יש כמה דברים מעניינים בדוגמה הזו:

  1. "Root 2 FTW" עדיין רינדור מעל ל-"Root 1 FTW". הסיבה לכך היא המיקום שבו מיקמנו את נקודת ההוספה <shadow>. אם רוצים להפוך את הכיוון, מעבירים את נקודת ההוספה: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. שימו לב שיש עכשיו נקודת הוספה <content> ב-root1. כך צומת הטקסט "Light DOM" מופיע במהלך הרינדור.

מה עובר עיבוד ב-<shadow>?

לפעמים כדאי לדעת איזה עץ צללים ישן יותר עובר עיבוד ב-<shadow>. אפשר לקבל הפניה לעץ הזה באמצעות .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

אחזור של root בצל של מארח

אם רכיב מארח Shadow DOM, אפשר לגשת לשורש הצל הצעיר ביותר שלו באמצעות .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

אם אתם חוששים שאנשים ייכנסו לצללים שלכם, תוכלו להגדיר מחדש את .shadowRoot כ-null:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

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

בניית DOM של צל ב-JS

אם אתם מעדיפים ליצור DOM ב-JS, ל-HTMLContentElement ול-HTMLShadowElement יש ממשקים לכך.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

הדוגמה הזו כמעט זהה לדוגמה שמופיעה בקטע הקודם. ההבדל היחיד הוא שאני משתמש עכשיו ב-select כדי לשלוף את <span> שנוסף לאחרונה.

עבודה עם נקודות הטמעה

צמתים שנבחרים מתוך רכיב המארח ו"מתחלקים" לעץ הצללים נקראים…תראו את ההתלהבות…צמתים מבוזרים! מותר להם לעבור את גבולות הצללית בזמן שנקודות ההכנסה מוזמנות פנימה.

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

לדוגמה:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

והנה, הרכיב h2 הוא לא צאצא של DOM האפל. זה עניין של עוד טיפ:

Element.getDistributedNodes()

אי אפשר לעבור ל-<content>, אבל ה-API של .getDistributedNodes() מאפשר לנו לשלוח שאילתות לצמתים המפוזרים בנקודת הטמעה:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

בדומה ל-.getDistributedNodes(), אפשר לבדוק לאילו נקודות הוספה מפוזר צומת מסוים, על ידי קריאה ל-.getDestinationInsertionPoints() שלו:

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

כלי: Shadow DOM Visualizer

קשה להבין את הקסם השחור של Shadow DOM. אני זוכרת את הניסיון שלי להבין את זה בפעם הראשונה.

כדי להמחיש איך פועל העיבוד של Shadow DOM, פיתחתי כלי באמצעות d3.js. אפשר לערוך את שתי התיבות של הרכיבים בצד ימין. אתם יכולים להדביק את ה-Markup שלכם ולשחק איתו כדי לראות איך הדברים עובדים ואיך נקודות ההוספה מעבירות צמתים מארחים לעץ הצל.

תצוגה חזותית של Shadow DOM
הפעלת הכלי להצגה חזותית של Shadow DOM

רוצה לנסות? נשמח לשמוע מה דעתך.

מודל האירוע

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

Play Action 1

  • זה מעניין. אמור להופיע mouseout מהרכיב המארח (<div data-host>) לצומת הכחול. למרות שהוא צומת מבוזר, הוא עדיין נמצא במארח, ולא ב-ShadowDOM. אם מעבירים את העכבר למטה לחלק הצהוב, mouseout מופיע שוב בצומת הכחול.

הפעלת פעולה 2

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

פעולת הפעלה 3

  • שימו לב שכאשר לוחצים על הקלט, הערך focusin לא מופיע בקלט אלא בצומת המארח עצמו. בוצע טירגוט מחדש!

אירועים שתמיד מופסקים

האירועים הבאים אף פעם לא חוצים את גבול הצל:

  • ביטול
  • error
  • בחירה
  • שינוי
  • משקל
  • אפס
  • resize
  • scroll
  • selectstart

סיכום

אני מקווה שתהיו מסכימים שShadow DOM הוא כלי חזק מאוד. בפעם הראשונה בהיסטוריה יש לנו אנקפסולציה נכונה בלי המטען הנוסף של <iframe> או שיטות ישנות אחרות.

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

למידע נוסף, אפשר לעיין במאמר ההקדמה של דומיניק Shadow DOM 101 ובמאמר שלי Shadow DOM 201: CSS & Styling.