องค์ประกอบที่กำหนดเอง 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 แนบโปรแกรมรับฟังเหตุการณ์ และอื่นๆ โปรดอ่านต่อเพื่อดูตัวอย่างเพิ่มเติม

การกําหนด JavaScript API ขององค์ประกอบ

ฟังก์ชันการทำงานขององค์ประกอบที่กําหนดเองจะกําหนดโดยใช้ ES2015 class ซึ่งขยาย 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 Listener กับตนเองได้ และคุณไม่จํากัดเพียง Listener เหตุการณ์ DOM API ทั้งหมดพร้อมใช้งานภายในโค้ดองค์ประกอบ ใช้ this เพื่อเข้าถึงพร็อพเพอร์ตี้ขององค์ประกอบ ตรวจสอบองค์ประกอบย่อย (this.children) โหนดการค้นหา (this.querySelectorAll('.items')) ฯลฯ

กฎในการสร้างองค์ประกอบที่กำหนดเอง

  1. ชื่อขององค์ประกอบที่กำหนดเองต้องมีขีดกลาง (-) ดังนั้น <x-tags>, <my-element> และ <my-awesome-app> จึงเป็นชื่อที่ถูกต้องทั้งหมด ส่วน <tabs> และ <foo_bar> นั้นไม่ถูกต้อง ข้อกำหนดนี้มีไว้เพื่อให้โปรแกรมแยกวิเคราะห์ HTML แยกองค์ประกอบที่กำหนดเองออกจากองค์ประกอบปกติได้ และยังช่วยให้ใช้งานร่วมกันได้ในอนาคตเมื่อมีการเพิ่มแท็กใหม่ลงใน HTML
  2. คุณจดทะเบียนแท็กเดียวกันได้ไม่เกิน 1 ครั้ง การพยายามดำเนินการดังกล่าวจะทำให้เกิด DOMException เมื่อแจ้งเบราว์เซอร์เกี่ยวกับแท็กใหม่แล้ว ก็เสร็จสิ้น ไม่มีการคืนสินค้า
  3. องค์ประกอบที่กำหนดเองต้องปิดเองไม่ได้ เนื่องจาก HTML อนุญาตให้องค์ประกอบเพียงไม่กี่รายการเท่านั้นที่ปิดเองได้ เขียนแท็กปิดเสมอ (<app-drawer></app-drawer>)

ความรู้สึกขององค์ประกอบที่กำหนดเอง

องค์ประกอบที่กําหนดเองสามารถกําหนดฮุกวงจรการทํางานพิเศษสําหรับการเรียกใช้โค้ดในช่วงเวลาที่น่าสนใจขององค์ประกอบ ฟีเจอร์เหล่านี้เรียกว่ารีแอ็กชันขององค์ประกอบที่กำหนดเอง

ชื่อ เรียกใช้เมื่อ
constructor สร้างหรืออัปเกรดอินสแตนซ์ขององค์ประกอบ มีประโยชน์สำหรับการเริ่มต้นรัฐ การตั้งค่า Listener เหตุการณ์ หรือการสร้าง Shadow 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 บางรายการ เช่น ตัวเลือกการช่วยเหลือพิเศษและ 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.
});

เนื้อหาที่กําหนดโดยองค์ประกอบ

องค์ประกอบที่กําหนดเองจะจัดการเนื้อหาของตนเองได้โดยใช้ DOM API ภายในโค้ดองค์ประกอบ รีแอ็กชันมีประโยชน์ในกรณีนี้

ตัวอย่าง - สร้างองค์ประกอบที่มี 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 Components เทมเพลตเป็นตัวยึดตําแหน่งที่เหมาะสมสําหรับการประกาศโครงสร้างขององค์ประกอบที่กําหนดเอง

ตัวอย่าง: การลงทะเบียนองค์ประกอบที่มีเนื้อหา 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, เมธอด, การช่วยเหลือพิเศษ) ไม่มีวิธีใดที่ดีกว่าในการเขียน 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() จะเปลี่ยนแปลงเล็กน้อยเมื่อขยายองค์ประกอบเนทีฟ พารามิเตอร์ที่ 3 ซึ่งจําเป็นจะบอกเบราว์เซอร์ว่ากําลังขยายแท็กใด ซึ่งจําเป็นเนื่องจากแท็ก 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)

แสดงผล Promise ที่จะดําเนินการเมื่อมีการกําหนดองค์ประกอบที่กําหนดเอง หากมีการกำหนดองค์ประกอบไว้แล้ว ให้แก้ไขทันที ปฏิเสธหากชื่อแท็กไม่ใช่ชื่อองค์ประกอบที่กําหนดเองที่ถูกต้อง

ตัวอย่าง

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

ประวัติและการรองรับเบราว์เซอร์

หากคุณติดตามเว็บคอมโพเนนต์ในช่วง 2-3 ปีที่ผ่านมา ก็จะทราบว่า Chrome 36 ขึ้นไปใช้ Custom Elements API เวอร์ชันที่ใช้ document.registerElement() แทน customElements.define() ซึ่งปัจจุบันถือว่าเป็นมาตรฐานเวอร์ชันที่เลิกใช้งานแล้ว เรียกว่า v0 customElements.define() เป็นเทคโนโลยีที่มาแรงและผู้ให้บริการเบราว์เซอร์เริ่มนำมาใช้งาน เรียกว่าองค์ประกอบที่กำหนดเอง v1

หากสนใจข้อกำหนดเวอร์ชันเก่า v0 โปรดดูบทความใน html5rocks

การสนับสนุนเบราว์เซอร์

Chrome 54 (สถานะ), Safari 10.1 (สถานะ) และ Firefox 63 (สถานะ) มีองค์ประกอบที่กำหนดเองเวอร์ชัน 1 Edge เริ่มการพัฒนาแล้ว

หากต้องการใช้ฟีเจอร์ตรวจหาองค์ประกอบที่กำหนดเอง ให้ตรวจสอบว่ามีรายการต่อไปนี้หรือไม่ window.customElements

const supportsCustomElementsV1 = 'customElements' in window;

โพลีฟิลล์

ในระหว่างที่เบราว์เซอร์ยังไม่รองรับอย่างแพร่หลาย เรามีโพลีฟิลล์แบบสแตนด์อโลนสำหรับองค์ประกอบที่กำหนดเองเวอร์ชัน 1 อย่างไรก็ตาม เราขอแนะนำให้ใช้ webcomponents.js loader เพื่อโหลด polyfill ของ Web Components อย่างเหมาะสม ตัวโหลดจะใช้การตรวจหาฟีเจอร์เพื่อโหลดโพลีไฟล์ที่จำเป็นเท่านั้นแบบไม่สอดคล้องกันตามที่เบราว์เซอร์ต้องการ

วิธีติดตั้ง

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 ดังนี้

  • ใช้ได้กับทุกเบราว์เซอร์ (มาตรฐานเว็บ) สำหรับการสร้างและขยายคอมโพเนนต์ที่นำมาใช้ซ้ำได้
  • ไม่ต้องใช้ไลบรารีหรือเฟรมเวิร์กเพื่อเริ่มต้นใช้งาน Vanilla JS/HTML FTW!
  • มีรูปแบบการเขียนโปรแกรมที่คุ้นเคย เป็นเพียง DOM/CSS/HTML
  • ทำงานร่วมกับฟีเจอร์อื่นๆ ของแพลตฟอร์มเว็บใหม่ได้ดี (Shadow DOM, <template>, CSS พร็อพเพอร์ตี้ที่กำหนดเอง ฯลฯ)
  • ผสานรวมกับเครื่องมือสำหรับนักพัฒนาเว็บของเบราว์เซอร์อย่างแน่นหนา
  • ใช้ประโยชน์จากฟีเจอร์การช่วยเหลือพิเศษที่มีอยู่