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 các 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ử tùy 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 tại hoặc mở rộng các thành phần mà các nhà phát triển khác có tác giả. API là nền tảng của web thành phần. Ứng dụng này mang đến một môi trường web phương pháp dựa trên tiêu chuẩn để tạo các thành phần có thể tái sử dụng chỉ là vanilla JS/HTML/CSS. Kết quả là ít phải viết mã hơn, dùng nhiều mã mô-đun và có thể tái sử dụng nhiều hơn trong ứng dụng của chúng tôi.

Giới thiệu

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

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

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

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

Thành phần chung customElements dùng để xác định một phần tử tuỳ chỉnh và quy trình giảng dạy 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à JavaScript class mở rộng cơ sở HTMLElement.

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>

Điều quan trọng cần nhớ là việc sử dụng phần tử tuỳ chỉnh cũng giống như bằng cách 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 phiên bản trên trang, được tạo tự động trong JavaScript, trình nghe sự kiện có thể được đính kèm, v.v. Keep để đọc thêm ví dụ.

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

Chức năng của phần tử tuỳ chỉnh được xác định bằng ES2015 class 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ộ DOM API và có nghĩa là bất kỳ thuộc tính/phương thức nào mà bạn thêm vào trở thành một phần trong 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 opendisabled thuộc tính và phương thức toggleDrawer(). Thẻ này cũng phản ánh các thuộc tính dưới dạng HTML .

Một điểm thú vị của các phần tử tuỳ chỉnh là this bên trong một phần định nghĩa lớp đề cập đến chính phần tử DOM, tức là thực thể của lớp. Trong ví dụ: this tham chiếu đến <app-drawer>. Điều này (😉) là cách phần tử có thể đính kèm trình nghe click vào chính trình nghe đó! Bạn không bị giới hạn ở trình nghe sự kiện. Toàn bộ DOM API 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> còn <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 phần tử tuỳ chỉnh với phần tử thông thường. Mã này cũng đảm bảo chuyển tiếp khả năng tương thích khi 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 làm như vậy 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, nó. Không yêu cầu rút lại.
  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ử phần tử tự đóng. Luôn viết thẻ đóng (<app-drawer></app-drawer>).

Biểu tượng cảm xúc cho phần tử tuỳ chỉnh

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ị của sự tồn tại của nó. Chúng được gọi là phần tử tuỳ chỉnh số lượt thể hiện cảm xúc.

Tên Được gọi khi
constructor Một thực thể của phần tử này là tạo hoặc được nâng cấp. Hữu ích khi khởi chạy trạng thái, thiết lập trình nghe sự kiện hoặc tạo bóng đổ. Xem quy cách để 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 đượ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 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 gọi thêm, xoá, cập nhật hoặc thay thế. Còn được gọi cho các giá trị ban đầu khi trình phân tích cú pháp tạo một phần tử, hoặc đã nâng cấp. Lưu ý: chỉ các thuộc tính được liệt kê trong thuộc tính observedAttributes sẽ nhận cuộc gọi lại này.
adoptedCallback Chiến lược phát hành đĩa đơn phần tử tuỳ chỉnh đã được di chuyển sang document mới (ví dụ: một người có tên là document.adoptNode(el)).

Các lệnh gọi lại phản ứng có tính đồng bộ. Nếu có người gọi cho 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ị loại bỏ khỏi DOM (ví dụ: người dùng gọi el.remove()).

Ví dụ: thêm các biểu tượng cảm xúc 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 phản ứng nếu/khi đó hợp lý. 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 việc cần thiết dọn dẹp trong disconnectedCallback(). Nhưng hãy cẩn thận! Bạn không thể dựa vào trong mọi trường hợp đều bị xoá khỏi DOM. 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>

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 mọi 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 khi đị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ư API hỗ trợ tiếp cận và CSS dựa vào các thuộc tính để làm việc.

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

Hãy xem lại <app-drawer> của chúng tôi. Người dùng thành phần này có thể muốn làm mờ thành phần này và/hoặc ngăn người dùng tương tác khi chế độ này 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 đó thêm vào DOM để bộ chọn của người dùng khớp với nhau. Phần tử này có thể cung cấp điều đó bằng cách phản ánh giá trị vào 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 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 vào 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 đang thiết lập các thuộc tính bổ sung trên <app-drawer> khi một Thuộc tính disabled đã thay đổi. Mặc dù chúng tôi không thực hiện tại đây, nhưng bạn có thể Bạn cũng có thể sử dụng attributeChangedCallback để đồng bộ hoá thuộc tính JS với .

Nâng cấp phần tử

HTML được 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ý tất cả trong một phần tử tuỳ chỉnh.

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 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. Đ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 do không xác định các thẻ. Quá trình gọi define() và cấp quyền truy cập hiện có phần tử có đị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(). Hàm này trả về một Lời hứa sẽ phân giải khi đượ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 tập hợp 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 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 là lựa chọn hữu ích trong trường hợp 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>

// VIỆC CẦN LÀM: DevSite - Mã mẫu bị xoá khi sử dụng trình xử lý sự kiện cùng dòng

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

Shadow DOM cung cấp một cách thức để một phần tử sở hữu, kết xuất và tạo kiểu cho một phần DOM tách biệt với phần còn lại của trang. Hừm, bạn thậm chí 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 tối 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á khi sử dụng trình xử lý sự kiện cùng dòng

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

Đối với những người không quen thuộc, <template> phần tử 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, trơ khi tải trang và có thể được kích hoạt sau trong thời gian chạy. Đây là một API gốc khác trên web dòng thành phần. 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 tối được tạo từ mộ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>

vài dòng mã này rất thành công. Hãy cùng tìm hiểu những việc quan trọng vào:

  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 qua <template>
  3. DOM của phần tử nằm cục bộ với 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 bóng

Tôi đang ở trong Shadow DOM. Mục đánh dấu của tôi được đánh dấu từ <template>.

// VIỆC CẦN LÀM: DevSite - Mã mẫu bị xoá khi sử dụng trình xử lý sự kiện cùng dòng

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 DOM bóng, người dùng vẫn có thể tạo kiểu phần tử tùy chỉnh của bạn từ 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 cách tính đặc trưng của CSS hoạt động nếu phần tử có các kiểu được xác định trong DOM tối. Xét về tính cụ thể, kiểu người dùng chiếm ưu thế. Họ sẽ luôn ghi đè định kiểu do phần tử xác định. Xem phần Tạo phần tử sử dụng Shadow DOM.

Các thành phần chưa đăng ký trước khi tạo kiểu

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 để tạo kiểu trước cho một thành phần. Cho ví dụ: bạn có thể muốn ngăn bố cục hoặc FOUC trực quan khác bằng cách ẩn không xác định thành phần và làm mờ chúng 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.

Mở rộng phần tử

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

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

Hoàn tất việc mở rộng một phần tử tuỳ chỉnh khác bằng cách mở rộng phần đị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> thích thú hơn. Thay vì sao chép hành vi và chức năng của <button>, thì một lựa chọn tốt hơn là tăng dần cải thiện phần tử hiện có bằng phần tử tuỳ chỉnh.

Phần tử tích hợp sẵn được tuỳ chỉnh là phần tử tuỳ chỉnh mở rộng một trong thẻ HTML tích hợp sẵn của trình duyệt. Lợi ích chính của việc mở rộng phạm vi là có được tất cả các tính năng của nó (thuộc tính DOM, phương thức, khả năng tiếp cận). Không có cách nào tốt hơn để viết web tiến bộ ứng dụng thay vì nâng cao dần 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ụ: 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ầ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 đôi chút khi mở rộng một mã gốc . Thông số thứ ba bắt buộc cho trình duyệt biết bạn đang dùng thẻ nào mở rộng. Điều này là cần thiết vì nhiều thẻ HTML chia sẻ cùng một DOM . <section>, <address><em> (trong số những người khác) đều dùng chung HTMLElement; cả <q><blockquote> đều dùng chung HTMLQuoteElement; v.v. Việc chỉ định {extends: 'blockquote'} cho phép trình duyệt biết bạn đang tạo <blockquote> thay vì <q>. Hãy xem phần tử HTML quy cách để xem danh sách đầy đủ các giao diện DOM của HTML.

Người dùng phần tử tích hợp sẵn tuỳ chỉnh có thể sử dụng phần tử đó theo nhiều cách. Chúng có thể khai báo 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 phiên bản 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;

Dưới đâ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 phiên bản 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 hoàn toàn hài lòng chấp nhận nó. Tại sao thẻ không chuẩn hoạt động? Câu trả lời là tệp HTML quy cách cho phép truy cập. Các phần tử không được xác định trong thông số kỹ thuật đượ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 chúng đượ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 một trình duyệt hỗ trợ các phần tử tuỳ chỉnh. Kích hoạt Console: 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

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

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)

Cho trước một tên thẻ phần tử tuỳ chỉnh hợp lệ, hàm này 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ử.

Ví dụ:

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

whenDefined(tagName)

Trả về Lời hứa (Promise) sẽ phân giải khi phần tử tuỳ chỉnh được xác định. Nếu đã đượ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!');
});

Lịch sử 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, biết rằng Chrome 36 trở lên đã triển khai một phiên bản API Phần tử tuỳ chỉnh sử dụng document.registerElement() thay vì customElements.define(). Bây giờ được coi là phiên bản không được dùng nữa của chuẩn, được gọi là v0. customElements.define() là một xu hướng mới và các nhà cung cấp trình duyệt bắt đầu triển khai. Đây được gọi là Phần tử tuỳ chỉnh phiên bản 1.

Nếu bạn tình cờ quan tâm đến thông số v0 cũ, hãy tham khảo html5rock bài viết.

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) đã 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ử tùy 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 hỗ trợ trình duyệt được phổ biến rộng rãi, vẫn có polyfill độc lập có sẵn cho Phần tử tuỳ chỉnh phiên bản 1. Tuy nhiên, bạn nên sử dụng web nhóm.js trình tải để tải các polyfill 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 ô thăm dò 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 tôi 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 nền tảng mới khác như Shadow DOM và <template>, đồng thời chúng tôi bắt đầu nhận ra rằng ảnh về Thành phần web:

  • Sử dụng 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 thư viện hoặc 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 hiệu quả với các tính năng mới khác của nền tảng web (Shadow DOM, <template>, CSS thuộc tính tuỳ chỉnh, 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ó.