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

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

Với Phần tử tuỳ chỉnh, nhà phát triển web có thể tạo thẻ HTML mới, tăng cường các thẻ HTML hiện có hoặc mở rộng các thành phần mà các nhà phát triển khác đã tạo. API là nền tảng của thành phần web. Khung này cung cấp một cách dựa trên tiêu chuẩn web để tạo các thành phần có thể sử dụng lại mà không cần gì ngoài JS/HTML/CSS gốc. Kết quả là ít mã hơn, mã mô-đun hơn và tái sử dụng nhiều hơn 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 để tạo cấu trúc cho các ứng dụng web. Ngôn ngữ này được gọi là HTML. Có thể bạn đã biết đến địa điểm này! Ngôn ngữ này mang tính khai báo, có thể di chuyển, được hỗ trợ tốt và dễ sử dụng. Mặc dù HTML rất tuyệt vời, nhưng từ vựng và khả năng mở rộng của HTML lại bị hạn chế. Tiêu chuẩn HTML sống luôn thiếu một cách để tự động liên kết hành vi JS với mã đánh dấu của bạn… cho đến thời điểm này.

Phần tử tuỳ chỉnh là giải pháp để hiện đại hoá HTML, điền vào các phần còn thiếu và gói cấu trúc với hành vi. Nếu HTML không cung cấp giải pháp cho một vấn đề, chúng ta có thể tạo một phần tử tuỳ chỉnh có thể giải quyết vấn đề đó. Các phần tử tùy chỉnh sẽ dạy trình duyệt các thủ thuật mới trong khi vẫn giữ lại các 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!

customElements toàn cục đượ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ẻ mà bạn muốn tạo và class JavaScript mở rộng HTMLElement cơ sở.

Ví dụ – xác định một bảng điều khiển ngăn trê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>

Xin lưu ý rằng việc sử dụng phần tử tuỳ chỉnh cũng không khác với 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, tạo linh động trong JavaScript, đính kèm trình nghe sự kiện, v.v. Hãy tiếp tục đọc để xem thêm ví dụ.

Xác định API JavaScript của một phần tử

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 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à một phương thức toggleDrawer(). Tệ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 tính năng gọn gàng của các phần tử tuỳ chỉnh là this bên trong đị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ử có thể đính kèm trình nghe click vào chính nó! Bạn không chỉ 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 các phần tử con (this.children), nút truy vấn (this.querySelectorAll('.items')), v.v.

Quy tắc về việc tạo phần tử tuỳ chỉnh

  1. Tên của 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ệ, còn <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. Điều này cũng đảm bảo khả năng tương thích chuyển tiếp khi thêm thẻ mới vào HTML.
  2. Bạn không thể đăng ký cùng một thẻ nhiều lần. Nếu bạn cố gắng làm như vậy, hệ thống sẽ gửi một DOMException. Sau khi bạn thông báo cho trình duyệt về một thẻ mới, đó là tất cả. Không nhận lại hàng.
  3. Các 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>).

Phản ứng phần tử tuỳ chỉnh

Một phần tử tuỳ chỉnh có thể xác định các trình kích hoạt vòng đời đặc biệt để chạy mã trong thời điểm thú vị của sự tồn tại. Đây được gọi là phản ứng 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 được nâng cấp. Hữu ích khi khởi tạo trạng thái, thiết lập trình nghe sự kiện hoặc tạo một shadow dom. 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 khi 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. Thông thường, 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 cho việc 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ế. Còn đượ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 được nâng cấp. Lưu ý: chỉ những thuộc tính được 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 vào một document mới (ví dụ: một người nào đó 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 mình bị xoá khỏi DOM (ví dụ: người dùng gọi el.remove()).

Ví dụ: thêm các phản ứng thành phần 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 các phản ứng nếu/khi có thể. 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 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 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 thuộc tính HTML. Ví dụ: khi giá trị của hidden hoặc id 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>

Quá trình này được gọi là "phản ánh thuộc tính đến thuộc tính". Hầu hết các thuộc tính 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 cho việc định cấu hình một phần tử theo cách khai báo và một số API nhất định như bộ chọn CSS và hỗ trợ tiếp cận sẽ dựa vào các thuộc tính để hoạt động.

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

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

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

Khi thuộc tính disabled thay đổi trong JS, chúng ta muốn thuộc tính đó được thêm vào DOM để bộ chọn của người dùng khớp. 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ó 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 những 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.
  }
}

Trong 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 ta 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 của thuộc tính đó.

Nâng cấp phần tử

HTML được cải tiến tăng 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 chỉ trong một lần.

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

Cải tiến 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 một loạt phần tử <app-drawer> trên trang và không bao giờ gọi customElements.define('app-drawer', ...) cho đến sau này. Điều này 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ờ các thẻ không xác định. Quá trình gọi define() và cấp cho một phần tử hiện có một định nghĩa lớp đượ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 Lời hứa (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 nâng cấp một nhóm phần tử con

<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 phần tử tuỳ chỉnh có thể quản lý nội dung của riêng mình bằng cách sử dụng các API DOM bên trong mã phần tử. Phản ứng sẽ rất hữu ích cho việc này.

Ví dụ – tạo một phần tử có một số 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>

// TODO: DevSite – Xoá mã mẫu vì mã này sử dụng trình xử lý sự kiện nội tuyến

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

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

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

// TODO: DevSite – Xoá mã mẫu vì mã này sử dụng trình xử lý sự kiện nội tuyến

Tạo các phần tử từ <template>

Đối với những người không quen thuộc, phần tử <template> cho phép bạn khai báo các mảnh của DOM được phân tích cú pháp, trơ khi tải trang và có thể được kích hoạt sau này trong thời gian chạy. Đây là một API gốc khác trong gia đình 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 Shadow DOM đượ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 có tác dụng rất lớn. Hãy cùng tìm hiểu những điều chính đang diễn ra:

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

Tôi đang ở trong Shadow DOM. Mã đánh dấu của tôi được đóng dấu từ một <mẫu>.

// TODO: DevSite – Xoá mã mẫu vì mã này sử dụng trình xử lý sự kiện nội tuyến

Định kiểu cho 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, thì người dùng vẫn có thể tạo kiểu cho phần tử tuỳ chỉnh của bạn trên trang của họ. Đâ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>

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

Định kiểu trước các phần tử chưa đăng ký

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ụ: bạn có thể muốn ngăn bố cục hoặc các FOUC hình ảnh khác bằng cách ẩn các thành phần chưa 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 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)) sẽ không còn khớp nữa.

Mở rộng phần tử

API Phần tử tuỳ chỉnh rất hữu ích để tạo các phần tử HTML mới, nhưng cũng hữu ích để 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

Bạn có thể mở rộng một phần tử tuỳ chỉnh khác 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> bắt mắt hơn. Thay vì sao chép hành vi và chức năng của <button>, bạn nên cải thiện dần phần tử hiện có bằng các phần tử tuỳ chỉnh.

Phần tử tích hợp tuỳ chỉnh là một phần tử tuỳ chỉnh 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ử đó (thuộc tính DOM, phương thức, khả năng hỗ trợ tiếp cận). Không có cách nào tốt hơn để viết ứng dụng web tiến bộ ngoài việc nâng cao dần các phần tử HTML hiện có.

Để mở rộng một phần tử, bạn cần tạo một định nghĩa 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ự, phần tử mở rộng <img> 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 ý rằng lệnh gọi đến define() thay đổi một chút khi mở rộng một phần tử gốc. Thông số thứ ba bắt buộc sẽ cho trình duyệt biết bạn đang mở rộng thẻ nào. Điều này là cần thiết vì nhiều thẻ HTML có chung một giao diện DOM. <section>, <address><em> (cùng với các loại khác) đều chia sẻ HTMLElement; cả <q><blockquote> đều chia sẻ HTMLQuoteElement; v.v. Việc chỉ định {extends: 'blockquote'} cho trình duyệt biết rằng bạn đang tạo một <blockquote> nâng cao thay vì <q>. 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 của một phần tử tích hợp tuỳ chỉnh có thể sử dụng phần tử đó theo một số cách. Họ có thể khai báo thuộc tính 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 sử 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 so với phần tử tùy chỉnh không xác định

HTML thoải mái và linh hoạt khi làm việc. Ví dụ: khai báo <randomtagthatdoesntexist> trên một trang và trình duyệt sẽ chấp nhận hoàn toàn. Tại sao thẻ không chuẩn hoạt động? Câu trả lời là quy cách HTML cho phép định dạng 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 dưới dạng 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 một tên hợp lệ (bao gồm dấu "-"). Bạn có thể kiểm tra thông tin này trong một trình duyệt có hỗ trợ các phần tử tuỳ chỉnh. Khởi động 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

customElements chung xác định các phương thức hữu ích để xử lý 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)

Với tên thẻ phần tử tuỳ chỉnh hợp lệ, 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 Lời hứa sẽ phân giải khi phần tử tuỳ chỉnh được xác định. Nếu phần tử đã đượ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!');
});

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

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

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ó Các phần tử tuỳ chỉnh phiên bản 1. Edge đã bắt đầu phát triển.

Để phát hiện tính năng 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;

Ống polyfill

Cho đến khi trình duyệt hỗ trợ rộng rãi, bạn có thể sử dụng một 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 webcomponents.js để tải các thành phần web polyfill 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 ô khám phá 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 cung cấp 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 các thành phần này với các thành phần gốc mới khác của nền tảng như Shadow DOM và <template>, chúng ta bắt đầu nhận ra bức tranh lớn về Thành phần web:

  • Sử dụng 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 thư viện hoặc khung để bắt đầu. JS/HTML thuần tuý 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 (Shadow DOM, <template>, thuộc tính tuỳ chỉnh CSS, v.v.)
  • Được tích hợp chặt chẽ với DevTools của trình duyệt.
  • Tận dụng bộ tính năng hỗ trợ tiếp cận hiện có.