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

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

באמצעות רכיבים מותאמים אישית, מפתחי אתרים יכולים ליצור תגי HTML חדשים, לשפר את תגי ה-HTML הקיימים, או להרחיב את הרכיבים שיש למפתחים אחרים שנוצר. ה-API הוא הבסיס של האינטרנט רכיבים. הוא מביא מבוססת על סטנדרטים ליצירת רכיבים לשימוש חוזר vanilla 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 של רכיב

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

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() היא מחזירה הבטחה שניתנת לפתרון כאשר הופך למוגדר.

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 של Shadow DOM

התכונה Shadow 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 באינטרנט של רכיבים שונים. תבניות הן מדד אידיאלי להצהרה של רכיב מותאם אישית.

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

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

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

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

גם אם הרכיב שלך מגדיר סגנון משלו באמצעות 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, שיטות, נגישות). אין דרך טובה יותר לכתוב אינטרנט מתקדם 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>. לעיון בקובץ ה-HTML מפרט לרשימה המלאה של ממשקי ה-DOM של 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 הוא גמיש וגמיש לעבודה. לדוגמה, הצהרה (declare) <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)

אם נותנים שם חוקי לתג של רכיב מותאם אישית, הפונקציה מחזירה את ה-constructor של הרכיב. הפונקציה מחזירה את הערך 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 (status), Safari 10.1 (סטטוס), ו יש ב-Firefox 63 (סטטוס) רכיבים מותאמים אישית גרסה 1. ב-Edge ופיתוח.

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

const supportsCustomElementsV1 = 'customElements' in window;

פוליפיל

עד שהתמיכה בדפדפן תהיה זמינה לכולם, פולי מילוי עצמאי זמינה לרכיבים מותאמים אישית גרסה 1. עם זאת, מומלץ להשתמש ברכיב webcomponents.js loader כדי לטעון באופן אופטימלי את רכיבי ה-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 מאפיינים מותאמים אישית וכו')
  • משולב היטב עם כלי הפיתוח של הדפדפן.
  • מינוף תכונות הנגישות הקיימות.