テンプレートをつくる

lit-htmlは、高速で効率的にHTMLを描画、更新するテンプレートライブラリです。様々なデータを扱うWeb UIをすぐに作ることができます。

この章では、lit-htmlの主な機能と概念を紹介します。

静的HTMLの描画

lit-htmlで行う最も簡単なことは、静的なHTMLを描画することです。

import {html, render} from 'lit-html';

// テンプレートを定義
const  myTemplate = html`<div>Hello World</div>`;

// テンプレートを描画
render(myTemplate, document.body);

lit-htmlテンプレートは、タグ付きテンプレートリテラルです。テンプレート自体は通常のJavaScript文字列のように見えますが、バッククォートで囲われています(`)。ブラウザはlit-htmlのhtmlタグ関数を文字列として認識します。

htmlタグ関数はTemplateResult (テンプレートが描画されるのに使われる軽量オブジェクト)を返します。

実際にrender関数は、DOMを作成し、それらをDOMツリーに追加します。上記の例では描画されたDOMがページのbodyタグの内容を置き換えます。

動的テキストを描画する

基本静的なテンプレートではなにもできません。lit-htmlでは、テンプレートリテラルに${expression}書式のプレースフォルダを使ってデータの表示(バインディング)ができます。

const aTemplate = html`<h1>${title}</h1>`;

動的なテンプレート作るのにテンプレート関数を作ることができます。値が変わるたびにテンプレート関数を呼び出されます。

import {html, render} from 'lit-html';

// テンプレート関数を定義
const  myTemplate = (name) => html`<div>Hello ${name}</div>`;

// 値によってテンプレートが描画される
render(myTemplate('world'), document.body);

// ... あとで ... 
// 違う値によってテンプレートを描画
render(myTemplate('lit-html'), document.body);

テンプレート関数が呼び出されると、lit-htmlはその時点でのJavaScript評価式の値を取得します。テンプレート関数はDOMを作らないので、高速で軽く動作します。

テンプレート関数は、入力値への関数としてTemplateResultを返します。これはlit-htmlの主な原則の1つです: 状態の 関数 としてUIをつくる

renderを呼び出すと、lit-htmlは最後に実行された描画において、変更されたテンプレートの一部分のみを更新します。これにより、lit-htmlの更新は非常に高速になっています。

JavaScript評価式を使う

前述の例では単純にテキストを挿入していますが、JavaScript評価式も使えます:

const myTemplate = (subtotal, tax) => html`<div>Total: ${subtotal + tax}</div>`;
const myTemplate2 = (name) => html`<div>${formatName(name.given, name.family, name.title)}</div>`;

属性へのバインド

テキストコンテンツにJavaScript評価式が使えることに加え、nodeの属性(attribute)やプロパティ(property)にも値をバインドすることができます。

デフォルトでは、属性への値の変更によって属性も変更されます:

// class属性を付与
const myTemplate(data) = html`<div class=${data.cssClass}>Stylish text.</div>`;

属性値は常に文字列となるので、JavaScript評価式は文字列に変換する必要があります。

’?’を接頭辞(prefix)に使うことによって属性に真偽値(boolean)を設定します。真偽値が真と評価された(truthy)時に属性が追加され、偽(falsy)の場合に取り除かれます:

const myTemplate2 = (data) => html`<div ?disabled=${!data.active}>Stylish text.</div>`;

プロパティへのバインド

nodeのJavaScriptプロパティにバインドするには.を接頭辞を使います:

const myTemplate3 = (data) => html`<my-list .listItems=${data.items}></my-list>`;

プロパティ・バインディングによって、複雑なデータをサブコンポーネントに渡すことができます。

この例のプロパティ名(listItems)は大文字と小文字が混在していることに注意してください。HTML属性は大文字と小文字を区別しませんが、lit-htmlはテンプレートを処理する際に大文字と小文字を区別します。

イベントハンドラの追加

テンプレートには宣言型イベントリスナーも含めることができます。イベントリスナは属性バインディングに似ていますが、接頭辞 @の後にイベント名が続きます:

const myTemplate = () => html`<button @click=${clickHandler}>Click Me!</button>`;

これはボタン要素へのaddEventListener('click', clickHandler)と同じです。

イベントリスナーは、普通の関数か、handleEventメソッドを持つオブジェクトのいずれかになります。

const clickHandler = {
  // handleEvent メソッドが必要
  handleEvent(e) { 
    console.log('クリックされました!');
  },
  // 0か一つ以上のイベントリスナオプションを
  // 持つことができます: capture, passive, onceなど
  capture: true;
}

イベントリスナーオブジェクト イベントリスナーオブジェクトを使ってリスナーを指定すると、リスナーオブジェクト自身がイベントコンテキスト(thisの値)として設定されます。 {.alert .alert-info}

テンプレートの入れ子

さらに複雑なテンプレートを作成するためにテンプレートを入れ子にできます。テキストを表示する TemplateResult であれば、TemplateResult はそこに挿入されます。

const myHeader = html`<h1>Header</h1>`;
const myPage = html`
  ${myHeader}
  <div>Here's my main page.</div>
`;

TemplateResultを返すテンプレート関数であれば、一緒に使えます:

// some complex view
const myListView = (items) => html`<ul>...</ul>`;

const myPage = (data) => html`
  ${myHeader}
  ${myListView(data.items)}
`;

テンプレートに条件分岐と繰り返しが使えることにより、様々なことができるようになります。

条件分岐テンプレート

lit-htmlには条件分岐に特別な組み込みの制御方法はありません。代わりに、通常のJavaScript評価式とJavaScript文を使います。

三項演算子による条件式

三項演算子は、インラインで条件を追加するのに最適です。

html`
  ${user.isloggedIn
      ? html`Welcome ${user.name}`
      : html`Please log in`
  }
`;

if文による条件式

if文の条件をテンプレートの外部で定義し、テンプレート内で使うことができます。

getUserMessage() {
  if (user.isloggedIn) {
    return html`Welcome ${user.name}`;
  } else {
    return html`Please log in`;
  }
}

html`
  ${getUserMessage()}
`

繰り返しのテンプレート

Array.mapによる繰り返し

標準のJavaScriptの機能を使って繰り返しのテンプレートをつくることができます。

また、lit-htmlには ディレクティブ (directives)というテンプレートで使われる特別な関数がいくつか用意されています。例えばrepeatディレクティブを使って、特定の動的リストをより効率的に描画することができます。

リストを描画するのに、Array.mapを使ってデータのリストをテンプレートのリストに変換ができます:

html`
  <ul>
    ${items.map((item) => html`<li>${item}</li>`)}
  </ul>
`;

このJavaScript評価式はTemplateResultオブジェクトの配列を返していることに注意してください。lit-htmlは、配列やイテラブル(iterable)なサブテンプレートやその他の値を描画します。

ループ文による繰り返し

また、別にテンプレートの配列を作成し、そのままテンプレートに渡すこともできます。

const itemTemplates = [];
for (const i of items) {
  itemTemplates.push(html`<li>${i}</li>`);
}

html`
  <ul>
    ${itemTemplates}
  </ul>
`;

repeatディレクティブによる繰り返し

ほとんどの場合、Array.mapが繰り返しを行う効率的な方法です。ただし、大きなリストを並べ替えたり、個々のエントリを追加・削除する場合は、多数のDOMノードを効率的に再作成する必要があります。

こういった場合に repeat ディレクティブ が使えます。ディレクティブ(Directive)とは描画を特別に制御する拡張可能な関数です。lit-htmlには、repeatのような組み込みのディレクティブが付属してます。

repeatディレクティブは、開発者がリストにおいて指定するユニークキーに基づき効率的に描画更新します。

repeat(items, keyFunction, itemTemplate)

引数:

例えば:

const employeeList = (employees) => html`
  <ul>
    ${repeat(employees, (employee) => employee.id, (employee, index) => html`
      <li>${index}: ${employee.familyName}, ${employee.givenName}</li>
    `)}
  </ul>
`;

employees配列を再度並び換えする場合、repeatディレクティブは既存のDOMノードを並び替えます。

これは普通に並び替えをする場合と何が違うのかというと、大きなリストを逆順に並び替えにすることを想像してください。

どちらがより効率的なのかは、ユースケースによって異なります。DOMノードを更新する方が移動させるよりもコストがかかる場合は、repeatディレクティブを使ってください。それ以外の場合は、Array.mapか、ループを使ってください。

テンプレートのキャッシュ: cacheディレクティブ

ほとんどの場合、条件分岐テンプレートはJavaScriptの条件式で済みます。ただし、大規模で複雑なテンプレートを置き換える場合にDOMを再作成するコストを節約したい場合があります。

こういった場合に cacheディレクティブ が使えます。ディレクティブ(Directive)は描画を特別に制御する拡張可能な関数です。cacheディレクティブは、現在描画していないテンプレートのDOMを保持(キャッシュ)します。

const detailView = (data) => html`<div>...</div>`; 
const summaryView = (data) => html`<div>...</div>`;

html`${cache(data.showDetails
  ? detailView(data) 
  : summaryView(data)
)}`

lit-htmlがテンプレートを再度描画する場合、変更された部分のみが更新されるので、必要以上にDOMを作成したり削除されることはありません。ただし、あるテンプレートから別のテンプレートに切り替えるときは、lit-htmlは古いDOMを削除して新しいDOMツリーを描画します。

cacheディレクティブは、バインディングや入力用に生成されたDOMをキャッシュします。上記の例ではsummaryView、detailViewテンプレートの両方がDOMがキャッシュされます 。あるビューから別のビューに切り替えると、lit-htmlはキャッシュされた新しいビューで入れ替え、最新のデータで更新します。