ウェブ コンポーネントから Lit Element へ

1. はじめに

最終更新日: 2021 年 8 月 10 日

ウェブ コンポーネント

Web Components は、ウェブページやウェブアプリで使用する、再利用可能なカプセル化されたカスタム HTML タグを作成できるウェブ プラットフォーム API セットです。ウェブ コンポーネント標準に基づいて構築されたカスタム コンポーネントやウィジェットは、最新のブラウザで動作し、HTML で動作する任意の JavaScript ライブラリやフレームワークで使用できます。

Lit とは

Lit は、あらゆるフレームワークで動作する、またはフレームワークをまったく必要としない、高速で軽量のウェブ コンポーネントを構築するためのシンプルなライブラリです。Lit では、共有可能なコンポーネント、アプリケーション、デザイン システムなどを作成できます。

Lit には、プロパティ、属性、レンダリングの管理など、Web Components の一般的なタスクを簡素化するための API が用意されています。

学習内容

  • Web コンポーネントとは
  • Web Components のコンセプト
  • ウェブ コンポーネントを作成する方法
  • lit-html と LitElement とは
  • ウェブ コンポーネント上での Lit の動作

作成するアプリの概要

  • 標準的な高評価 / 低評価ウェブ コンポーネント
  • 高評価 / 低評価 Lit ベースのウェブ コンポーネント

必要なもの

  • 更新された最新のブラウザ(Chrome、Safari、Firefox、Chromium Edge)。Web Components は最新のすべてのブラウザで動作し、ポリフィルは Microsoft Internet Explorer 11 と Chrome 以外の Microsoft Edge で使用できます。
  • HTML、CSS、JavaScript、Chrome DevTools の知識。

2. 設定とプレイグラウンドを探索する

コードへのアクセス

Codelab の随所で、次のように Lit プレイグラウンドへのリンクが表示されます。

コードのチェックポイント

このプレイグラウンドは、ブラウザで完全に動作するコード サンドボックスです。TypeScript ファイルと JavaScript ファイルをコンパイルして実行できるほか、ノード モジュールへのインポートを自動的に解決することもできます。例:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

これらのチェックポイントを開始点として使用して、Lit Playground でチュートリアル全体を進めることができます。VS Code を使用している場合は、これらのチェックポイントを使用して、任意のステップの開始コードをダウンロードしたり、それらを使用して作業をチェックしたりできます。

点灯している Playground UI の探索

ファイルセレクタのタブバーには [Section 1]、コード編集セクションには [Section 2]、出力プレビューには [Section 3]、プレビューの再読み込みボタンには [Section 4] というラベルが付いています。

Lit プレイグラウンド UI のスクリーンショットに、この Codelab で使用するセクションがハイライト表示されています。

  1. ファイルセレクタ。プラスボタンにも注目してください...
  2. ファイル エディタ。
  3. コードのプレビュー。
  4. [再読み込み] ボタン。
  5. ダウンロード ボタン。

VS Code の設定(上級者向け)

この VS Code 設定を使用するメリットは次のとおりです。

  • テンプレート タイプのチェック
  • テンプレートの情報と予測入力

NPM、VS Code(および lit-plugin プラグイン)がすでにインストールされており、その環境の使用方法を把握している場合は、次の手順でプロジェクトをダウンロードして開始できます。

  • ダウンロード ボタンを押します
  • tar ファイルの内容をディレクトリに展開する
  • ベア モジュール指定子を解決できる開発用サーバー(Lit チームでは @web/dev-server を推奨)をインストールします。
  • 開発用サーバーを実行してブラウザを開きます(@web/dev-server を使用している場合は npx web-dev-server --node-resolve --watch --open を使用できます)。
    • サンプル package.json を使用している場合は、npm run serve を使用します。

3. カスタム要素を定義する

カスタム要素

ウェブ コンポーネントとは、4 つのネイティブ ウェブ API の集合体です。それらは次のとおりです。

  • ES モジュール
  • カスタム要素
  • Shadow DOM
  • HTML テンプレート

ES モジュールの仕様をすでに使用しています。これにより、<script type="module"> でページに読み込まれるインポートとエクスポートを備えた JavaScript モジュールを作成できます。

カスタム要素の定義

コードのチェックポイント

カスタム要素の仕様では、ユーザーが JavaScript を使用して独自の HTML 要素を定義できます。ネイティブのブラウザ要素と区別できるように、名前にはハイフン(-)を含める必要があります。index.js ファイルをクリアし、カスタム要素クラスを定義します。

index.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

カスタム要素は、HTMLElement を拡張するクラスをハイフン付きのタグ名に関連付けることで定義します。customElements.define の呼び出しは、クラス RatingElement を tagName ‘rating-element' に関連付けるようブラウザに指示します。つまり、<rating-element> という名前のドキュメント内のすべての要素がこのクラスに関連付けられます。

ドキュメントの本文に <rating-element> を配置して、レンダリングされる内容を確認します。

index.html

<body>
 <rating-element></rating-element>
</body>

ここで出力を見ると、何もレンダリングされていないことがわかります。<rating-element> を表示する方法をブラウザに設定していないため、これは想定どおりの結果です。Chrome Dev Tools で <rating-element> を選択することで、カスタム要素の定義が成功したことを確認できます。要素セレクタを使用し、コンソールで以下を呼び出します。

$0.constructor

出力は次のようになります。

class RatingElement extends HTMLElement {}

カスタム要素のライフサイクル

カスタム要素には一連のライフサイクル フックがあります。それらは次のとおりです。

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor は、document.createElement(‘rating-element')new RatingElement() の呼び出しなど、要素が最初に作成されるときに呼び出されます。コンストラクタは要素を設定する場所として適していますが、要素の「起動」のためにコンストラクタ内で DOM 操作を行うことは一般的におすすめできません。パフォーマンス上の理由を確認できます。

connectedCallback は、カスタム要素が DOM に追加されると呼び出されます。通常、この段階で最初の DOM 操作が行われます。

disconnectedCallback は、カスタム要素が DOM から削除されると呼び出されます。

attributeChangedCallback(attrName, oldValue, newValue) は、ユーザー指定の属性のいずれかが変更されると呼び出されます。

adoptedCallback は、HTMLTemplateElement などで、adoptNode を介して別の documentFragment からメイン ドキュメントにカスタム要素が採用されたときに呼び出されます。

DOM のレンダリング

ここでカスタム要素に戻り、DOM を関連付けます。DOM にアタッチされるときの要素のコンテンツを設定します。

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

constructor では、rating というインスタンス プロパティを要素に格納します。connectedCallback で、DOM の子を <rating-element> に追加して、現在の評価と高評価 / 低評価ボタンを表示します。

4. Shadow DOM

Shadow DOM が選ばれる理由

前の手順で挿入した style タグのセレクタでは、ページ上の評価要素とボタンが選択されます。その結果、スタイルが要素の外に出て、スタイルを設定するつもりがない他のノードが選択されることがあります。また、このカスタム要素外の他のスタイルによって、カスタム要素内のノードが意図せずスタイル設定されることがあります。たとえば、メイン ドキュメントのヘッド部にスタイルタグを配置してみましょう。

コードのチェックポイント

index.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

出力では、評価のスパンの周りに赤い枠線ボックスが表示されます。これはごくわずかなケースですが、DOM カプセル化がないため、より複雑なアプリケーションでは大きな問題が発生する可能性があります。そこで役に立つのが Shadow DOM です。

Shadow ルートのアタッチ

要素に Shadow Root をアタッチし、そのルート内に DOM をレンダリングします。

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

ページを更新すると、メイン ドキュメントのスタイルでは Shadow Root 内のノードを選択できなくなっていることがわかります。

この操作をどのように行いましたか。connectedCallback で、要素にシャドウルートをアタッチする this.attachShadow を呼び出しました。open モードは、シャドウ コンテンツを検査可能で、this.shadowRoot を介してシャドウルートにもアクセスできることを意味します。Chrome インスペクタでウェブ コンポーネントも確認します。

Chrome インスペクタの DOM ツリー。<rating-element> がa#shadow-root(open)を子として、その Shadowroot の中の以前の DOM です。

コンテンツを保持している展開可能なシャドウルートが表示されます。その Shadow ルートの内側にあるものはすべて、Shadow DOM と呼ばれます。Chrome Dev Tools で評価要素を選択して $0.children を呼び出すと、子が返されないことがわかります。これは、Shadow DOM が直接の子と同じ DOM ツリーの一部ではなく、Shadow Tree とみなされるためです。

Light DOM

テスト: <rating-element> の直接の子としてノードを追加します。

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

ページを更新すると、このカスタム要素の Light DOM 内のこの新しい DOM ノードはページに表示されません。これは、Shadow DOM には、<slot> 要素を介して Light DOM ノードを Shadow DOM にどのように投影するかを制御する機能があるためです。

5. HTML テンプレート

テンプレートを使用する理由

innerHTML とテンプレートのリテラル文字列をサニタイズせずに使用すると、スクリプト インジェクションでセキュリティ上の問題が発生する可能性があります。これまでは DocumentFragment を使用するメソッドがありましたが、テンプレート定義時に画像の読み込みやスクリプトが実行されるなどの問題や、再利用性の障害が生じるという他の問題も伴います。ここで役立つのが <template> 要素です。テンプレートでは、inert DOM、ノードをクローニングする高パフォーマンスの方法、再利用可能なテンプレートを使用できます。

テンプレートの使用

次に、HTML テンプレートを使用するようにコンポーネントを移行します。

コードのチェックポイント

index.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

ここでは、DOM コンテンツをメイン ドキュメントの DOM のテンプレート タグに移動しました。カスタム要素の定義をリファクタリングします。

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

このテンプレート要素を使用するには、テンプレートにクエリを実行して内容を取得し、templateContent.cloneNode を使用してそれらのノードのクローンを作成します。true 引数でディープ クローンを実行します。次に、そのデータを使用して DOM を初期化します。

これで、Web Component が完成しました。まだ何の機能もありません。次に、いくつかの機能を追加します。

6. 機能の追加

プロパティ バインディング

現在、rating-element に評価を設定する唯一の方法は、要素を作成し、オブジェクトに rating プロパティを設定して、ページに配置することです。残念ながら、これはネイティブ HTML 要素の動作とは異なります。ネイティブ HTML 要素は、プロパティと属性の両方の変更に伴って更新される傾向があります。

以下の行を追加して、rating プロパティが変更されたときにカスタム要素でビューが更新されるようにします。

コードのチェックポイント

index.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

評価プロパティにセッターとゲッターを追加し、評価要素のテキストが利用できる場合は更新します。つまり、この要素に評価プロパティを設定すると、ビューが更新されます。Dev Tools コンソールで簡単なテストを行います。

属性バインディング

次に、属性が変更されたときにビューを更新します。これは、<input value="newValue"> を設定した場合にビューを更新する入力に似ています。幸い、ウェブ コンポーネントのライフサイクルには attributeChangedCallback が含まれています。以下の行を追加して評価を更新します。

index.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

attributeChangedCallback をトリガーするには、RatingElement.observedAttributes which defines the attributes to be observed for changes の静的ゲッターを設定する必要があります。次に、DOM で宣言的に評価を設定します。次のように入力します。

index.html

<rating-element rating="5"></rating-element>

評価は宣言的に更新される必要があります。

ボタンの機能

足りないのはボタンの機能だけです。このコンポーネントの動作では、ユーザーが高評価または低評価を 1 つ指定し、ユーザーに視覚的なフィードバックを提供できるようにする必要があります。これは、いくつかのイベント リスナーとリフレクション プロパティで実装できますが、最初に以下の行を追加して、視覚的なフィードバックが得られるようにスタイルを更新します。

index.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

Shadow DOM では、:host セレクタは Shadow ルートがアタッチされているノードまたはカスタム要素を参照します。この場合、vote 属性が "up" の場合は高評価ボタンが緑色になりますが、vote 属性が "down", then it will turn the thumb-down button red の場合は高評価ボタンが緑色になります。次に、rating を実装した方法と同様に、vote のリフレクション プロパティまたは属性を作成して、このロジックを実装します。プロパティ セッターとゲッターから始めます。

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

constructornull を使用して _vote インスタンス プロパティを初期化し、セッターで新しい値が異なるかどうかを確認します。その場合は、それに応じて評価を調整します。また、重要な点として、vote 属性を this.setAttribute でホストに反映させます。

次に、属性のバインディングを設定します。

index.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

繰り返しになりますが、これは rating 属性バインディングで行ったプロセスと同じです。voteobservedAttributes に追加し、vote プロパティを attributeChangedCallback に設定するとします。最後に、クリック イベント リスナーを追加して、ボタンに機能を追加します。

index.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

constructor で、いくつかのクリック リスナーを要素にバインドし、参照を維持します。connectedCallback で、ボタンのクリック イベントをリッスンします。disconnectedCallback でこれらのリスナーをクリーンアップし、クリック リスナー自体で、vote を適切に設定します。

これで、完全な機能を備えたウェブ コンポーネントが完成しました。ボタンをクリックしてみて!問題は、JS ファイルが 96 行、HTML ファイルは 43 行に達し、コードが非常に冗長で、このような単純なコンポーネントにとって不可欠なことです。ここで役立つのが Google の Lit プロジェクトの出番です。

7. lit-html

コードのチェックポイント

コードのチェックポイント

lit-html を使用する理由

何よりもまず、<template> タグは有用でパフォーマンスが優れていますが、コンポーネントのロジックとともにパッケージ化されていないため、テンプレートを残りのロジックとともに配布することが困難です。さらに、テンプレート要素の使用方法は本質的に命令型コードに適しており、多くの場合、宣言型コーディング パターンと比べてコードが読みにくくなります。

そこで役立つのが lit-html です。Lit html は Lit のレンダリング システムです。JavaScript で HTML テンプレートを記述し、データとともにそれらのテンプレートを効率的にレンダリングおよび再レンダリングして、DOM を作成および更新します。これは一般的な JSX ライブラリや VDOM ライブラリに似ていますが、ブラウザでネイティブに実行され、多くの場合、はるかに効率的に実行されます。

Lit HTML の使用

次に、ネイティブ ウェブ コンポーネント rating-element を移行して、タグ付きテンプレート リテラルを使用する Lit テンプレートを使用します。これは、テンプレート文字列を、特別な構文で引数として受け取る関数です。その後、Lit は内部でテンプレート要素を使用して、レンダリングを高速化するとともに、セキュリティのためのサニタイズ機能を提供します。まず、ウェブ コンポーネントに render() メソッドを追加して、index.html<template> を Lit テンプレートに移行します。

index.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

index.html からテンプレートを削除することもできます。このレンダリング メソッドでは、template という変数を定義し、html タグ付きのテンプレートのリテラル関数を呼び出します。また、${...} のテンプレート リテラル補間構文を使用して、span.rating 要素内で単純なデータ バインディングを実行しています。つまり、最終的にそのノードを命令的に更新する必要がなくなるということです。さらに、lit render メソッドを呼び出して、テンプレートを Shadow ルートに同期的にレンダリングします。

宣言型構文への移行

<template> 要素を取り除いたので、コードをリファクタリングして、新しく定義された render メソッドを呼び出します。まず、lit のイベント リスナー バインディングを活用して、リスナーコードを消去します。

index.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

Lit テンプレートでは、@EVENT_NAME バインディング構文を使用してノードにイベント リスナーを追加できます。この場合、ボタンがクリックされるたびに vote プロパティを更新します。

次に、constructorconnectedCallbackdisconnectedCallback 内のイベント リスナーの初期化コードをクリーンアップします。

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

3 つのコールバックすべてからクリック リスナーのロジックを削除し、さらに disconnectedCallback を完全に削除することもできました。また、connectedCallback から DOM 初期化コードをすべて削除して、見た目をより洗練されたものにすることもできました。これは、_onUpClick リスナー メソッドと _onDownClick リスナー メソッドを削除できることも意味します。

最後に、render メソッドを使用するようにプロパティ セッターを更新して、プロパティまたは属性が変更されたときにドメインを更新できるようにします。

index.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

ここでは、rating セッターから DOM 更新ロジックを削除し、vote セッターから render の呼び出しを追加しました。バインディングとイベント リスナーが適用される場所がわかるようになったため、テンプレートがさらに読みやすくなりました。

ページを更新すると、正常に機能する評価ボタンが表示され、賛成票が押されたときに次のように表示されます。

高評価 / 低評価スライダー(値が 6、高評価の親指の色が緑色)

8. LitElement

LitElement を選ぶ理由

コードに問題があります。まず、vote プロパティまたは属性を変更すると、rating プロパティが変更され、render が 2 回呼び出される可能性があります。レンダリングの繰り返し呼び出しは基本的に NoOps で効率的ですが、JavaScript VM はその関数を同期的に 2 回呼び出すことに依然として時間を費やしています。2 つ目は、大量のボイラープレート コードを必要とするため、新しいプロパティと属性を追加するのが面倒なことです。そこで出番となるのが LitElement です。

LitElement は、フレームワークや環境を横断して使用できる高速で軽量なウェブ コンポーネントを作成するための Lit の基本クラスです。次に、それを使用するように実装を変更することで、LitElementrating-element で何ができるかを見てみましょう。

LitElement の使用

まず、lit パッケージから LitElement 基本クラスをインポートしてサブクラス化します。

コードのチェックポイント

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

rating-element の新しい基本クラスである LitElement をインポートします。次に、html インポートを保持し、最後に css を使用します。これにより、内部で CSS 数学、テンプレート、その他の機能用に CSS タグ付きのテンプレート リテラルを定義できます。

次に、スタイルを render メソッドから Lit の静的スタイルシートに移動します。

index.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

Lit では、ほとんどのスタイルがこれに該当します。Lit はこれらのスタイルを利用し、Constructable Stylesheets などのブラウザ機能を使用してレンダリング時間を短縮します。また、必要に応じて古いブラウザの Web Components ポリフィルにも渡します。

Lifecycle

Lit には、ネイティブのウェブ コンポーネントのコールバックに加えて、レンダリング ライフサイクル コールバック メソッドのセットが導入されています。これらのコールバックは、宣言された Lit プロパティが変更されたときにトリガーされます。

この機能を使用するには、レンダリング ライフサイクルをトリガーするプロパティを静的に宣言する必要があります。

index.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

ここでは、ratingvote が LitElement レンダリング ライフサイクルをトリガーすることと、文字列属性をプロパティに変換するために使用される型を定義します。

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

さらに、vote プロパティの reflect フラグにより、vote セッターで手動でトリガーしたホスト要素の vote 属性が自動的に更新されます。

これで静的プロパティ ブロックが用意できたので、属性とプロパティのレンダリング更新ロジックをすべて削除できます。つまり、以下のメソッドを削除できます。

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating(セッターとゲッター)
  • vote(セッターとゲッターだが、変更ロジックはセッターから取得する)

残っているのは、constructor と、新しい willUpdate ライフサイクル メソッドの追加です。

index.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

ここでは、ratingvote を単純に初期化し、vote セッター ロジックを willUpdate ライフサイクル メソッドに移動します。LitElement はプロパティの変更をバッチ処理し、レンダリングを非同期で行うため、更新対象のプロパティが変更されるたびに、willUpdate メソッドが render の前に呼び出されます。willUpdate でリアクティブなプロパティ(this.rating など)を変更しても、不要な render ライフサイクル呼び出しがトリガーされることはありません。

最後に、render は LitElement ライフサイクル メソッドであり、Lit テンプレートを返す必要があります。

index.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

シャドウルートを確認する必要がなくなり、以前に 'lit' パッケージからインポートした render 関数を呼び出す必要もなくなります。

要素がプレビューでレンダリングされます。クリックしてみてください。

9. 完了

お疲れさまでした。これで、ウェブ コンポーネントをゼロから作成し、LitElement に進化させることができました。

Lit は超小型(圧縮して gzip 圧縮したものが 5 KB 未満)、非常に高速で、コーディングも非常に楽しい。コンポーネントを他のフレームワークで使用できるようにすることも、それを使用して本格的なアプリを構築することもできます。

以上で、ウェブ コンポーネントの概要、作成方法、Lit を使用して簡単に作成する方法を学びました。

コードのチェックポイント

完成したコードを Google のコードと照らし合わせてチェックしますか?こちらで比較できます。

次のステップ

他の Codelab もぜひご覧ください。

参考資料

コミュニティ