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

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

องค์ประกอบที่กำหนดเองช่วยให้นักพัฒนาเว็บสร้างแท็ก HTML ใหม่ เพิ่มประสิทธิภาพแท็ก HTML ที่มีอยู่ หรือขยายองค์ประกอบที่นักพัฒนาซอฟต์แวร์รายอื่นสร้างได้ API เป็นพื้นฐานของคอมโพเนนต์เว็บ ซึ่งจะนำวิธีการมาตรฐานเว็บ มาสร้างคอมโพเนนต์ที่นำมาใช้ซ้ำได้ โดยใช้เพียง Vanilla 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, แนบ 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>)

รีแอ็กชันขององค์ประกอบที่กำหนดเอง

องค์ประกอบที่กำหนดเองจะกำหนด hook วงจรพิเศษสำหรับการเรียกใช้โค้ดในช่วงเวลาที่น่าสนใจของการมีอยู่ได้ ซึ่งเรียกว่าการแสดงความรู้สึกขององค์ประกอบที่กำหนดเอง

ชื่อ โทรเมื่อ
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() โดยจะแสดงคำสัญญาที่จะได้รับการแก้ไขเมื่อมีการกำหนดองค์ประกอบ

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> จะช่วยให้คุณประกาศ Fragment ของ 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. 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)) จะไม่ตรงอีกต่อไป

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

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

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

const supportsCustomElementsV1 = 'customElements' in window;

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

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

ติดตั้ง:

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