תבנית, יחידת קיבולת (Slot) והצללה

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

התקן של Web Component מורכב משלושה חלקים – תבניות HTML, רכיבים מותאמים אישית ו-Shadow DOM. השילוב הזה מאפשר ליצור אלמנטים מותאמים אישית ועצמאיים (encapsulated) לשימוש חוזר, שניתן לשלב בצורה חלקה באפליקציות קיימות, כמו כל שאר רכיבי ה-HTML שכבר הסברנו.

בקטע הזה ניצור את רכיב <star-rating> – רכיב אינטרנט שמאפשר למשתמשים לדרג חוויה בסולם של כוכב אחד עד חמישה כוכבים. כשנותנים שם לרכיב מותאם אישית, מומלץ להשתמש באותיות קטנות בלבד. כמו כן, צריך לכלול מקף, כי כך תוכלו להבחין בין רכיבים רגילים לבין רכיבים מותאמים אישית.

במאמר זה נדון בשימוש ברכיבים <template> ו-<slot>, במאפיין slot וב-JavaScript ליצירת תבנית עם Shadow DOM מוקטן. לאחר מכן נשתמש שוב ברכיב שהוגדר, ונתאים אישית קטע טקסט, בדיוק כמו שאתם עושים כל רכיב או רכיב אינטרנט. בנוסף, נדון בשימוש ב-CSS מתוך ומחוץ לרכיב המותאם אישית.

הרכיב <template>

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

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

מאחר שהתוכן של הרכיב <template> לא נכתב במסך, הרכיב <form> והתוכן שלו לא מעובדים. כן, ה-Codepen הזה ריק, אבל אם בודקים את כרטיסיית ה-HTML, רואים את תגי העיצוב <template>.

בדוגמה הזו, <form> הוא לא צאצא של <template> ב-DOM. במקום זאת, תכנים של רכיבי <template> הם צאצאים של DocumentFragment שמוחזרים על ידי המאפיין HTMLTemplateElement.content. כדי שהתוכן יהיה גלוי, יש להשתמש ב-JavaScript כדי לשלוף את התוכן ולהוסיף אותם ל-DOM.

קוד ה-JavaScript הקצר הזה לא יצר רכיב מותאם אישית. במקום זאת, הדוגמה הזו הוסיפה את התוכן של <template> לתוך <body>. התוכן הפך לחלק מה-DOM הגלוי שניתן לסגנון.

צילום מסך של ה-codepen הקודם כפי שמוצג ב-DOM.

דרישת JavaScript כדי להטמיע תבנית עבור דירוג כוכב אחד בלבד אינה שימושית במיוחד, אבל יצירת רכיב אינטרנט לווידג'ט דירוג הכוכבים שניתן להתאמה אישית שחוזר על עצמו הוא שימושי.

הרכיב <slot>

אנחנו כוללים יחידת קיבולת (Slot) כדי לכלול מקרא בהתאמה אישית לכל אירוע. ב-HTML יש רכיב <slot> כ-placeholder בתוך <template>. אם מזינים שם, נוצר 'משבצת בעלת שם'. ניתן להשתמש במשבצת עם שם כדי להתאים אישית תוכן בתוך רכיב אינטרנט. באמצעות הרכיב <slot> אנחנו יכולים לקבוע את המיקום של רכיבי הצאצאים של הרכיב בהתאמה אישית בתוך עץ הצל שלו.

בתבנית שלנו, אנחנו משנים את <legend> ל-<slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

המאפיין name משמש להקצאת יחידות קיבולת לרכיבים אחרים אם לרכיב יש מאפיין Slot שהערך שלו תואם לשם של יחידת קיבולת (Slot) בעלת שם. אם לרכיב המותאם אישית אין התאמה של יחידת קיבולת (Slot), התוכן של <slot> יעובד. לכן כללנו <legend> עם תוכן כללי שמותר להציג אם מישהו רק כולל את <star-rating></star-rating>, ללא תוכן, ב-HTML שלו.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

המאפיין משבצת הוא מאפיין גלובלי המשמש להחלפת התוכן של <slot> בתוך <template>. ברכיב המותאם אישית שלנו, הרכיב עם מאפיין המשבצת הוא <legend>. אין צורך. בתבנית שלנו, הערך <slot name="star-rating-legend"> יוחלף ב-<anyElement slot="star-rating-legend">, כאשר <anyElement> יכול להכיל כל רכיב, אפילו רכיב מותאם אישית אחר.

רכיבים לא מוגדרים

ב-<template> שלנו השתמשנו ברכיב <rating>. הרכיב הזה לא מותאם אישית. במקום זאת, זהו רכיב לא ידוע. דפדפנים לא נכשלים כשהם לא מזהים רכיב כלשהו. הדפדפן מתייחס לרכיבי HTML לא מזוהים כרכיבים מוטבעים אנונימיים שניתן לסגנן באמצעות CSS. בדומה ל-<span>, לרכיבים <rating> ו-<star-rating> אין סגנונות או סמנטיקה של סוכן משתמש.

חשוב לדעת שהרכיבים <template> והתוכן שלהם לא מעובדים. ה-<template> הוא רכיב ידוע שמכיל תוכן שלא עובר רינדור. הרכיב <star-rating> עדיין לא הוגדר. עד שנגדיר רכיב, הדפדפן יציג אותו כמו כל הרכיבים הבלתי מזוהים. בשלב זה, ה-<star-rating> הלא מזוהה מטופל כרכיב אנונימי בתוך השורה, כך שהתוכן כולל מקרא ו-<p> ב-<star-rating> השלישי מוצג כפי שהיה אילו היו מופיעים ב-<span> במקום זאת.

נגדיר את הרכיב שלנו כך שימיר את הרכיב הבלתי מזוהה הזה לרכיב מותאם אישית.

רכיבים מותאמים אישית

כדי להגדיר רכיבים מותאמים אישית, צריך להפעיל את JavaScript. לאחר הגדרתו, התוכן של הרכיב <star-rating> יוחלף ב-root מסוג צל המכיל את כל התוכן של התבנית שנשייך אליה. רכיבי <slot> מהתבנית מוחלפים בתוכן של הרכיב בתוך <star-rating>, שערך המאפיין slot שלו תואם לערך השם של <slot>, אם יש כזה. אם לא, יוצג התוכן של יחידות הקיבולת (Slot) של התבנית.

תוכן באלמנט מותאם אישית שאינו משויך ליחידת קיבולת (Slot) — ה-<p>Is this text visible?</p> ב-<star-rating> השלישי שלנו — לא נכלל ב-shadow root ולכן לא מוצג.

אנחנו מגדירים את הרכיב המותאם אישית בשם star-rating על ידי הרחבה של HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

אחרי שהרכיב מוגדר, בכל פעם שהדפדפן ייתקל ברכיב <star-rating>, הוא יעובד כפי שהוגדר על ידי הרכיב עם #star-rating-template, שהוא התבנית שלנו. הדפדפן יצרף עץ DOM של צל לצומת, ויוסיף שכפול של תוכן התבנית ל-DOM של הצללית הזו. שימו לב שהרכיבים שבהם אפשר attachShadow() הם מוגבלים.

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

אם תסתכלו על הכלים למפתחים, תוכלו לראות שה-<form> מה-<template> הוא חלק מבסיס הצללית של כל רכיב מותאם אישית. שכפול של התוכן של <template> מופיע בכל רכיב מותאם אישית בכלים למפתחים וגלוי בדפדפן, אבל התוכן של הרכיב המותאם אישית עצמו לא מוצג במסך.

צילום מסך של כלי פיתוח שבו מוצג תוכן התבנית המשוכפלת בכל רכיב מותאם אישית.

בדוגמה <template>, הוספנו את תוכן התבנית לגוף המסמך, והוספנו את התוכן ל-DOM הרגיל. בהגדרה של customElements השתמשנו באותו appendChild(), אבל התוכן של התבנית המשוכפלת נוסף ל-DOM של צללים.

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

DOM של צל

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

מכיוון שהוספנו את התוכן ל-DOM של צל, אנחנו יכולים לכלול רכיב <style> שמספק CSS מקוצר לרכיב המותאם אישית.

בהיבט של הרכיב המותאם אישית, אנחנו לא צריכים לחשוש שסגנונות יחלחלו אל שאר המסמך. אנחנו יכולים לצמצם במידה משמעותית את הספציפיות של הבוררים. לדוגמה, רכיבי הקלט היחידים שנעשה בהם שימוש ברכיב המותאם אישית הם לחצני בחירה, ולכן אפשר להשתמש ב-input במקום ב-input[type="radio"] כבורר.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

רכיבי האינטרנט עוברים אנקפסולציה של תגי עיצוב ב-<template> וסגנונות CSS מוגבלים ל-DOM של הצללית ומוסתרים מכל מה שנמצא מחוץ לרכיבים, אבל התוכן של יחידת הקיבולת (Slot) שעובר עיבוד – החלק <anyElement slot="star-rating-legend"> של ה-<star-rating>, לא עובר אנקפסולציה.

עיצוב מחוץ להיקף הנוכחי

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

עץ הצל הוא עץ ה-DOM שבתוך ה-DOM של הצל. שורש הצל הוא צומת השורש של עץ הצללים.

הפסאודו-מחלקה :host בוחרת ב-<star-rating>, רכיב מארח הצללית. מארח הצללית הוא צומת ה-DOM שאליו מצורף ה-DOM של הצל. כדי לטרגט רק גרסאות ספציפיות של המארח, צריך להשתמש ב-:host(). הפעולה הזו תבחר רק את הרכיבים של מארח הצלליות שתואמים לפרמטר שהועבר, כמו בורר מחלקה או בורר מאפיינים. כדי לבחור את כל האלמנטים המותאמים אישית, תוכלו להשתמש ב-star-rating { /* styles */ } ב-CSS הגלובלי או ב-:host(:not(#nonExistantId)) בסגנונות התבנית. במונחים של ספציפיות, שירות ה-CSS הגלובלי מנצח.

פסאודו-הרכיב ::slotted() חוצה את גבול ה-DOM של הצל מתוך ה-DOM של הצל. אם רכיב מחורץ תואם לבורר, הוא בוחר ברכיב. בדוגמה שלנו, ::slotted(legend) תואם לשלושת המקרא שלנו.

כדי לטרגט DOM של צל מ-CSS בהיקף הגלובלי, צריך לערוך את התבנית. אפשר להוסיף את המאפיין part לכל רכיב שרוצים לעצב. לאחר מכן משתמשים בפסאודו-רכיב ::part() כדי להתאים בין רכיבים בתוך עץ צללים שתואמים לפרמטר שהועבר. רכיב העוגן או רכיב המקור של פסאודו-רכיב הוא שם המארח, או הרכיב המותאם אישית, במקרה הזה star-rating. הפרמטר הוא הערך של המאפיין part.

אם סימון התבנית שלנו התחיל כך:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

אנחנו יכולים למקד ל<form> ול<fieldset> באמצעות:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

שמות חלקים פועלים בצורה דומה למחלקות: לרכיב יכולים להיות כמה שמות חלקים שמופרדים ברווחים, ולכמה רכיבים יכול להיות אותו שם חלק.

ל-Google יש רשימת משימות מצוינת ליצירת רכיבים מותאמים אישית. מומלץ גם לקרוא על DOM DOMs הצהרתי.

בדיקת ההבנה

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

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

True.
אפשר לנסות שוב.
לא נכון.
נכון!

איזו תשובה היא התיאור הנכון של הרכיב <template>?

רכיב גנרי המשמש להצגת תוכן כלשהו בדף.
אפשר לנסות שוב.
רכיב placeholder.
אפשר לנסות שוב.
רכיב המשמש להצהרה על קטעי HTML, שלא יעובדו כברירת מחדל.
נכון!