カスタム要素のベスト プラクティス

カスタム要素を使用すると、独自の HTML タグを作成できます。このチェックリストでは、質の高い要素を作成するためのおすすめの方法を紹介します。

カスタム要素を使用すると、HTML を拡張して独自のタグを定義できます。これらはきわめて強力な機能ですが、低レベルであるため、独自の要素を実装する最適な方法が必ずしも明確であるとは限りません。

最適なエクスペリエンスを実現するため、このチェックリストを以下にまとめました。適切に動作するカスタム要素にするために必要なすべての要素が網羅されています。

チェックリスト

Shadow DOM

スタイルをカプセル化するために Shadow ルートを作成します。

それはなぜでしょうか? 要素のシャドウルートにスタイルをカプセル化すると、使用される場所に関係なく機能するようになります。これは、デベロッパーが自分の要素を別の要素のシャドウルート内に配置する場合に特に重要です。これは、チェックボックスやラジオボタンなどの単純な要素にも適用されます。シャドウルート内のコンテンツがスタイルそのものだけになる場合もあります。
<howto-checkbox> 要素。

コンストラクタ内に Shadow ルートを作成します。

それはなぜでしょうか? コンストラクタは、その要素に関する排他的な知識を持っている場合に使用します。この段階で、他の要素に影響を与えたくない実装の詳細を設定できます。この作業を後のコールバック(connectedCallback など)で行う場合は、要素がデタッチされてからドキュメントに再アタッチされるという状況に備える必要があります。
<howto-checkbox> 要素。

要素が作成するすべての子を Shadow ルートに配置します。

それはなぜでしょうか? 要素によって作成された子は実装の一部であり、非公開にする必要があります。シャドウルートを保護しないと、外部の JavaScript が誤ってこれらの子に干渉する可能性があります。
<howto-tabs> 要素。

<slot> を使用して、Light DOM の子を Shadow DOM に投影します

それはなぜでしょうか? HTML の子によってコンポーネントがよりコンポーズ可能になるため、コンポーネントのユーザーがコンポーネント内のコンテンツを指定できるようになります。ブラウザがカスタム要素をサポートしていない場合、ネストされたコンテンツは引き続き利用可能であり、表示とアクセスが可能です。
<howto-tabs> 要素。

デフォルトの inline を使用したくない場合は、:host 表示スタイルを設定します(例: blockinline-blockflex)。

それはなぜでしょうか? カスタム要素はデフォルトで display: inline であるため、width または height を設定しても効果はありません。これはデベロッパーが驚くことが多く、ページのレイアウトに関連する問題を引き起こす可能性があります。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> 要素。詳細については、リエントランシーの問題を回避するをご覧ください。

リッチデータ(オブジェクト、配列)のみをプロパティとして受け入れることを目指します。

それはなぜでしょうか? 一般的に、属性を使用してリッチデータ(プレーンな JavaScript オブジェクトや配列)を受け入れる組み込みの HTML 要素の例はありません。リッチデータは、メソッド呼び出しまたはプロパティを介して受け入れられます。リッチデータを属性として受け取ることには、明らかにいくつかのデメリットがあります。大きなオブジェクトを文字列にシリアル化するには費用がかかることがあり、この文字列化プロセスでオブジェクト参照がすべて失われます。たとえば、別のオブジェクトや 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() は、アップグレードされていないインスタンスから値を取得し、プロパティを削除します。これにより、カスタム要素独自のプロパティ セッターがシャドーイングされなくなります。これにより、要素の定義が最終的に読み込まれたときに、すぐに正しい状態を反映できます。

リピート回数に関する問題の回避

次のように、attributeChangedCallback() を使用して状態を基盤となるプロパティに反映したくなるかもしれません。

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

しかし、プロパティ セッターも属性に反映されている場合、無限ループが発生する可能性があります。

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

または、プロパティ セッターが属性に反映されるようにし、ゲッターが属性に基づいて値を決定するようにすることもできます。

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