องค์ประกอบที่กำหนดเอง v1 - คอมโพเนนต์เว็บที่นำมาใช้ใหม่ได้

องค์ประกอบที่กำหนดเองช่วยให้นักพัฒนาเว็บกำหนดแท็ก HTML ใหม่ ขยายแท็กที่มีอยู่ และสร้างคอมโพเนนต์เว็บที่นำมาใช้ใหม่ได้

ด้วยองค์ประกอบที่กำหนดเอง นักพัฒนาเว็บจะสร้างแท็ก HTML ใหม่ได้ เพิ่มแท็ก HTML ที่มีอยู่ หรือขยายคอมโพเนนต์ที่นักพัฒนาซอฟต์แวร์รายอื่นมี เป็นผู้เขียน API คือพื้นฐานของเว็บ คอมโพเนนต์ เครื่องมือนี้ช่วยนำ วิธีมาตรฐานในการสร้างคอมโพเนนต์ที่นำมาใช้ใหม่ได้ โดยใช้วิธีการไม่เกิน vanilla JS/HTML/CSS ผลที่ได้คือใช้โค้ดน้อยลง แยกโค้ดเป็นโมดูล และใช้งานซ้ำมากขึ้นใน แอปของเรา

บทนำ

เบราว์เซอร์นี้มีเครื่องมือที่ยอดเยี่ยมสำหรับการวางโครงสร้างเว็บแอปพลิเคชัน ตอนนี้ ที่เรียกว่า HTML คุณอาจเคยได้ยินเรื่องนี้มาบ้างแล้ว เป็นเทคโนโลยีที่ประกาศชื่อ พกพาสะดวก ได้รับการสนับสนุน และใช้งานง่าย ดีอย่าง HTML อาจจะเป็นคำศัพท์ และ สามารถขยายการใช้งานได้จำกัด HTML Lising มาโดยตลอด เชื่อมโยงลักษณะการทำงาน 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 สามารถแนบ Listener เหตุการณ์ได้ ฯลฯ อ่านเพื่อดูตัวอย่างเพิ่มเติม

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

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>

ข้อความที่กําหนดเองของผู้ใช้

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

การสร้างองค์ประกอบจาก <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>

โค้ด 2-3 บรรทัดนี้ตอบโจทย์ได้ทุกเรื่อง เรามาดูสิ่งสำคัญที่ควรทำกัน ใน:

  1. เรากำลังกำหนดองค์ประกอบใหม่ใน HTML: <x-foo-from-template>
  2. Shadow DOM ขององค์ประกอบสร้างจาก <template>
  3. DOM ขององค์ประกอบอยู่ภายในองค์ประกอบเนื่องจาก Shadow DOM
  4. CSS ภายในขององค์ประกอบมีขอบเขตอยู่ที่องค์ประกอบด้วย Shadow DOM

ฉันอยู่ใน Shadow DOM มาร์กอัปของฉันถูกประทับจาก <template>

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

การจัดรูปแบบองค์ประกอบที่กำหนดเอง

แม้ว่าองค์ประกอบจะกำหนดการจัดรูปแบบของตัวเองโดยใช้ 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 โดยใช้ Pseudo-Class :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 แทน เพื่อปรับปรุง 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 เดียวกัน ของ Google <section>, <address> และ <em> (และอีกมากมาย) แชร์ทั้งหมด HTMLElement; ทั้ง <q> และ <blockquote> ต่างใช้ HTMLQuoteElement ร่วมกัน ฯลฯ การระบุ {extends: 'blockquote'} ช่วยให้เบราว์เซอร์ทราบว่าคุณกำลังสร้าง เพิ่ม <blockquote> แทนที่จะเป็น <q> โปรดดูแท็ก HTML spec เพื่อดูรายการอินเทอร์เฟซ 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 มีความสะดวกสบายและยืดหยุ่นในการทำงาน เช่น ประกาศ <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 (สถานะ) Safari 10.1 (สถานะ) และ Firefox 63 (status) มี องค์ประกอบที่กำหนดเอง v1 Edge เริ่มต้นแล้ว การพัฒนา

ในการตรวจหาองค์ประกอบที่กำหนดเอง ให้ตรวจหาการมีอยู่ของ window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

ใยโพลีเอสเตอร์

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

ติดตั้ง ดังนี้

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> และเราเริ่มตระหนักถึง ภาพของคอมโพเนนต์เว็บ

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