Shadow DOM

LitコンポーネントはDOMをカプセル化するためにShadow DOMを使います。 Shadow DOMを使うとコンポーネントのDOMツリーをdocumentと分離されているカプセル化されたDOMツリーにすることができます。 DOMのカプセル化はページ内で動作する(Web componentsやLitコンポーネントを含む)他のコードとの相互運用性を実現するための重要な要素です。

Shadow DOMには下記の利点があります。

Shadow DOMに関する詳しい情報はShadow DOM v1: Self-Contained Web ComponentsUsing shadow DOMを見てください。

Shadow DOM内のNodeにアクセスする

LitはrenderRootにコンポーネントをレンダリングします。shadow rootはデフォルトでrenderRootです。 コンポーネント内の要素を取得するためにthis.renderRoot.querySelector()のようなDOMクエリーAPIを使います。

renderRootはshadow rootもしくは1つの要素です。それらは.querySelectorAll().childrenのようなAPIを持ちます。

下記の例では、(firstUpdatedで)コンポーネントの最初のレンダリングの後にコンポーネント内のDOMを取得しています。 また、ゲッタでコンポーネント内のDOMを取得しています。

firstUpdated() {
  this.staticNode = this.renderRoot.querySelector('#static-node');
}

get _closeButton() {
  return this.renderRoot.querySelector('#close-button');
}

LitElementは上記のゲッタの処理を省略して書くためのデコレータのセットを用意しています。

@query、@queryAll、@queryAsyncデコレータ

@query、@queryAll、@queryAsyncデコレータを使うとコンポーネント内にあるNodeに簡単にアクセスすることができます。

@query

クラスプロパティをrenderRootからNodeを返すゲッタに変更します。 オプションである第2引数にtrueを渡すとDOMクエリは1回のみ実行され、その結果がキャッシュされます。 これは取得対象のNodeが代わらないケースではパフォーマンスが向上します。

import {LitElement, html} from 'lit';
import {query} from 'lit/decorators/query.js';

class MyElement extends LitElement {
  @query('#first')
  _first;

  render() {
    return html`
      <div id="first"></div>
      <div id="second"></div>
    `;
  }
}

上記のデコレータを使ったコードは下記と等価です。

get _first() {
  return this.renderRoot?.querySelector('#first') ?? null;
}

@queryAll

@queryと似ていますがマッチするNodeを1つだけ返すのではなくすべて返します。 これはquerySelectorAllを実行することと等価です。

import {LitElement, html} from 'lit';
import {queryAll} from 'lit/decorators/queryAll.js';

class MyElement extends LitElement {
  @queryAll('div')
  _divs;

  render() {
    return html`
      <div id="first"></div>
      <div id="second"></div>
    `;
  }
}

上記の_divsはテンプレート内の<div>要素を2つとも返します。 TypeScriptでの@queryAllプロパティの型はNodeListOf<HTMLElement>です。 取得するNodeが明確である場合、より詳細な型を指定することができます。

@queryAll('button')
_buttons!: NodeListOf<HTMLButtonElement>

buttonsの後の!はTypeScriptのnon-null assertion operatorです。 これはbuttonsには常にnullundefinedが入らないことを示します。

@queryAsync

@queryと似ています。@queryAsyncはNodeを返すのではなく、保留中のレンダリングが完了した後にNodeを解決するPromiseを返します。 updateComplete Promiseをawaitする代わりにこれを使うことができます。 これは@queryAsyncによって返されるNodeが他のプロパティの変更に影響を受ける場合に便利です。

slot要素を使って子要素をレンダリングする

下記のようにコンポーネントに子要素を配置することができます。

<my-element>
  <p>A child</p>
</my-element>

デフォルトでは要素がshadow treeを持つ場合、その子要素はレンダリングされません。

子要素をレンダリングするにはテンプレートに<slot>要素を1つ以上配置する必要があります。 <slot>要素を使って子要素を配置する位置を指定します。

slot要素を使う

子要素をレンダリングするには、要素のテンプレートに<slot>を配置します。 子要素は<slot>要素の子要素の様にレンダリングされます。

名前付きslotを使う

子要素を指定したslot要素に割り当てるには、子要素のslot属性をslot要素のname属性にマッチさせます。

<my-element>
  <p slot="two">Include me in slot "two".</p>
</my-element>

<hr>

<my-element>
  <p slot="one">Include me in slot "one".</p>
  <p slot="nope">This one will not render at all.</p>
  <p>No default slot, so this one won't render either.</p>
</my-element>
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
export class MyElement extends LitElement {
  protected render() {
    return html`
      <p>
        <slot name="one"></slot>
        <slot name="two"></slot>
      </p>
    `;
  }
}

デフォルトでslotに適用されるコンテンツを指定する

slot要素に割り当てられるデフォルトのコンテンツを指定することができます。 slot要素に対応するコンテンツが存在しない場合、デフォルトのコンテンツは表示されます。

<slot>I am fallback content</slot>

デフォルトのコンテンツをレンダリングする 子Nodeがslotに適用された場合、デフォルトのコンテンツはレンダリングされません。 name属性のないslot要素は任意の子Nodeを適用します。 <example-element> </example-element>の様に子Nodeがスペースだけの場合でもデフォルトのコンテンツはレンダリングされません。 custom elementの子要素にLitエクスプレッションを使う場合、 意図した通りにデフォルトのコンテンツがレンダリングされるようにレンダリングしない値を使ってください。 詳しくはレンダリングしない値を見てください。

slotに適用された子要素にアクセスする

shadow root内のslotに割り当てられた子要素にアクセスするには、 slotchangeイベントでWeb標準のslot.assignedNodesメソッドもしくはslot.assignedElementsメソッドを使います。

下記のように、特定のslotに割り当てられた要素を返すゲッタを作成することができます。

get _slottedChildren() {
  const slot = this.shadowRoot.querySelector('slot');
  return slot.assignedElements({flatten: true});
}

slotchangeイベントを使うとslotに割り当てられたNodeが変更された時に処理を実行することができます。 下記の例では、すべてのslotに割り当てられた要素のテキストコンテンツを取得しています。

handleSlotchange(e) {
  const childNodes = e.target.assignedNodes({flatten: true});
  this.allText = childNodes.map((node) => {
    return node.textContent ? node.textContent : ''
  }).join('');
}

render() {
  return html`<slot @slotchange=${this.handleSlotchange}></slot>`;
}

詳しくはHTMLSlotElementを見てください。

@queryAssignedElementsデコレータと@queryAssignedNodesデコレータ

@queryAssignedElementsはクラスのプロパティを指定したslotのslot.assignedElementsを返すgetterに変換します。 @queryAssignedNodesはクラスのプロパティを指定したslotのslot.assignedNodesを返すgetterに変換します。 これらのクエリを使ってslotに割り当てられた要素もしくはNodeを取得します。

これら2つのデコレータにオプションで下記のプロパティを持つobjectを渡すことができます。

プロパティ 説明
flatten slot.assignedElementsslot.assignedNodesの引数のflatten
slot クエリの対象となるslot要素のname属性を指定します。何も指定しない場合はデフォルトのslotになります。
selector (queryAssignedElementsのみ) CSSセレクタを指定します。そのセレクタにマッチした要素のみ返します。

両者の違いは結果に要素のみが含まれるかそれに加えてテキストNodeが含まれるかです。 どちらを使うかはユースケースによります。

@queryAssignedElements({slot: 'list', selector: '.item'})
_listItems!: Array<HTMLElement>;

@queryAssignedNodes({slot: 'header', flatten: true})
_headerNodes!: Array<Node>;

上記のコードは下記と等価です。

get _listItems() {
  const slot = this.shadowRoot.querySelector('slot[name=list]');
  return slot.assignedElements().filter((node) => node.matches('.item'));
}

get _headerNodes() {
  const slot = this.shadowRoot.querySelector('slot[name=header]');
  return slot.assignedNodes({flatten: true});
}

render rootを変更する

各Litコンポーネントはrender rootを保有しています。 render rootはコンポーネント内のDOMを内包しています。

デフォルトでは、LitElementはopenモードのshadowRootを生成します。そして、LitElementはshadowRootの内側にレンダリングします。その結果、下記のようなDOM構造を生成します。

<my-element>
  #shadow-root
    <p>child 1</p>
    <p>child 2</p>

LitElementでrender rootを変更する方法は下記の2つです。

shadowRootOptionsを設定する

render rootを変更する一番シンプルな方法はstatic shadowRootOptionsプロパティを変更することです。 デフォルトのcreateRenderRootの実装は、コンポーネントのshadow rootを作成する際にattachShadowstatic shadowRootOptionsプロパティを引数として渡します。 だから、static shadowRootOptionsプロパティを変更することでmodedelegatesFocus等の設定を変更することができます。

class DelegatesFocus extends LitElement {
  static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};
}

詳しくはElement.attachShadow()を見てください。

createRenderRootを実装する

デフォルトのcreateRenderRootの実装はopen modeのshadow rootを作成します。そして、static styleクラスフィールドにセットされているスタイルをそれに加えます。 詳しくはスタイルを見てください。

コンポーネントのrender rootを変更するには、createRenderRootがテンプレートをレンダリングした結果を内包するNodeを返すように実装します。

例えば、テンプレートを要素の子要素としてメインのDOMツリーにレンダリングする(shadow domではなく通常のDOMとしてレンダリングする)には、createRenderRootthisを返すように実装します。

shadow domではなく通常のDOMとしてレンダリングすることは非推奨です。


License

Japanese part

Creative Commons Attribution-NonCommercial 4.0 International Public License

Copyright (c) 2022 38elements

Other

Creative Commons Attribution 3.0 Unported

Copyright (c) 2020 Google LLC. All rights reserved.

BSD 3-Clause License

Copyright (c) 2020 Google LLC. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.