自訂元素最佳做法

自訂元素可讓您建構自己的 HTML 標記。這份檢查清單涵蓋最佳做法,可協助您建立優質元素。

自訂元素可讓您擴充 HTML 並定義自己的標記。這是非常強大的功能,但也屬於低階,因此不一定能清楚實作自有元素的最佳方式。

為協助您打造最佳體驗,我們整理了這份檢查清單。可細分所有我們認為達到自訂元素所需的一切元素。

檢查清單

陰影 DOM

建立用來封裝樣式的陰影根。

為什麼? 將元素陰影根層級的樣式封裝可確保無論在何處都能運作。如果開發人員想將元素置於其他元素的陰影根內部,這點就特別重要。這適用於核取方塊或圓形按鈕等簡單的元素。陰影根層級的唯一內容會是樣式本身。
範例 <howto-checkbox> 元素。

在建構函式中建立陰影根。

為什麼? 建構函式指具備元素的專屬知識。因此,現在是設定實作詳細資料的好時機,也就是您不希望其他元素幹擾到其他元素。在 connectedCallback 等之後的回呼中執行這項工作,意味著您需要防止元素卸離,再重新附加至文件時。
範例 <howto-checkbox> 元素。

將元素建立的所有子項放入陰影根層級。

為什麼? 元素建立的子項屬於實作項目,因此必須設為不公開。在沒有陰影根層級保護的情況下,JavaScript 外部可能會無意間幹擾這些子項。
範例 <howto-tabs> 元素。

使用 <slot> 將 light DOM 子項投影到 shadow DOM

為什麼? 允許元件使用者以 HTML 子項的形式指定元件中的內容,讓元件更具可組合項。如果瀏覽器不支援自訂元素,則仍可使用及存取巢狀結構內容。
範例 <howto-tabs> 元素。

設定 :host 顯示樣式 (例如 blockinline-blockflex),除非您偏好使用預設的 inline

為什麼? 自訂元素預設為 display: inline,因此設定 widthheight 不會產生任何作用。這通常是對開發人員造成驚訝的現象,而且可能會造成網頁版面配置相關問題。除非您偏好使用 inline 螢幕,否則應一律設定預設的 display 值。
範例 <howto-checkbox> 元素。

新增遵循隱藏屬性的 :host 顯示樣式。

為什麼? 使用預設 display 樣式的自訂元素 (例如 :host { display: block }) 會覆寫內建明確度的內建 hidden 屬性。如果您預期在元素上設定 hidden 屬性來顯示 display: none,可能會感到意外。除了預設的 display 樣式之外,您還可以使用 :host([hidden]) { display: none } 新增對 hidden 的支援。
範例 <howto-checkbox> 元素。

屬性和屬性

不要覆寫作者設定和全域屬性。

為什麼? 全域屬性是指必須出現在所有 HTML 元素中的屬性。一些範例包括 tabindexrole。建議將自訂元素初始的 tabindex 設為 0,讓自訂元素成為可聚焦的鍵盤。不過,請務必先檢查,確認使用您元素的開發人員是否已將此元素設為其他值。舉例來說,如果將 tabindex 設為 -1,代表他們不想讓元素進行互動。
範例 <howto-checkbox> 元素。詳情請參閱不要覆寫網頁作者。

一律接受原始資料 (字串、數字、布林值) 做為屬性或屬性。

為什麼? 自訂元素 (例如內建項目) 應可供設定。 設定可透過宣告方式、屬性或透過 JavaScript 屬性以命令方式傳遞。在理想情況下,所有屬性都應連結至對應的資源。
範例 <howto-checkbox> 元素。

盡量讓原始資料屬性和屬性保持同步,同時反映資源與屬性,反之亦然。

為什麼? 您永遠不知道使用者將如何與您的元素互動。他們可能會在 JavaScript 中設定屬性,然後預期使用 getAttribute() 等 API 讀取該值。如果每項屬性都有對應的屬性,且兩者都反映了相同的屬性,則可讓使用者更容易使用您的元素。換句話說,呼叫 setAttribute('foo', value) 也應一併設定對應的 foo 屬性,反之亦然。當然,這項規則仍有例外情況。請勿反映高頻率屬性,例如影片播放器中的 currentTime。請運用最佳判斷。如果是使用者與屬性或屬性互動,並不容易反映該屬性或屬性,
範例 <howto-checkbox> 元素。詳情請參閱避免重複發生的問題一節。

僅接受多媒體資料 (物件、陣列) 做為屬性。

為什麼? 一般來說,沒有任何範例內建 HTML 元素透過屬性 (純 JavaScript 物件和陣列) 接受多媒體資料。透過方法呼叫或屬性接受多媒體資料。接受多媒體資料做為屬性有以下兩個明顯缺點:將大型物件序列化為字串可能非常昂貴,而且所有物件參照在這個字串化過程中都會遺失。舉例來說,如果您將具有參照其他物件 (或可能是 DOM 節點) 的物件字串化,這些參照就會遺失。

不要反映屬性的多媒體資料屬性。

為什麼? 將多媒體資料屬性反映到屬性成本非常高昂,而且必須對相同的 JavaScript 物件進行序列化和還原序列化作業。除非您有隻能透過這項功能解決的用途,否則建議您避免使用。

建議檢查元素在升級之前是否設定過屬性。

為什麼? 載入元素定義之前,使用您元素的開發人員可能會嘗試設定元素的屬性。這種情況在開發人員使用的架構上會處理載入元件、在頁面中標示元件,以及將元件屬性繫結至模型時更是如此。
範例 <howto-checkbox> 元素。詳情請參閱將屬性設為延遲

請勿自行套用類別。

為什麼? 需要表示狀態的元素應使用屬性來達到此目的。一般而言,使用您的元素是 class 屬性的擁有者,因此可能會在無意間寫入開發人員類別。

活動

因應內部元件活動分派事件。

為什麼? 例如,當計時器或動畫完成,或是資源載入完畢時,元件可能會因為元件知道的活動而發生變更的屬性。建議您根據這些變更進行分派事件,通知主機元件的狀態不同。

請勿因主機設定屬性 (向下資料流) 而分派事件。

為什麼? 因回應主機設定而分派事件的做法非常多 (主機知道目前狀態,因為已設定該屬性)。因應主機設定屬性分派事件,可能會導致資料繫結系統產生無限迴圈。
範例 <howto-checkbox> 元素。

釋疑影片

不要覆寫網頁作者

使用元素的開發人員可能會想覆寫部分初始狀態。例如,變更其 ARIA roletabindex 的可聚焦性。請先確認這些屬性和任何其他全域屬性是否都已設定,再套用您自己的值。

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

將屬性設為延遲

載入定義之前,開發人員可能會嘗試在元素上設定屬性。尤其在開發人員使用的架構來處理載入元件、將元件插入頁面,以及將屬性繫結至模型時,這一點尤其重要。

在以下範例中,Angular 透過宣告將模型的 isChecked 屬性繫結至核取方塊的 checked 屬性。如果如何勾選方塊的定義延遲載入,Angular 可能會嘗試在元素升級前設定已勾選的屬性。

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

自訂元素應檢查其執行個體是否已設定任何屬性,以處理這個情況。<howto-checkbox> 使用名為 _upgradeProperty() 的方法示範這個模式。

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

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

_upgradeProperty() 會擷取未升級執行個體的值並刪除該屬性,因此不會遮蔽自訂元素本身的屬性 setter。這樣一來,在元素定義最終載入時,就能立即反映正確的狀態。

避免重複發生的問題

建議您使用 attributeChangedCallback() 來反映基礎屬性的狀態,例如:

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

但如果屬性 setter 也反映了屬性,這可能會建立無限迴圈。

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

另一個方法是讓屬性 setter 反映屬性,並讓 getter 根據屬性決定其值。

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

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

在這個範例中,新增或移除屬性也會設定屬性。

最後,attributeChangedCallback() 可用來處理連帶效果,例如套用 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;
    ...
  }
}