רכיבים מותאמים אישית v1 – רכיבי אינטרנט לשימוש חוזר

רכיבים מותאמים אישית מאפשרים למפתחי אתרים להגדיר תגי HTML חדשים, להרחיב תגים קיימים וליצור רכיבי אינטרנט לשימוש חוזר.

בעזרת רכיבים מותאמים אישית, מפתחי אינטרנט יכולים ליצור תגי HTML חדשים, לשפר תגי HTML קיימים או להרחיב את הרכיבים שפותחו על ידי מפתחים אחרים. ה-API הוא הבסיס של רכיבי אינטרנט. הוא מספק דרך מבוססת-תקנים ליצירת רכיבים לשימוש חוזר, באמצעות JS/HTML/CSS רגילים בלבד. התוצאה היא פחות קוד, קוד מודולרי ושימוש חוזר נרחב יותר באפליקציות שלנו.

מבוא

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

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

הגדרת רכיב חדש

כדי להגדיר רכיב HTML חדש, אנחנו צריכים את העוצמה של JavaScript!

המשתנה הגלובלי customElements משמש להגדרת רכיב מותאם אישית ולהדרכת הדפדפן לגבי תג חדש. קוראים ל-customElements.define() עם שם התג שרוצים ליצור ועם קוד JavaScript ‏class שמרחיב את הבסיס HTMLElement.

דוגמה – הגדרת חלונית של מגירה בנייד, <app-drawer>:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

דוגמה לשימוש:

<app-drawer></app-drawer>

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

הגדרת ממשק API של JavaScript של רכיב

הפונקציונליות של אלמנט מותאם אישית מוגדרת באמצעות class של ES2015 שמרחיב את HTMLElement. הרחבה של HTMLElement מבטיחה שהרכיב המותאם אישית יקבל בירושה את כל DOM API, ומשמעות הדבר היא שכל המאפיינים או השיטות שתוסיפו לכיתה יהיו חלק מממשק ה-DOM של הרכיב. בעיקרון, משתמשים בכיתה כדי ליצור JavaScript API ציבורי לתג.

דוגמה – הגדרת ממשק ה-DOM של <app-drawer>:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

בדוגמה הזו אנחנו יוצרים מגירה עם מאפיין open, מאפיין disabled ו-method‏ toggleDrawer(). הוא גם משקף מאפיינים בתור מאפייני HTML.

תכונה מעניינת של רכיבים מותאמים אישית היא ש-this בתוך הגדרת הכיתה מתייחס לרכיב ה-DOM עצמו, כלומר למופעים של הכיתה. בדוגמה שלנו, הערך של this מתייחס ל-<app-drawer>. כך (😉) המרכיב יכול לצרף לעצמו מאזין click. בנוסף, אין הגבלה על פונקציות event listener. כל DOM API זמין בקוד הרכיב. אפשר להשתמש ב-this כדי לגשת למאפיינים של הרכיב, לבדוק את צאצאיו (this.children), לבצע שאילתות על צמתים (this.querySelectorAll('.items')) וכו'.

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

  1. השם של אלמנט מותאם אישית חייב להכיל מקף (-). לכן, <x-tags>, ‏<my-element> ו-<my-awesome-app> הם שמות תקינים, אבל <tabs> ו-<foo_bar> הם לא. הדרישות האלה נדרשות כדי שמנתח ה-HTML יוכל להבדיל בין רכיבים מותאמים אישית לבין רכיבים רגילים. הוא גם מבטיח תאימות עתידית כשמוסיפים תגים חדשים ל-HTML.
  2. אי אפשר לרשום את אותו תג יותר מפעם אחת. ניסיון לעשות זאת יוביל להשלכת DOMException. אחרי שמעדכנים את הדפדפן על תג חדש, זהו. אין אפשרות להחזיר מוצרים.
  3. אי אפשר לסגור רכיבים מותאמים אישית באופן אוטומטי כי ב-HTML מותר לסגור באופן אוטומטי רק כמה רכיבים. תמיד צריך לכתוב תג סוגר (<app-drawer></app-drawer>).

תגובות לרכיבים בהתאמה אישית

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

שם הקריאה מתבצעת כאשר
constructor נוצרת מופע של הרכיב או שהוא משודרג. שימושי לטעינה של המצב, להגדרת פונקציות מעקב אירועים או ליצירת DOM בצל. במפרט מפורטות ההגבלות על הפעולות שאפשר לבצע ב-constructor.
connectedCallback הפונקציה נקראת בכל פעם שהרכיב מוכנס ל-DOM. שימושי להרצת קוד הגדרה, כמו אחזור משאבים או עיבוד. באופן כללי, כדאי לדחות את העבודה עד לזמן הזה.
disconnectedCallback הפונקציה נקראת בכל פעם שהרכיב מוסר מה-DOM. שימושי להרצת קוד לניקוי.
attributeChangedCallback(attrName, oldVal, newVal) הקריאה מתבצעת כשמאפיין שנצפה נוסף, הוסר, עודכן או הוחלף. הקריאה מתבצעת גם עבור ערכים ראשוניים כשרכיב נוצר על ידי המנתח או משודרג. הערה: רק למאפיינים שמפורטים בנכס observedAttributes תישלח קריאה חוזרת (callback).
adoptedCallback הרכיב המותאם אישית הועבר ל-document חדש (למשל, document.adoptNode(el)).

הקריאות החוזרות של התגובות הן סינכרוניות. אם משתמש יבצע קריאה ל-el.setAttribute() ברכיב, הדפדפן יבצע קריאה מיידית ל-attributeChangedCallback(). באופן דומה, תקבלו אירוע disconnectedCallback() מיד אחרי שהרכיב יוסר מה-DOM (למשל, המשתמש קורא ל-el.remove()).

דוגמה: הוספת תגובות של רכיבים מותאמים אישית ל-<app-drawer>:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

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

מאפיינים ומאפיינים משניים

שיקוף מאפיינים למאפיינים

לרוב, מאפייני HTML משקפים את הערך שלהם בחזרה ל-DOM כמאפיין HTML. לדוגמה, כשהערכים של hidden או id משתנים ב-JS:

div.id = 'my-id';
div.hidden = true;

הערכים חלים על ה-DOM הפעיל כמאפיינים:

<div id="my-id" hidden>

הפעולה הזו נקראת העברת מאפיינים למאפיינים. כמעט כל נכס ב-HTML עושה זאת. למה? מאפיינים שימושיים גם להגדרת אלמנט באופן דקלרטיבי, וממשקי API מסוימים, כמו ממשקי API לנגישות וסלקטורים של CSS, מסתמכים על מאפיינים כדי לפעול.

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

כדאי לעיין ב-<app-drawer> שלנו. צרכן הרכיב הזה עשוי לרצות להעלות אותו בהדרגה ו/או למנוע אינטראקציה של משתמשים כשהרכיב מושבת:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

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

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

מעקב אחרי שינויים במאפיינים

מאפייני HTML הם דרך נוחה למשתמשים להצהיר על מצב ראשוני:

<app-drawer open disabled></app-drawer>

כדי שהרכיבים יוכלו להגיב לשינויים במאפיינים, צריך להגדיר attributeChangedCallback. הדפדפן יפעיל את השיטה הזו לכל שינוי במאפיינים שמפורטים במערך observedAttributes.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

בדוגמה הזו, אנחנו מגדירים מאפיינים נוספים ב-<app-drawer> כשמאפיין disabled משתנה. אנחנו לא עושים זאת כאן, אבל אפשר גם להשתמש ב-attributeChangedCallback כדי לשמור על סנכרון בין נכס JS למאפיין שלו.

שדרוגי רכיבים

HTML משופר באופן הדרגתי

כבר למדנו שרכיבים מותאמים אישית מוגדרים באמצעות קריאה ל-customElements.define(). עם זאת, אין צורך להגדיר ולרשום רכיב בהתאמה אישית בבת אחת.

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

שיפור הדרגתי הוא תכונה של רכיבים מותאמים אישית. במילים אחרות, אפשר להצהיר על כמה רכיבי <app-drawer> בדף ולא להפעיל את customElements.define('app-drawer', ...) עד הרבה יותר מאוחר. הסיבה לכך היא שהדפדפן מתייחס לרכיבים מותאמים אישית פוטנציאליים באופן שונה, בזכות תגים לא מוכרים. התהליך של קריאה ל-define() והוספת הגדרת כיתה לרכיב קיים נקרא 'שדרוג רכיבים'.

כדי לדעת מתי שם התג מוגדר, אפשר להשתמש ב-window.customElements.whenDefined(). הפונקציה מחזירה Promise שמתבצע כשהרכיב מוגדר.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

דוגמה – עיכוב העבודה עד שקבוצה של רכיבי צאצא תשודרג

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

תוכן שמוגדר על ידי רכיב

רכיבים מותאמים אישית יכולים לנהל את התוכן שלהם באמצעות ממשקי ה-API של DOM בתוך קוד הרכיב. תגובות יכולות לעזור לכם בכך.

דוגמה – יצירת רכיב עם קוד HTML מסוים שמוגדר כברירת מחדל:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

ההצהרה על התג הזה תיצור:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// TODO: DevSite - Code sample removed as it used inline event handlers

יצירת רכיב שמשתמש ב-Shadow DOM

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

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

כדי להשתמש ב-Shadow DOM ברכיב מותאם אישית, צריך להפעיל את this.attachShadow בתוך constructor:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

דוגמה לשימוש:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

טקסט מותאם אישית של המשתמש

// TODO: DevSite - Code sample removed as it used inline event handlers

יצירת רכיבים מ-<template>

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

דוגמה: רישום רכיב עם תוכן של Shadow DOM שנוצר מ-<template>:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

שורות הקוד הקצרות האלה עושות עבודה נהדרת. אלה הדברים העיקריים שחשוב להבין:

  1. אנחנו מגדירים רכיב חדש ב-HTML: <x-foo-from-template>
  2. ה-Shadow DOM של הרכיב נוצר מ-<template>
  3. ה-DOM של האלמנט הוא מקומי לאלמנט בזכות Shadow DOM
  4. ה-CSS הפנימי של הרכיב מוגבל לרכיב בזכות Shadow DOM

אני נמצא ב-Shadow DOM. תגי העיצוב שלי חותמת מתוך <template>.

// TODO: DevSite - Code sample removed as it used inline event handlers

עיצוב רכיב בהתאמה אישית

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

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

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

עיצוב מראש של רכיבים לא רשומים

לפני שמשדרגים רכיב, אפשר לטרגט אותו ב-CSS באמצעות פסאודו-הקלאס :defined. האפשרות הזו שימושית כדי להגדיר מראש סגנון לרכיב. לדוגמה, כדי למנוע FOUC של פריסה או רכיבים חזותיים אחרים, אפשר להסתיר רכיבים לא מוגדרים ולהציג אותם בהדרגה כשהם מוגדרים.

דוגמה – הסתרת <app-drawer> לפני ההגדרה שלו:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

אחרי שהערך של <app-drawer> מוגדר, הבורר (app-drawer:not(:defined)) כבר לא תואם.

הרחבת רכיבים

ה-Custom Elements API שימושי ליצירת רכיבי HTML חדשים, אבל הוא גם שימושי להרחבת רכיבים מותאמים אישית אחרים או אפילו את ה-HTML המובנה בדפדפן.

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

כדי להרחיב אלמנט מותאם אישית אחר, צריך להרחיב את הגדרת הכיתה שלו.

דוגמה – יצירת <fancy-app-drawer> שמרחיב את <app-drawer>:

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

הרחבת רכיבי HTML מקומיים

נניח שרציתם ליצור <button> מעוצב יותר. במקום לשכפל את ההתנהגות והפונקציונליות של <button>, עדיף לשפר בהדרגה את הרכיב הקיים באמצעות רכיבים מותאמים אישית.

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

כדי להרחיב רכיב, צריך ליצור הגדרת כיתה שעוברת בירושה מממשק ה-DOM הנכון. לדוגמה, רכיב בהתאמה אישית שמרחיב את <button> צריך לרשת מ-HTMLButtonElement במקום מ-HTMLElement. באופן דומה, רכיב שמרחיב את <img> צריך להרחיב את HTMLImageElement.

דוגמה – הרחבה של <button>:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

שימו לב שהקריאה ל-define() משתנה מעט כשמרחיבים רכיב מקורי. הפרמטר השלישי הנדרש מאפשר לדפדפן לדעת איזה תג אתם מרחיבים. הדבר נחוץ כי לתגי HTML רבים יש את אותו ממשק DOM. <section>,‏ <address> ו-<em> (בין היתר) משתפים את HTMLElement,‏ <q> ו-<blockquote> משתפים את HTMLQuoteElement וכו'. כשמציינים את {extends: 'blockquote'}, הדפדפן יודע שאתם יוצרים <blockquote> משודרג במקום <q>. הרשימה המלאה של ממשקי ה-DOM של HTML מפורטת במפרט HTML.

צרכנים של רכיב מובנה בהתאמה אישית יכולים להשתמש בו בכמה דרכים. הם יכולים להצהיר על כך על ידי הוספת המאפיין is="" לתג המקורי:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

יוצרים מכונה ב-JavaScript:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

או להשתמש באופרטור new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

דוגמה נוספת להרחבה של <img>.

דוגמה – הרחבה של <img>:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

המשתמשים מגדירים את הרכיב הזה בתור:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

או יוצרים מכונה ב-JavaScript:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

פרטים שונים

רכיבים לא ידועים לעומת רכיבים מותאמים אישית לא מוגדרים

קל לעבוד עם HTML, והוא גמיש. לדוגמה, אפשר להצהיר על <randomtagthatdoesntexist> בדף והדפדפן יקבל את ההצהרה הזו בברכה. למה תגים לא סטנדרטיים פועלים? התשובה היא שמפרט ה-HTML מאפשר זאת. רכיבים שלא מוגדרים במפרט מנותחים בתור HTMLUnknownElement.

הדבר לא נכון לגבי רכיבים מותאמים אישית. אלמנטים מותאמים אישית פוטנציאליים מנותחים בתור HTMLElement אם הם נוצרים עם שם תקין (כולל "-"). אפשר לבדוק זאת בדפדפן שתומך באלמנטים מותאמים אישית. פותחים את מסוף Chrome:‏ Ctrl+Shift+J (או Cmd+Opt+J ב-Mac) ומדביקים את שורות הקוד הבאות:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

הפניית API

המשתנה הגלובלי customElements מגדיר שיטות שימושיות לעבודה עם אלמנטים בהתאמה אישית.

define(tagName, constructor, options)

הגדרת רכיב מותאם אישית חדש בדפדפן.

דוגמה

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

פונקציה שמקבלת שם תג תקין של אלמנט בהתאמה אישית ומחזירה את ה-constructor של האלמנט. הפונקציה מחזירה את הערך undefined אם לא נרשם הגדרת רכיב.

דוגמה

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

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

דוגמה

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

היסטוריה ותמיכה בדפדפנים

אם אתם עוקבים אחרי רכיבי אינטרנט בשנים האחרונות, אתם יודעים שגרסה של Custom Elements API הופעלה ב-Chrome מגרסה 36 ואילך, והיא משתמשת ב-document.registerElement() במקום ב-customElements.define(). הגרסה הזו נחשבת עכשיו לגרסה שהוצאה משימוש של התקן, שנקראת v0. customElements.define() הוא הטרנד החדש, וספקי הדפדפנים מתחילים להטמיע אותו. הוא נקרא 'רכיבים מותאמים אישית גרסה 1'.

אם אתם מעוניינים במפרט הקודם של גרסה 0, תוכלו לקרוא את המאמר ב-html5rocks.

תמיכה בדפדפנים

גרסה 1 של רכיבי Custom Elements זמינה ב-Chrome 54 (סטטוס), ב-Safari 10.1 (סטטוס) וב-Firefox 63 (סטטוס). התחלנו לפתח את Edge.

כדי לזהות אלמנטים מותאמים אישית, בודקים אם הערך של window.customElements הוא:

const supportsCustomElementsV1 = 'customElements' in window;

פוליפיל

עד שתהיה תמיכה רחבה בדפדפנים, יש polyfill עצמאי שזמין ל-Custom Elements v1. עם זאת, מומלץ להשתמש ב-webcomponents.js loader כדי לטעון בצורה אופטימלית את ה-polyfills של רכיבי ה-Web. הטעינה מתבצעת באופן אסינכררוני, והמבצע משתמש בזיהוי תכונות כדי לטעון רק את ה-polyfills הנחוצים לדפדפן.

מתקינים אותו:

npm install --save @webcomponents/webcomponentsjs

שימוש:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

סיכום

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

  • רכיבים בדפדפנים שונים (תקן אינטרנט) ליצירה ולהרחבה של רכיבים לשימוש חוזר.
  • אין צורך בספרייה או ב-framework כדי להתחיל. Vanilla JS/HTML FTW!
  • מודל תכנות מוכר. זה פשוט DOM/CSS/HTML.
  • עובדת היטב עם תכונות אחרות חדשות בפלטפורמת האינטרנט (Shadow DOM,‏ <template>, מאפייני CSS בהתאמה אישית וכו')
  • שילוב הדוק עם כלי הפיתוח של הדפדפן.
  • ניצול תכונות נגישות קיימות.