רכיבים מותאמים אישית 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>).

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

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

שם הקריאה מתבצעת כאשר
constructor נוצרת מופע של הרכיב או שהוא משודרג. שימושי לטעינה של המצב, להגדרת פונקציות מעקב אירועים או ליצירת DOM בצל. במפרט מפורטות ההגבלות על הפעולות שאפשר לבצע ב-constructor.
connectedCallback הפונקציה נקראת בכל פעם שהרכיב מוכנס ל-DOM. שימושי להרצת קוד הגדרה, כמו אחזור משאבים או עיבוד. באופן כללי, כדאי לדחות את העבודה עד לזמן הזה.
disconnectedCallback הפונקציה נקראת בכל פעם שהאלמנט מוסר מה-DOM. שימושי להרצת קוד לניקוי.
attributeChangedCallback(attrName, oldVal, newVal) הקריאה מתבצעת כשמאפיין שנצפה נוסף, הוסר, עודכן או הוחלף. הקריאה מתבצעת גם עבור ערכים ראשוניים כשהרכיב נוצר על ידי המנתח או משודרג. הערה: רק מאפיינים שרשומים בנכס observedAttributes יקבלו את הקריאה החוזרת הזו.
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 - קוד דוגמת קוד הוסר כאשר הוא משתמש ברכיבי handler של אירועים מוטבעים

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

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

דוגמה: רישום רכיב עם תוכן של 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 אם הם נוצרים עם שם תקין (כולל "-"). אפשר לבדוק זאת בדפדפן שתומך באלמנטים מותאמים אישית. פותחים את מסוף Google Cloud:‏ 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() הוא הטרנד החדש, וספקי הדפדפנים מתחילים להטמיע אותו. הוא נקרא Custom Elements v1.

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