צל DOM 301

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

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

שימוש בכמה שורשי צל (shadow roots)

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

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

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

"נקודות הוספה של Shadow"‏ (<shadow>) דומות לנקודות הוספה רגילות (<content>) בכך שהן פלייסהולדרים. עם זאת, במקום להיות פלייסהולדרים לתוכן של מארח, הם מארחים של עצי צל אחרים. זהו 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 הישן יותר שעובר רינדור ב-<shadow>. אפשר לקבל הפניה לעץ הזה דרך .olderShadowRoot:

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

קבלת שורש הצל של מארח

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

Shadow DOM Visualizer
הפעלת Shadow DOM Visualizer

כדאי לנסות ולספר לי מה דעתך.

מודל אירועים

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

Play Action 1

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

Play Action 2

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

Play Action 3

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

אירועים שתמיד נעצרים

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

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

סיכום

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

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

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