Các phương pháp hay nhất về phần tử tuỳ chỉnh

Phần tử tuỳ chỉnh cho phép bạn tạo thẻ HTML của riêng mình. Danh sách kiểm tra này đề cập đến các phương pháp hay nhất để giúp bạn xây dựng thành phần có chất lượng cao.

Phần tử tuỳ chỉnh cho phép bạn mở rộng HTML và xác định thẻ của riêng mình. Chúng là một tính năng cực kỳ mạnh mẽ, nhưng cũng cấp thấp, nghĩa là không phải lúc nào bạn cũng không rõ cách tốt nhất để triển khai phần tử của mình.

Để giúp bạn tạo ra trải nghiệm tốt nhất có thể, chúng tôi đã tổng hợp danh sách kiểm tra này. Lớp này chia nhỏ mọi thứ chúng ta nghĩ cần có để trở thành một phần tử tuỳ chỉnh hoạt động tốt.

Danh sách kiểm tra

DOM bóng

Tạo một gốc bóng đổ để đóng gói các kiểu.

Tại sao? Việc đóng gói các kiểu trong gốc bóng đổ của phần tử đảm bảo rằng kiểu đóng gói này sẽ hoạt động bất kể vị trí được sử dụng. Điều này đặc biệt quan trọng nếu nhà phát triển muốn đặt phần tử của bạn vào bên trong gốc đổ bóng của một phần tử khác. Cách này áp dụng cho cả các phần tử đơn giản như hộp đánh dấu hoặc nút chọn. Trong trường hợp này, có thể nội dung duy nhất bên trong gốc bóng đổ là chính các kiểu đó.
Ví dụ Phần tử <howto-checkbox>.

Tạo gốc bóng đổ trong hàm khởi tạo.

Tại sao? Hàm khởi tạo là khi bạn có Kiến thức chuyên biệt về phần tử của mình. Đây là thời điểm tuyệt vời để thiết lập các chi tiết triển khai mà bạn không muốn các phần tử khác can thiệp. Khi thực hiện việc này trong một lệnh gọi lại sau này, chẳng hạn như connectedCallback, có nghĩa là bạn sẽ cần đề phòng các tình huống trong đó phần tử bị tách ra rồi được đính kèm vào tài liệu.
Ví dụ Phần tử <howto-checkbox>.

Đặt bất kỳ phần tử con nào mà thành phần tạo ra vào gốc bóng đổ.

Tại sao? Các thành phần con do phần tử của bạn tạo ra là một phần trong quá trình triển khai phần tử đó và nên được đặt ở chế độ riêng tư. Nếu không có sự bảo vệ của một gốc bóng đổ, JavaScript bên ngoài có thể vô tình can thiệp vào các phần tử con này.
Ví dụ Phần tử <howto-tabs>.

Sử dụng <slot> để chiếu thành phần con DOM sáng vào DOM bóng

Tại sao? Cho phép người dùng thành phần chỉ định nội dung trong thành phần vì thành phần con HTML sẽ làm cho thành phần của bạn dễ kết hợp hơn. Khi trình duyệt không hỗ trợ các phần tử tuỳ chỉnh, nội dung lồng nhau vẫn có sẵn, người dùng có thể nhìn thấy và truy cập được.
Ví dụ Phần tử <howto-tabs>.

Đặt kiểu hiển thị :host (ví dụ: block, inline-block, flex) trừ khi bạn ưu tiên kiểu mặc định là inline.

Tại sao? Các phần tử tuỳ chỉnh là display: inline theo mặc định, vì vậy, việc đặt width hoặc height của các phần tử đó sẽ không có hiệu lực. Điều này thường khiến các nhà phát triển cảm thấy bất ngờ và có thể gây ra các vấn đề liên quan đến việc bố trí trang. Nếu không muốn hiển thị inline, bạn nên luôn đặt giá trị display mặc định.
Ví dụ Phần tử <howto-checkbox>.

Thêm kiểu hiển thị :host tuân thủ thuộc tính ẩn.

Tại sao? Một phần tử tuỳ chỉnh có kiểu display mặc định (ví dụ: :host { display: block }) sẽ ghi đè thuộc tính hidden tích hợp sẵn có tính đặc hiệu thấp hơn. Điều này có thể khiến bạn ngạc nhiên nếu việc đặt thuộc tính hidden trên phần tử để kết xuất thuộc tính display: none. Ngoài kiểu display mặc định, hãy thêm tính năng hỗ trợ cho hidden bằng :host([hidden]) { display: none }.
Ví dụ Phần tử <howto-checkbox>.

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

Không ghi đè các thuộc tính chung của tác giả.

Tại sao? Thuộc tính chung là những thuộc tính có trong tất cả các phần tử HTML. Ví dụ: tabindexrole. Một phần tử tuỳ chỉnh có thể cần đặt tabindex ban đầu là 0 để phần tử này có thể lấy tiêu điểm bằng bàn phím. Tuy nhiên, bạn nên luôn kiểm tra trước để xem liệu nhà phát triển sử dụng phần tử của bạn đã đặt giá trị này thành một giá trị khác hay chưa. Chẳng hạn, nếu họ đặt tabindex thành -1, thì đó là tín hiệu cho thấy họ không muốn phần tử này có khả năng tương tác.
Ví dụ Phần tử <howto-checkbox>. Điều này được giải thích thêm trong phần Không ghi đè tác giả của trang.

Luôn chấp nhận dữ liệu gốc (chuỗi, số, boolean) dưới dạng thuộc tính hoặc thuộc tính.

Tại sao? Bạn phải định cấu hình được các phần tử tuỳ chỉnh, chẳng hạn như các phần tử tích hợp sẵn. Cấu hình có thể được chuyển theo cách khai báo, thông qua các thuộc tính hoặc bắt buộc thông qua các thuộc tính JavaScript. Tốt nhất là bạn cũng nên liên kết mỗi thuộc tính với một thuộc tính tương ứng.
Ví dụ Phần tử <howto-checkbox>.

Cố gắng đồng bộ hoá các thuộc tính và thuộc tính dữ liệu gốc, phản ánh giữa các thuộc tính và ngược lại.

Tại sao? Bạn không bao giờ biết cách người dùng sẽ tương tác với phần tử của mình. Họ có thể đặt một thuộc tính trong JavaScript và sau đó mong muốn đọc giá trị đó bằng một API như getAttribute(). Nếu mỗi thuộc tính có một thuộc tính tương ứng và cả hai thuộc tính này đều phản ánh, thì điều này sẽ giúp người dùng làm việc với phần tử của bạn dễ dàng hơn. Nói cách khác, việc gọi setAttribute('foo', value) cũng sẽ đặt thuộc tính foo tương ứng và ngược lại. Tất nhiên, có những trường hợp ngoại lệ đối với quy tắc này. Bạn không nên phản ánh các thuộc tính tần suất cao (ví dụ: currentTime) trong trình phát video. Hãy suy xét kỹ lưỡng. Nếu có vẻ người dùng sẽ tương tác với một thuộc tính hoặc thuộc tính và việc phản ánh thuộc tính hoặc thuộc tính đó không nặng nề, hãy thực hiện việc này.
Ví dụ Phần tử <howto-checkbox>. Điều này được giải thích thêm trong bài viết Tránh các vấn đề về khả năng đăng ký lại.

Cố gắng chỉ chấp nhận dữ liệu đa dạng thức (đối tượng, mảng) làm thuộc tính.

Tại sao? Nói chung, không có ví dụ nào về phần tử HTML tích hợp chấp nhận dữ liệu đa dạng thức (các mảng và đối tượng JavaScript thuần tuý) thông qua các thuộc tính của chúng. Thay vào đó, dữ liệu đa dạng thức được chấp nhận thông qua các lệnh gọi phương thức hoặc thuộc tính. Có một số nhược điểm rõ ràng của việc chấp nhận dữ liệu đa dạng thức làm thuộc tính: việc chuyển đổi tuần tự một đối tượng lớn thành một chuỗi có thể tốn kém và mọi tham chiếu đối tượng sẽ bị mất trong quá trình tạo chuỗi này. Ví dụ: nếu bạn tạo chuỗi cho một đối tượng có tham chiếu đến một đối tượng khác hoặc có thể là nút DOM, thì các thông tin tham chiếu đó sẽ bị mất.

Đừng phản ánh thuộc tính dữ liệu đa dạng thức cho các thuộc tính.

Tại sao? Việc phản ánh các thuộc tính dữ liệu đa dạng thức cho các thuộc tính là rất tốn kém, đòi hỏi phải chuyển đổi tuần tự và giải tuần tự các đối tượng JavaScript tương tự. Trừ phi có một trường hợp sử dụng chỉ có thể giải quyết bằng tính năng này, tốt nhất là bạn nên tránh dùng tính năng này.

Hãy cân nhắc kiểm tra những thuộc tính có thể đã được thiết lập trước khi phần tử này được nâng cấp.

Tại sao? Một nhà phát triển sử dụng phần tử của bạn có thể cố gắng đặt một thuộc tính trên phần tử đó trước khi định nghĩa về phần tử đó được tải. Điều này đặc biệt đúng nếu nhà phát triển đang sử dụng một khung xử lý các thành phần tải, đóng dấu các thành phần đó vào trang và liên kết các thuộc tính của chúng với một mô hình.
Ví dụ Phần tử <howto-checkbox>. Tìm hiểu thêm trong phần Tạo thuộc tính lazy.

Đừng tự áp dụng lớp học.

Tại sao? Các phần tử cần thể hiện trạng thái của chúng phải sử dụng các thuộc tính. Thuộc tính class thường được coi là thuộc sở hữu của nhà phát triển thông qua phần tử của bạn. Việc tự ghi thuộc tính đó có thể vô tình truy cập vào các lớp của nhà phát triển.

Sự kiện

Gửi các sự kiện để phản hồi hoạt động của thành phần nội bộ.

Tại sao? Thành phần của bạn có thể có các thuộc tính thay đổi theo hoạt động mà chỉ thành phần của bạn biết, chẳng hạn như khi bộ tính giờ hoặc ảnh động hoàn tất hoặc tài nguyên tải xong. Bạn nên gửi các sự kiện để phản hồi những thay đổi này để thông báo cho máy chủ lưu trữ rằng trạng thái của thành phần này là khác.

Không gửi các sự kiện để phản hồi khi máy chủ thiết lập một tài sản (luồng dữ liệu xuống dưới).

Tại sao? Việc gửi một sự kiện để phản hồi cho chế độ cài đặt thuộc tính của máy chủ là không cần thiết (máy chủ lưu trữ biết trạng thái hiện tại vì đã đặt trạng thái hiện tại). Việc gửi các sự kiện để phản hồi một chế độ cài đặt của máy chủ lưu trữ một thuộc tính có thể tạo ra vòng lặp vô hạn với các hệ thống liên kết dữ liệu.
Ví dụ Phần tử <howto-checkbox>.

Video giải thích

Không ghi đè tác giả của trang

Có thể nhà phát triển đang sử dụng phần tử của bạn nên ghi đè một số trạng thái ban đầu của phần tử. Ví dụ: thay đổi role của ARIA hoặc khả năng lấy tiêu điểm bằng tabindex. Hãy kiểm tra xem các thuộc tính này và bất kỳ thuộc tính chung nào khác đã được thiết lập hay chưa trước khi áp dụng các giá trị của riêng bạn.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Biến các thuộc tính thành tải từng phần

Nhà phát triển có thể cố gắng đặt một thuộc tính trên phần tử của bạn trước khi định nghĩa xong phần tử đó. Điều này đặc biệt đúng nếu nhà phát triển đang sử dụng một khung xử lý các thành phần tải, chèn các thành phần đó vào trang và liên kết các thuộc tính của chúng với một mô hình.

Trong ví dụ sau, Angular sẽ khai báo liên kết thuộc tính isChecked của mô hình với thuộc tính checked của hộp đánh dấu. Nếu định nghĩa cho hộp kiểm Search- Hoa Kỳ bị tải từng phần, có thể Angular sẽ cố gắng đặt thuộc tính đã đánh dấu trước khi phần tử này được nâng cấp.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

Phần tử tuỳ chỉnh sẽ xử lý trường hợp này bằng cách kiểm tra xem có thuộc tính nào đã được đặt trên thực thể của phần tử đó hay không. <howto-checkbox> minh hoạ mẫu này bằng phương thức có tên _upgradeProperty().

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty() ghi lại giá trị từ bản sao chưa nâng cấp và xoá thuộc tính để không làm ẩn phương thức setter thuộc tính riêng của phần tử tuỳ chỉnh. Bằng cách này, khi định nghĩa của phần tử cuối cùng cũng tải, thì phần tử đó có thể phản ánh đúng trạng thái.

Tránh các vấn đề về việc không truy cập lại

Bạn có thể sử dụng attributeChangedCallback() để phản ánh trạng thái cho một thuộc tính cơ bản, chẳng hạn như:

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

Tuy nhiên, điều này có thể tạo ra một vòng lặp vô hạn nếu phương thức setter thuộc tính cũng phản ánh đến thuộc tính.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

Một cách khác là cho phép phương thức setter thuộc tính phản ánh thuộc tính và để phương thức getter xác định giá trị của phương thức này dựa trên thuộc tính.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

get checked() {
  return this.hasAttribute('checked');
}

Trong ví dụ này, việc thêm hoặc xoá thuộc tính cũng sẽ thiết lập thuộc tính đó.

Cuối cùng, bạn có thể sử dụng attributeChangedCallback() để xử lý các hiệu ứng phụ như áp dụng trạng thái ARIA.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}