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

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

אריק בידלמן

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

מבוא

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

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

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

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

המאפיין הגלובלי customElements משמש להגדרה של אלמנט מותאם אישית ולהעברת הדפדפן ללמוד על תג חדש. קוראים ל-customElements.define() עם שם התג שרוצים ליצור ו-class של JavaScript שמרחיב את הבסיס 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, לצרף אליהן פונקציות event listener וכו'. המשיכו לקרוא כדי לראות עוד דוגמאות.

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

הפונקציונליות של רכיב מותאם אישית מוגדרת באמצעות 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 ושיטת toggleDrawer(). הוא גם משקף מאפיינים כמאפייני HTML.

תכונה מצוינת של רכיבים מותאמים אישית היא ש-this בתוך הגדרת מחלקה מתייחס לרכיב ה-DOM עצמו, כלומר המופע של המחלקה. בדוגמה שלנו, this מתייחס ל-<app-drawer>. כך האלמנט (😉) יכול לצרף לעצמו רכיב מאזין של click! ואתם לא מוגבלים רק למאזינים לאירועים. כל ה-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 מופע של הרכיב נוצר או משודרג. שימושי לאתחול המצב, להגדרת event listener או ליצירה של shadow dom. אפשר לעיין במפרט כדי לראות מהן ההגבלות על הפעולות שאפשר לבצע בconstructor.
connectedCallback מתבצעת קריאה בכל פעם שהאלמנט מוכנס ל-DOM. שימושי להרצת קוד הגדרה, כמו אחזור משאבים או רינדור. באופן כללי, מומלץ לעכב את העבודה עד למועד הזה.
disconnectedCallback מתבצעת קריאה בכל פעם שהרכיב מוסר מה-DOM. שימושי להפעלת קוד ניקוי.
attributeChangedCallback(attrName, oldVal, newVal) מופעלת כשמוסיפים, מסירים, מעדכנים או מחליפים מאפיין שנצפה. היא נקראת גם לערכים ראשוניים כשרכיב נוצר על ידי מנתח הנתונים, או משודרג. הערה: רק המאפיינים שרשומים בנכס observedAttributes יקבלו את הקריאה החוזרת הזו.
adoptedCallback הרכיב בהתאמה אישית הועבר אל document חדש (למשל, מישהו בשם document.adoptNode(el)).

קריאות חוזרות (callback) של תגובות הן סינכרוניות. אם מישהו יתקשר ל-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 מסוימים, כמו נגישות ובוררי 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 - דוגמת הקוד הוסרה מכיוון שנעשה בה שימוש ברכיבי handler של אירועים מוטבעים

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

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

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

כדי להשתמש ב-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 פרימטיבי במשפחת רכיבי האינטרנט. Templates הם placeholder אידיאלי להצהרה על המבנה של רכיב מותאם אישית.

דוגמה: רישום רכיב עם תוכן DOM של Shadow שנוצר מ-<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. ה-DOM של ההצללה של הרכיב נוצר מ-<template>
  3. ה-DOM של הרכיב הוא מקומי לרכיב הודות ל-DOM של Shadow
  4. ה-CSS הפנימי של הרכיב בהיקף של הרכיב הודות ל-DOM של Shadow

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

// TODO: DevSite - דוגמת הקוד הוסרה מכיוון שנעשה בה שימוש ברכיבי handler של אירועים מוטבעים

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

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

<!-- 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 אם לרכיב יש סגנונות שמוגדרים ב-DOM DOM. מבחינת ספציפיות, סגנונות המשתמשים מנצחים. הן תמיד יעקפו את הסגנון שהוגדר על-ידי הרכיב. עיינו בקטע יצירת רכיב שמשתמש ב-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, שיטות, נגישות). הדרך הכי טובה לכתוב Progressive Web App היא הדרך להשפר בהדרגה רכיבי 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 אם הם נוצרים עם שם חוקי (כולל התו '-'). אפשר לבדוק זאת בדפדפן שתומך ברכיבים מותאמים אישית. מפעילים את המסוף: 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)

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

דוגמה

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

whenDefined(tagName)

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

דוגמה

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

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

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

אם אתה מתעניין במפרט הישן של v0, עיין במאמר בנושא HTML5rocks.

תמיכת דפדפן

ב-Chrome 54 (סטטוס), ב-Safari 10.1 (סטטוס) וב-Firefox 63 (סטטוס) יש Custom Elements v1. Edge התחילו לפתח.

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

const supportsCustomElementsV1 = 'customElements' in window;

פוליפיל

עד שהתמיכה בדפדפן תהיה זמינה לכולם, תוכלו להשתמש ב-Polyfill עצמאי שזמין ל-Custom Elements v1. עם זאת, מומלץ להשתמש בטוען ה-webcomponents.js כדי לטעון באופן אופטימלי את ה-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>, אפשר להבין את התמונה הגדולה של רכיבי האינטרנט:

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