องค์ประกอบที่กำหนดเอง 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 เหตุการณ์ หรือการสร้างขอบเขตเงา ดูข้อจำกัดเกี่ยวกับสิ่งที่คุณทำได้ใน 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 ใน 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() โดยจะแสดงผล "คำสัญญา" ที่ได้รับการแก้ไขเมื่อกำหนดองค์ประกอบแล้ว

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>

// สิ่งที่ต้องทำ: DevSite - นำตัวอย่างโค้ดออกเนื่องจากใช้เครื่องจัดการเหตุการณ์ในบรรทัด

การสร้างองค์ประกอบที่ใช้ 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)) ไม่ตรงกันอีกต่อไป

ขยายองค์ประกอบ

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)

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

ตัวอย่าง

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 (status), Safari 10.1 (สถานะ) และ Firefox 63 (สถานะ) มี องค์ประกอบที่กำหนดเอง v1 Edge ได้เริ่มการพัฒนาแล้ว

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

const supportsCustomElementsV1 = 'customElements' in window;

โพลีฟิลล์

เรามี Polyfill แบบสแตนด์อโลนสำหรับองค์ประกอบที่กำหนดเอง v1 จนกว่าจะมีการรองรับเบราว์เซอร์ในวงกว้าง อย่างไรก็ตาม เราขอแนะนำให้ใช้ 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, พร็อพเพอร์ตี้ที่กำหนดเอง ฯลฯ)
  • ผสานรวมกับเครื่องมือสำหรับนักพัฒนาเว็บของเบราว์เซอร์อย่างแน่นหนา
  • ใช้ประโยชน์จากฟีเจอร์การช่วยเหลือพิเศษที่มีอยู่