ה-DOM (Document Object Model) הוא ממשק תכנותי (API) עבור קוד העמוד, המייצג את הדף כמבנה עץ של אובייקטים.
כל אלמנט HTML (לדוגמה,
אך לפתיחות הזו יש חיסרון. כאשר אנו יוצרים רכיב מורכב לשימוש חוזר (לדוגמה, נגן וידאו מותאם אישית או ווידג'ט לוח שנה), המבנה הפנימי והסגנונות שלו הופכים לפגיעים. סגנונות CSS מהדף הראשי יכולים בטעות "לזלוג" לתוך הרכיב ולשבור את המראה שלו. באופן דומה, קוד ה-JavaScript של הדף יכול לשנות בשוגג את האלמנטים הפנימיים של הרכיב, ובכך לשבש את הלוגיקה שלו.
כדי לפתור בעיה זו, קיים Shadow DOM.
במהותו, Shadow DOM הוא "DOM בתוך DOM". זהו עץ אלמנטים נסתר המוצמד לאלמנט רגיל בדף (הנקרא "המארח" – host) אך מבודד מה-DOM הראשי. הוא מאפשר למפתח ליצור גבול אטום סביב המבנה הפנימי של רכיב, ובכך להגן עליו מהעולם החיצון.
Shadow DOM מאפשר לך להצמיד עצי DOM נסתרים לאלמנטים בעץ ה-DOM הרגיל. עץ הצל הזה מתחיל בשורש צל (shadow root), שתחתיו ניתן להוסיף כל אלמנט, בדיוק כמו ב-DOM הרגיל.
ישנם מספר מונחים מרכזיים הקשורים ל-Shadow DOM שכדאי להכיר. מארח הצל (Shadow host) הוא צומת ה-DOM הרגיל שאליו מוצמד ה-Shadow DOM. עץ ה-DOM בתוך ה-Shadow DOM ידוע בשם עץ הצל (Shadow tree). הנקודה שבה ה-Shadow DOM מסתיים וה-DOM הרגיל מתחיל נקראת גבול הצל (Shadow boundary). לבסוף, הצומת השורשי של עץ הצל הוא שורש הצל (Shadow root).

ניתן לתפעל צמתים בתוך ה-Shadow DOM בדיוק כמו צמתים רגילים. ההבדל הוא ששום קוד בתוך ה-Shadow DOM אינו יכול להשפיע על שום דבר מחוצה לו, מה שמבטיח כימוס (encapsulation) אמין.
לפני שה-Shadow DOM הפך זמין למפתחי אינטרנט, דפדפנים כבר השתמשו בו כדי לכמס את המבנה הפנימי של אלמנטים סטנדרטיים. לדוגמה, אלמנט ה-<video> עם הפקדים. כל מה שרואים ב-DOM הוא תג ה-<video>, אך הוא מכיל מספר כפתורים ופקדים אחרים בתוך ה-Shadow DOM שלו.
יצירת Shadow DOM
ניתן ליצור Shadow DOM באופן אימפרטיבי באמצעות JavaScript או באופן דקלרטיבי ישירות ב-HTML.
באופן אימפרטיבי עם JavaScript
שיטה זו מושלמת ליישומים המרונדרים בצד הלקוח. אנו בוחרים את האלמנט המארח וקוראים עליו למתודה attachShadow().
<!-- סימון HTML -->
<div id="host"></div>
<span>אני לא ב-Shadow DOM</span>
// מצא את המארח והצמד Shadow DOM
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
// צור והוסף אלמנטים לעץ הצל
const span = document.createElement("span");
span.textContent = "אני בתוך ה-Shadow DOM";
shadow.appendChild(span);
התוצאה בדף תיראה כך:
אני בתוך ה-Shadow DOM
אני לא ב-Shadow DOM
באופן דקלרטיבי עם HTML
עבור יישומים שבהם רינדור בצד השרת חשוב, ניתן להגדיר Shadow DOM באופן דקלרטיבי באמצעות אלמנט <template> עם התכונה shadowrootmode.
<div id="host">
<template shadowrootmode="open">
<p>פסקה זו נמצאת בתוך ה-Shadow DOM.</p>
<style>
p { color: red; } /* סגנונות אלו יהיו מבודדים */
</style>
</template>
</div>
כאשר הדפדפן מעבד קוד זה, הוא יוצר באופן אוטומטי שורש צל עבור ה-<div> וממקם את תוכן ה-<template> בתוכו. תג ה-<template> עצמו נעלם מעץ ה-DOM הראשי.
כימוס: הגנה מפני JavaScript ו-CSS
היתרון העיקרי של Shadow DOM הוא בידוד. בואו נראה איך זה עובד.
כימוס JavaScript
נוסיף כפתור שמנסה לשנות את כל אלמנטי ה-<span> בדף.
// ... קוד יצירת ה-Shadow DOM ...
const upper = document.querySelector("#upper-button");
upper.addEventListener("click", () => {
// סלקטור זה מחפש בכל המסמך
const spans = document.querySelectorAll("span");
for (const span of spans) {
span.textContent = span.textContent.toUpperCase();
}
});
כאשר לוחצים על הכפתור, רק ה-<span> במסמך הראשי משתנה. האלמנט בתוך ה-Shadow DOM נשאר ללא שינוי מכיוון ש-document.querySelectorAll() אינו יכול "להסתכל" מעבר לגבול הצל.
גישה ל-Shadow DOM: המאפיין shadowRoot ועצים מקוננים
כאשר אנו קוראים ל-host.attachShadow({ mode: "open" }), אנו יוצרים Shadow DOM "פתוח". זה אומר שאנו יכולים לגשת לתוכן שלו מבחוץ דרך host.shadowRoot.
// מצא span-ים רק בתוך עץ הצל של מארח ספציפי
const spansInShadow = host.shadowRoot.querySelectorAll("span");
אם משתמשים ב-mode: "closed", המאפיין host.shadowRoot יחזיר null, והגישה החיצונית לעץ הצל נחסמת. זה אינו מנגנון אבטחה קפדני, אלא יותר מוסכמה עבור מפתחים שהחלקים הפנימיים של הרכיב אינם אמורים להיות נגישים.
עבודה עם עצי צל מקוננים
בארכיטקטורות רכיבים מורכבות, אלמנט מותאם אישית אחד יכול להכיל אלמנטים מותאמים אישית אחרים, שלכל אחד מהם Shadow DOM משלו. כדי לגשת לאלמנט בעץ צל מקונן עמוק, יש לעבור דרך כל shadowRoot באופן סדרתי.
מבנה לדוגמה הוא רכיב <nmbrs-form> (הטופס הראשי), שבתוכו יש <div> המכיל <nmbrs-button> (כפתור מותאם אישית), ובתוך הכפתור המותאם אישית הזה יש אלמנט <button> HTML אמיתי.
כדי לגשת לכפתור זה מההקשר הגלובלי, הנתיב הוא:
// 1. מצא את רכיב השורש במסמך הראשי
const formComponent = document.querySelector('nmbrs-form');
// 2. היכנס לעץ הצל שלו
const shadowRoot1 = formComponent.shadowRoot;
// 3. מצא את רכיב הכפתור המקונן
const buttonComponent = shadowRoot1.querySelector('div div.btn-container nmbrs-button');
// 4. היכנס לעץ הצל של רכיב זה
const shadowRoot2 = buttonComponent.shadowRoot;
// 5. לבסוף, מצא את אלמנט הכפתור
const finalButton = shadowRoot2.querySelector('button#button');
כשרשרת אחת:
const button = document.querySelector('nmbrs-form').shadowRoot
.querySelector('div div.btn-container nmbrs-button').shadowRoot
.querySelector('button#button');
שרשרת ארוכה זו מדגימה את כוחו של הכימוס: כדי להגיע לפרטים פנימיים, יש לעבור במפורש דרך כל "גבול". זה הופך את הקוד לצפוי יותר ומגן על רכיבים מפני שינויים מקריים.
כימוס CSS
סגנונות המוגדרים בדף הראשי אינם משפיעים על אלמנטים בתוך ה-Shadow DOM.
/* סגנון זה חל רק על span-ים במסמך הראשי */
span {
color: blue;
border: 1px solid black;
}
ה-<span> בתוך עץ הצל לא יקבל את הסגנונות הללו. זה פותר את בעיית התנגשויות הסגנון המקריות.
עיצוב בתוך ה-Shadow DOM
סגנונות המוגדרים בתוך עץ הצל, בתורם, אינם משפיעים על הדף הראשי. ישנן שתי דרכים עיקריות להוסיף אותם.
1. גיליונות סגנון בנייה (Constructable Stylesheets)
שיטה זו מאפשרת ליצור אובייקט CSSStyleSheet ב-JavaScript ולהחיל אותו על עץ צל אחד או יותר. זה יעיל אם יש לך סגנונות משותפים לרכיבים רבים.
const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black; }");
const shadow = host.attachShadow({ mode: "open" });
// החל את גיליון הסגנונות על שורש הצל
shadow.adoptedStyleSheets = [sheet];
2. אלמנט <style>
דרך דקלרטיבית פשוטה היא למקם תג <style> ישירות בתוך עץ הצל (לרוב בתוך <template>).
<template id="my-element">
<style>
span {
color: red;
border: 2px dotted black;
}
</style>
<span>אני בתוך ה-Shadow DOM</span>
</template>
Shadow DOM ורכיבים מותאמים אישית (Custom Elements): שילוב מושלם
הכוח המלא של ה-Shadow DOM מתגלה בעת יצירת רכיבים מותאמים אישית (Custom Elements). ללא כימוס, הם היו שבירים ביותר.
רכיב מותאם אישית הוא מחלקה המרחיבה את HTMLElement. בדרך כלל, האלמנט עצמו משמש כמארח הצל, והמבנה הפנימי שלו נוצר בתוך עץ הצל.
דוגמה לרכיב פשוט <filled-circle>:
class FilledCircle extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
// יצירת המימוש הפנימי (למשל, עיגול SVG)
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("r", "50");
circle.setAttribute("cx", "50");
circle.setAttribute("cy", "50");
// הצבע מגיע מהתכונה של המארח
circle.setAttribute("fill", this.getAttribute("color"));
svg.appendChild(circle);
shadow.appendChild(svg);
}
}
customElements.define("filled-circle", FilledCircle);
כעת אנו יכולים להשתמש בו ב-HTML כמו תג רגיל מבלי לדאוג למבנה הפנימי שלו:
<filled-circle color="blue"></filled-circle>
<filled-circle color="green"></filled-circle>
כל אחד מהרכיבים הללו יהיה מכומס לחלוטין ומוגן מהדף החיצוני.