Phần tử tùy chỉnh phiên bản 1 – Thành phần web có thể tái sử dụng

Phần tử tuỳ chỉnh cho phép nhà phát triển web xác định thẻ HTML mới, mở rộng thẻ hiện có và tạo các thành phần web có thể tái sử dụng.

Với Phần tử tuỳ chỉnh, nhà phát triển web có thể tạo thẻ HTML mới, cải thiện các thẻ HTML hiện có hoặc mở rộng các thành phần mà nhà phát triển khác đã tạo. API là nền tảng của các thành phần web. Thư viện này mang đến một cách thức dựa trên tiêu chuẩn web để tạo các thành phần có thể sử dụng lại chỉ bằng JS/HTML/CSS. Nhờ đó, bạn sẽ giảm được mã, mã mô-đun và nhiều khả năng tái sử dụng trong ứng dụng.

Giới thiệu

Trình duyệt cung cấp cho chúng ta một công cụ tuyệt vời để xây dựng cấu trúc cho các ứng dụng web. Đó gọi là HTML. Có thể bạn đã biết đến! Nền tảng này mang tính khai báo, di động, được hỗ trợ tốt và dễ sử dụng. Dù HTML có thể tuyệt vời, vốn từ vựng và khả năng mở rộng của nó vẫn bị hạn chế. Tiêu chuẩn sống HTML luôn thiếu cách thức tự động liên kết hành vi JS với mã đánh dấu của bạn... cho đến nay.

Phần tử tuỳ chỉnh là giải pháp cho việc hiện đại hoá HTML, điền vào các phần bị thiếu và nhóm cấu trúc với hành vi. Nếu HTML không cung cấp giải pháp cho vấn đề, chúng ta có thể tạo một phần tử tuỳ chỉnh để giải quyết vấn đề. Các phần tử tuỳ chỉnh hướng dẫn trình duyệt các thủ thuật mới trong khi vẫn duy trì những lợi ích của HTML.

Xác định một phần tử mới

Để xác định một phần tử HTML mới, chúng ta cần sức mạnh của JavaScript!

Tập hợp customElements chung được dùng để xác định một phần tử tuỳ chỉnh và hướng dẫn trình duyệt về một thẻ mới. Gọi customElements.define() bằng tên thẻ bạn muốn tạo và JavaScript class mở rộng HTMLElement cơ sở.

Ví dụ – xác định bảng điều khiển ngăn thiết bị di động, <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 {...});

Ví dụ về cách sử dụng:

<app-drawer></app-drawer>

Điều quan trọng cần nhớ là việc sử dụng phần tử tuỳ chỉnh cũng như việc sử dụng <div> hoặc bất kỳ phần tử nào khác. Bạn có thể khai báo các thực thể trên trang, được tạo động trong JavaScript, trình nghe sự kiện có thể được đính kèm, v.v. Hãy đọc tiếp để biết thêm ví dụ.

Xác định API JavaScript của thành phần

Chức năng của một phần tử tuỳ chỉnh được xác định bằng cách sử dụng class ES2015, mở rộng HTMLElement. Việc mở rộng HTMLElement đảm bảo phần tử tuỳ chỉnh kế thừa toàn bộ API DOM và có nghĩa là mọi thuộc tính/phương thức mà bạn thêm vào lớp này sẽ trở thành một phần của giao diện DOM của phần tử. Về cơ bản, hãy sử dụng lớp này để tạo API JavaScript công khai cho thẻ của bạn.

Ví dụ – xác định giao diện DOM của <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);

Trong ví dụ này, chúng ta sẽ tạo một ngăn có thuộc tính open, thuộc tính disabled và phương thức toggleDrawer(). Lớp này cũng phản ánh các thuộc tính dưới dạng thuộc tính HTML.

Một đặc điểm thú vị của các phần tử tuỳ chỉnh là this bên trong một định nghĩa lớp tham chiếu đến chính phần tử DOM, tức là thực thể của lớp. Trong ví dụ của chúng tôi, this tham chiếu đến <app-drawer>. Đây (😉) là cách phần tử này có thể đính kèm trình nghe click vào chính nó! Bạn không bị giới hạn ở trình nghe sự kiện. Toàn bộ API DOM có sẵn bên trong mã phần tử. Sử dụng this để truy cập vào các thuộc tính của phần tử, kiểm tra phần tử con (this.children), nút truy vấn (this.querySelectorAll('.items')), v.v.

Quy tắc tạo phần tử tuỳ chỉnh

  1. Tên của một phần tử tuỳ chỉnh phải chứa dấu gạch ngang (-). Vì vậy, <x-tags>, <my-element><my-awesome-app> đều là tên hợp lệ, trong khi <tabs><foo_bar> thì không. Yêu cầu này là để trình phân tích cú pháp HTML có thể phân biệt các phần tử tuỳ chỉnh với các phần tử thông thường. Tính năng này cũng đảm bảo khả năng tương thích chuyển tiếp khi các thẻ mới được thêm vào HTML.
  2. Bạn không thể đăng ký cùng một thẻ nhiều lần. Việc cố gắng thực hiện việc này sẽ gửi một DOMException. Sau khi bạn thông báo cho trình duyệt về thẻ mới, đó là xong. Không được phép nhận lại.
  3. Phần tử tuỳ chỉnh không thể tự đóng vì HTML chỉ cho phép một vài phần tử tự đóng. Luôn viết một thẻ đóng (<app-drawer></app-drawer>).

Thể hiện cảm xúc trong phần tử tuỳ chỉnh

Một phần tử tuỳ chỉnh có thể xác định các hook đặc biệt trong vòng đời để chạy mã trong những thời điểm thú vị khi phần tử đó tồn tại. Những hành động này được gọi là phản ứng của phần tử tuỳ chỉnh.

Tên Được gọi khi
constructor Một bản sao của phần tử đã được tạo hoặc nâng cấp. Hữu ích để khởi tạo trạng thái, thiết lập trình nghe sự kiện hoặc tạo phạm vi bóng đổ. Hãy xem thông số kỹ thuật để biết các quy định hạn chế về những việc bạn có thể làm trong constructor.
connectedCallback Được gọi mỗi khi phần tử được chèn vào DOM. Hữu ích để chạy mã thiết lập, chẳng hạn như tìm nạp tài nguyên hoặc kết xuất. Nhìn chung, bạn nên cố gắng trì hoãn công việc cho đến thời điểm này.
disconnectedCallback Được gọi mỗi khi phần tử bị xoá khỏi DOM. Hữu ích khi chạy mã dọn dẹp.
attributeChangedCallback(attrName, oldVal, newVal) Được gọi khi một thuộc tính đã quan sát được thêm, xoá, cập nhật hoặc thay thế. Thuộc tính này cũng được gọi cho các giá trị ban đầu khi một phần tử do trình phân tích cú pháp tạo ra hoặc đã nâng cấp. Lưu ý: chỉ các thuộc tính liệt kê trong thuộc tính observedAttributes mới nhận được lệnh gọi lại này.
adoptedCallback Phần tử tuỳ chỉnh đã được chuyển sang document mới (ví dụ: ai đó có tên là document.adoptNode(el)).

Lệnh gọi lại phản ứng có tính đồng bộ. Nếu ai đó gọi el.setAttribute() trên phần tử của bạn, trình duyệt sẽ gọi attributeChangedCallback() ngay lập tức. Tương tự, bạn sẽ nhận được disconnectedCallback() ngay sau khi phần tử của bạn bị xoá khỏi DOM (ví dụ: người dùng gọi el.remove()).

Ví dụ: thêm lượt thể hiện cảm xúc của phần tử tuỳ chỉnh vào <app-drawer>:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

Xác định biểu tượng cảm xúc nếu/khi thích hợp. Nếu phần tử của bạn đủ phức tạp và mở kết nối đến IndexedDB trong connectedCallback(), hãy thực hiện công việc dọn dẹp cần thiết trong disconnectedCallback(). Nhưng hãy cẩn thận! Bạn không thể dựa vào việc phần tử của mình sẽ bị xoá khỏi DOM trong mọi trường hợp. Ví dụ: disconnectedCallback() sẽ không bao giờ được gọi nếu người dùng đóng thẻ.

Thuộc tính và thuộc tính

Phản ánh thuộc tính cho các thuộc tính

Thông thường, các thuộc tính HTML sẽ phản ánh giá trị của chúng trở lại DOM dưới dạng một thuộc tính HTML. Ví dụ: khi giá trị của hidden hoặc id được thay đổi trong JS:

div.id = 'my-id';
div.hidden = true;

các giá trị được áp dụng cho DOM trực tiếp dưới dạng thuộc tính:

<div id="my-id" hidden>

Đây được gọi là "phản ánh các thuộc tính tới các thuộc tính". Hầu hết mọi tài sản trong HTML đều thực hiện việc này. Tại sao? Các thuộc tính cũng hữu ích khi định cấu hình phần tử theo cách khai báo và một số API như bộ chọn hỗ trợ tiếp cận và bộ chọn CSS dựa vào các thuộc tính để hoạt động.

Việc phản ánh một thuộc tính rất hữu ích ở bất kỳ nơi nào bạn muốn đồng bộ hoá nội dung biểu thị DOM của phần tử với trạng thái JavaScript của phần tử đó. Một lý do bạn có thể muốn phản ánh một thuộc tính là để áp dụng kiểu do người dùng xác định khi trạng thái JS thay đổi.

Hãy nhớ lại <app-drawer>. Người dùng thành phần này có thể muốn làm mờ và/hoặc ngăn tương tác của người dùng khi thành phần này bị tắt:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

Khi thay đổi thuộc tính disabled trong JS, chúng ta muốn thêm thuộc tính đó vào DOM để bộ chọn của người dùng khớp với nhau. Phần tử có thể cung cấp hành vi đó bằng cách phản ánh giá trị cho một thuộc tính cùng tên:

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();
}

Quan sát các thay đổi đối với thuộc tính

Thuộc tính HTML là một cách thuận tiện để người dùng khai báo trạng thái ban đầu:

<app-drawer open disabled></app-drawer>

Các phần tử có thể phản ứng với các thay đổi về thuộc tính bằng cách xác định attributeChangedCallback. Trình duyệt sẽ gọi phương thức này cho mọi thay đổi đối với các thuộc tính được liệt kê trong mảng 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.
  }
}

Ở ví dụ này, chúng ta sẽ đặt các thuộc tính bổ sung trên <app-drawer> khi thuộc tính disabled thay đổi. Mặc dù chúng tôi không thực hiện việc này ở đây, nhưng bạn cũng có thể sử dụng attributeChangedCallback để đồng bộ hoá thuộc tính JS với thuộc tính tương ứng.

Nâng cấp phần tử

HTML nâng cao dần dần

Chúng ta đã biết rằng các phần tử tuỳ chỉnh được xác định bằng cách gọi customElements.define(). Nhưng điều này không có nghĩa là bạn phải xác định + đăng ký một phần tử tuỳ chỉnh trong một lần.

Bạn có thể sử dụng phần tử tuỳ chỉnh trước khi đăng ký định nghĩa của phần tử đó.

Tính năng nâng cao tăng dần là một tính năng của các phần tử tuỳ chỉnh. Nói cách khác, bạn có thể khai báo nhiều phần tử <app-drawer> trên trang và không bao giờ gọi customElements.define('app-drawer', ...) cho đến sau này. Nguyên nhân là do trình duyệt xử lý các phần tử tuỳ chỉnh tiềm năng theo cách khác nhau nhờ vào thẻ không xác định. Quá trình gọi define() và cung cấp định nghĩa về lớp cho một phần tử hiện có được gọi là "nâng cấp phần tử".

Để biết thời điểm tên thẻ được xác định, bạn có thể sử dụng window.customElements.whenDefined(). Phương thức này trả về một Promise sẽ phân giải khi phần tử được xác định.

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

Ví dụ – trì hoãn công việc cho đến khi một nhóm các phần tử con được nâng cấp

<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.
});

Nội dung do phần tử xác định

Các thành phần tuỳ chỉnh có thể quản lý nội dung của riêng chúng bằng cách sử dụng API DOM bên trong mã phần tử. Tính năng Phản ứng sẽ giúp ích cho bạn trong trường hợp này.

Ví dụ – tạo một phần tử có HTML mặc định:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

Việc khai báo thẻ này sẽ tạo ra:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// VIỆC CẦN LÀM: DevSite - Mã mẫu bị xoá vì đã sử dụng trình xử lý sự kiện nội tuyến

Tạo một phần tử sử dụng DOM bóng

DOM tối cung cấp cách thức để một phần tử sở hữu, hiển thị và tạo kiểu cho một phần của DOM riêng biệt với phần còn lại của trang. Bạn thậm chí có thể ẩn toàn bộ ứng dụng chỉ trong một thẻ:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

Để sử dụng DOM bóng trong một phần tử tuỳ chỉnh, hãy gọi this.attachShadow bên trong 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));
  }
  // ...
});

Ví dụ về cách sử dụng:

<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>

Văn bản tuỳ chỉnh của người dùng

// VIỆC CẦN LÀM: DevSite - Mã mẫu bị xoá vì đã sử dụng trình xử lý sự kiện nội tuyến

Tạo phần tử qua <template>

Đối với những người chưa quen thuộc, phần tử <template> cho phép bạn khai báo các phân đoạn của DOM được phân tích cú pháp, không thay đổi khi tải trang và có thể được kích hoạt sau trong thời gian chạy. Đó là một API nguyên gốc khác trong nhóm thành phần web. Mẫu là phần giữ chỗ lý tưởng để khai báo cấu trúc của một phần tử tuỳ chỉnh.

Ví dụ: đăng ký một phần tử có nội dung DOM bóng được tạo từ <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>

Một vài dòng mã này mang đến một điểm nhấn rất quan trọng. Hãy hiểu những bước quan trọng sau đây:

  1. Chúng ta đang xác định một phần tử mới trong HTML: <x-foo-from-template>
  2. DOM bóng của phần tử được tạo từ <template>
  3. DOM của phần tử cục bộ so với phần tử nhờ DOM bóng
  4. CSS nội bộ của phần tử nằm trong phạm vi của phần tử đó nhờ vào DOM bóng

Tôi đang ở trong DOM bóng. Thẻ đánh dấu của tôi được đóng dấu từ một <template>.

// VIỆC CẦN LÀM: DevSite - Mã mẫu bị xoá vì đã sử dụng trình xử lý sự kiện nội tuyến

Tạo kiểu cho một phần tử tuỳ chỉnh

Ngay cả khi phần tử của bạn xác định kiểu riêng bằng Shadow DOM, người dùng vẫn có thể tạo kiểu cho phần tử tuỳ chỉnh của bạn từ trang của họ. Những kiểu này được gọi là "kiểu do người dùng xác định".

<!-- 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>

Có thể bạn sẽ tự hỏi cách hoạt động của tính đặc trưng của CSS nếu phần tử có các kiểu được xác định trong DOM bóng. Xét về tính cụ thể, kiểu người dùng sẽ chiến thắng. Các kiểu này sẽ luôn ghi đè kiểu do phần tử xác định. Xem phần về Tạo phần tử sử dụng DOM bóng.

Phần tử chưa đăng ký tạo kiểu trước

Trước khi một phần tử được nâng cấp, bạn có thể nhắm mục tiêu phần tử đó trong CSS bằng cách sử dụng lớp giả :defined. Điều này rất hữu ích khi tạo kiểu trước cho một thành phần. Ví dụ: có thể bạn muốn ngăn bố cục hoặc FOUC trực quan khác bằng cách ẩn các thành phần không xác định và làm mờ các thành phần đó khi chúng được xác định.

Ví dụ – ẩn <app-drawer> trước khi được xác định:

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;
}

Sau khi <app-drawer> được xác định, bộ chọn (app-drawer:not(:defined)) không còn khớp nữa.

Phần tử mở rộng

Custom Elements API (API Phần tử tuỳ chỉnh) rất hữu ích cho việc tạo các phần tử HTML mới, nhưng cũng hữu ích khi mở rộng các phần tử tuỳ chỉnh khác hoặc thậm chí là HTML tích hợp của trình duyệt.

Mở rộng phần tử tuỳ chỉnh

Việc mở rộng một phần tử tuỳ chỉnh khác được thực hiện bằng cách mở rộng định nghĩa lớp của phần tử đó.

Ví dụ: tạo <fancy-app-drawer> mở rộng <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);

Mở rộng phần tử HTML gốc

Giả sử bạn muốn tạo một <button> yêu thích hơn. Thay vì sao chép hành vi và chức năng của <button>, bạn nên tăng dần phần tử hiện có bằng cách sử dụng các phần tử tuỳ chỉnh.

Phần tử tích hợp tuỳ chỉnh là phần tử tuỳ chỉnh giúp mở rộng một trong các thẻ HTML tích hợp của trình duyệt. Lợi ích chính của việc mở rộng một phần tử hiện có là có được tất cả các tính năng của phần tử đó (các thuộc tính, phương thức, khả năng hỗ trợ tiếp cận của DOM). Không có cách nào để viết ứng dụng web tiến bộ tốt hơn là cải thiện dần các thành phần HTML hiện có.

Để mở rộng một phần tử, bạn cần tạo định nghĩa về lớp kế thừa từ giao diện DOM chính xác. Ví dụ: một phần tử tuỳ chỉnh mở rộng <button> cần kế thừa từ HTMLButtonElement thay vì HTMLElement. Tương tự, một phần tử mở rộng <img> cũng cần mở rộng HTMLImageElement.

Ví dụ – mở rộng <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'});

Lưu ý lệnh gọi đến define() thay đổi đôi chút khi mở rộng một phần tử gốc. Tham số thứ ba bắt buộc cho trình duyệt biết thẻ bạn đang mở rộng. Điều này là cần thiết vì nhiều thẻ HTML có cùng một giao diện DOM. <section>, <address><em> (cùng với các tập dữ liệu khác) đều chia sẻ HTMLElement; cả <q><blockquote> đều chia sẻ HTMLQuoteElement; v.v. Việc chỉ định {extends: 'blockquote'} sẽ cho trình duyệt biết rằng bạn đang tạo một <blockquote> cải tiến thay vì một <q>. Hãy xem thông số kỹ thuật HTML để biết danh sách đầy đủ các giao diện DOM của HTML.

Người dùng phần tử tích hợp tuỳ chỉnh có thể sử dụng phần tử đó theo nhiều cách. Họ có thể khai báo thông tin này bằng cách thêm thuộc tính is="" vào thẻ gốc:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

tạo một thực thể trong 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);

hoặc dùng toán tử new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

Sau đây là một ví dụ khác mở rộng <img>.

Ví dụ – mở rộng <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'});

Người dùng khai báo thành phần này là:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

hoặc tạo một thực thể trong 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);

Thông tin chi tiết khác

Phần tử không xác định và phần tử tùy chỉnh không xác định

HTML nhẹ và linh hoạt để làm việc. Ví dụ: khai báo <randomtagthatdoesntexist> trên một trang và trình duyệt hoàn toàn sẵn lòng chấp nhận điều đó. Tại sao thẻ không theo chuẩn lại hoạt động? Câu trả lời là thông số kỹ thuật HTML cho phép công cụ này. Các phần tử không được quy cách xác định sẽ được phân tích cú pháp là HTMLUnknownElement.

Điều này không đúng đối với các phần tử tuỳ chỉnh. Các phần tử tuỳ chỉnh tiềm năng được phân tích cú pháp dưới dạng HTMLElement nếu được tạo bằng tên hợp lệ (bao gồm dấu "-"). Bạn có thể kiểm tra điều này trong trình duyệt có hỗ trợ các phần tử tuỳ chỉnh. Kích hoạt Bảng điều khiển: Ctrl+Shift+J (hoặc Cmd+Opt+J trên máy Mac) rồi dán vào các dòng mã sau:

// "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

Tài liệu tham khảo API

Tập hợp customElements toàn cục xác định các phương thức hữu ích để làm việc với các phần tử tuỳ chỉnh.

define(tagName, constructor, options)

Xác định một phần tử tuỳ chỉnh mới trong trình duyệt.

Ví dụ:

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

Nếu có tên thẻ phần tử tuỳ chỉnh hợp lệ, sẽ trả về hàm khởi tạo của phần tử. Trả về undefined nếu chưa đăng ký định nghĩa phần tử nào.

Ví dụ:

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

Trả về một Promise sẽ giải quyết khi phần tử tuỳ chỉnh được xác định. Nếu phần tử này đã được xác định, hãy phân giải ngay lập tức. Từ chối nếu tên thẻ không phải là tên phần tử tuỳ chỉnh hợp lệ.

Ví dụ:

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

Hỗ trợ trình duyệt và nhật ký

Nếu đã theo dõi các thành phần web trong vài năm qua, thì bạn sẽ biết rằng Chrome 36+ đã triển khai một phiên bản của Custom Elements API (API Phần tử tuỳ chỉnh) sử dụng document.registerElement() thay vì customElements.define(). Phiên bản này hiện được coi là một phiên bản tiêu chuẩn không dùng nữa, được gọi là v0. customElements.define() là nội dung mới và là những gì các nhà cung cấp trình duyệt đang bắt đầu triển khai. Đó là Phần tử tuỳ chỉnh phiên bản 1.

Nếu bạn quan tâm đến thông số kỹ thuật cũ của phiên bản 0, hãy xem bài viết về html5rock.

Hỗ trợ trình duyệt

Chrome 54 (trạng thái), Safari 10.1 (trạng thái) và Firefox 63 (trạng thái) có Custom Elements v1. Edge đang bắt đầu phát triển.

Để tính năng phát hiện các phần tử tuỳ chỉnh, hãy kiểm tra sự tồn tại của window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Vải polyfill

Cho đến khi trình duyệt được hỗ trợ rộng rãi, chúng tôi sẽ cung cấp polyfill độc lập cho Phần tử tuỳ chỉnh phiên bản 1. Tuy nhiên, bạn nên sử dụng trình tải web tools.js để tải các polyfill cho thành phần web một cách tối ưu. Trình tải sử dụng tính năng phát hiện tính năng để chỉ tải không đồng bộ các lớp lọc lỗi cần thiết mà trình duyệt yêu cầu.

Cài đặt:

npm install --save @webcomponents/webcomponentsjs

Cách sử dụng:

<!-- 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>

Kết luận

Phần tử tuỳ chỉnh mang lại cho chúng ta một công cụ mới để xác định các thẻ HTML mới trong trình duyệt và tạo các thành phần có thể sử dụng lại. Kết hợp chúng với các nền tảng nguyên gốc mới khác như Shadow DOM và <template>, chúng ta bắt đầu nhận thấy tổng quan về các Thành phần web:

  • Trên nhiều trình duyệt (tiêu chuẩn web) để tạo và mở rộng các thành phần có thể sử dụng lại.
  • Không cần có thư viện hay khung để bắt đầu. Vanilla JS/HTML FTW!
  • Cung cấp một mô hình lập trình quen thuộc. Chỉ là DOM/CSS/HTML.
  • Hoạt động tốt với các tính năng mới khác của nền tảng web (Bóng DOM, <template>, thuộc tính tuỳ chỉnh CSS, v.v.)
  • Được tích hợp chặt chẽ với Công cụ cho nhà phát triển của trình duyệt.
  • Tận dụng bộ tính năng hỗ trợ tiếp cận hiện có.