צל 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. אפשר לבדוק את ההתנהגות הזו בכלי הפיתוח.

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

נקודות הכנסה של צל

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

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

Play Action 3

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

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

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

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

סיכום

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

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

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