エクスプレッション

Litテンプレートはエクスプレッション(expressions)と呼ばれる動的な値を${...}の形式で埋め込むことができます。 エクスプレッションをJavaScriptのにすることもできます。 エクスプレッションはテンプレートが評価されるときに評価されます。そして、その結果はテンプレートのレンダリング結果に影響を与えます。 Litコンポーネントはrenderメソッドを実行する毎にこれをします。

エクスプレッションはテンプレートの特定の場所にのみ配置することができます。 エクスプレッションがどう解釈されるかは、それがある場所で決まります。 例えば、要素タグ内にあるエクスプレッションはその要素に影響を与えます。 要素のコンテンツ内にあるエクスプレッションは子Nodeと同じような位置で子Nodeやテキストをレンダリングします。

エクスプレッションの値が有効かどうかはエクスプレッションの位置によって異なります。 一般的に全てのエクスプレッションは文字列や数値などのプリミティブな値を受け入れます。そして、いくつかのエクスプレッションはそれに加えていくつかの型が使用可能です。 それに加えて、全てのエクスプレッションはディレクティブを受け入れることができます。 ディレクティブはエクスプレッションはの処理を変更してレンダリングする特別な関数です。 詳しくはカスタムディレクティブを見てください。

以下に各エクスプレッションタイプのクイックリファレンスと詳しい説明へのリンクを記載します。

html`
<h1>Hello ${name}</h1>
<ul>
  ${listItems}
</ul>`
html`<div class=${highlightClass}></div>`
html`<div ?hidden=${!show}></div>`
html`<input .value=${value}>`
html`<button @click=${this._clickHandler}>Go</button>`
html`<input ${ref(inputRef)}>`

以下のセクションで各エクスプレッションの詳しい説明をします。 テンプレートの構造のより詳しい説明はWell-formed HTML有効なエクスプレッションの位置を見てください。

Child expressions

要素のタグの始まりと終わりの間にあるエクスプレッションは要素に子Nodeを加えます。例えば、

html`<p>Hello, ${name}</p>`

もしくは、

html`<main>${bodyText}</main>`

この位置にあるエクスプレッションは以下の値を受け入れることができます。

プリミティブ値

Litはほとんどすべてのプリミティブ値をレンダリングすることができます。 そして、テキストコンテントに挿入される場合はそれらを文字列に変換します。

5のような数値は'5'の文字列にレンダリングされます。 BigIntも同様に扱われます。

booleanのtrue'true'にレンダリングされます。false'false'にレンダリングされます。でも、ふつうはbooleanでこういうことはしません。 通常、booleanは条件として使われるます。詳しくは条件見てください。

空文字('')、nullundefinedは特別な意味を持ちます。 そして、それらは何もレンダリングしません。 詳しくは子コンテンツの削除を見てください。

Symbolは文字列に変換されません。child expressionに置かれた場合、例外が発生します。

センチネル値

Litはchild expressionに使うことができるいくつかの特別なセンチネル値を提供します。

noChangeセンチネル値はエクスプレッションの既存の値を変更しません。 これは通常、カスタムディレクティブで使われます。 詳しくは変更がないことを伝えるを見てください。

nothingセンチネルは何もレンダリングしません。 詳しくは子コンテンツの削除を見てください。

Templates

エクスプレッション内にTemplateResultを返すエクスプレッションを配置することができるので、テンプレートをネストしたり組み合わせたりすることができます。

const nav = html`<nav>...</nav>`;
const page = html`
  ${nav}
  <main>...</main>
`;

これは素のJavaScriptを使って条件分岐のあるテンプレートや繰り返しがあるテンプレート等を生成することができることを意味します。

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

条件分岐のあるテンプレートに関する詳しい説明は条件にあります。

繰り返しがあるテンプレートに関する詳しい説明はリストにあります。

DOM nodes

DOM Nodeはchild expressionに渡すことができます。 通常、DOM Nodeはhtmlを使ったテンプレートを記述することでレンダリングされます。 しかし、必要な時は下記のようにDOM Nodeを直接レンダリングすることができます。 この時、現在の親Nodeから削除されて、NodeはDOMツリーに取り付けられます。

const div = document.createElement('div');
const page = html`
  ${div}
  <p>This is some text</p>
`;

使用可能な型の配列もしくはiterables

エクスプレッションは使用可能な型を格納する配列、iterable、それらの組み合わせを返すことができます。 つまり、Array.map()を使って繰り返し表現を生成することができます。 詳しくはリストを見てください。

子コンテンツの削除

nullundefined、空文字列('')、Litのnothingセンチネル値は、1つ前のレンダリングされたコンテンツを削除します。そして、Nodeをレンダリングしません。

子コンテンツの配置もしくは削除はよく条件分岐によって行われます。 詳しくは条件に応じて何もレンダリングしないを見てください。

フォールバックコンテンツを持つslotに対応するコンテンツがない場合、フォールバックコンテンツがレンダリングされます。 詳しくはデフォルトでslotに適用されるコンテンツを指定するを見てください。

Attribute expressions

エクスプレッションを使って要素の属性やプロパティをセットすることができます。

デフォルトでは属性の値にエクスプレッションがあるとそれが属性の値になります。

html`<div class=${this.textClass}>Stylish text.</div>`;

属性の値は必ず文字列なので、エクスプレッションは文字列に変換することができる値を返す必要があります。

上記のようにエクスプレッションが属性の値全体の場合、属性の値を"で囲むことを省略できます。 下記のようにエクスプレッションが属性の値の一部の場合、属性の値を"で囲む必要があります。

html`<img src="/images/${this.image}">`;

一部のプリミティブ値は属性にセットされると特殊な評価をされます。 Booleanは文字列に変換されます。例えば、false'false'に変換されます。 undefinednullは空文字("")としてレンダリングされます。

Boolean attribute expressions

下記のように属性名の先頭に?を付けるとboolean attributesになります。 エクスプレッションにtrueになる値がセットされると属性は配置されます。 falseになる値がセットされると属性は削除されます。

html`<div ?hidden=${!this.showAdditional}>This text may be hidden.</div>`;

属性の削除

disabledhiddenboolean attributesで対応できます。しかし、属性の値を構成するデータの一部が欠けている場合に属性を削除したい場合があります。

下記の例について考えてみましょう。

html`<img src="/images/${this.imagePath}/${this.imageFile}">`;

this.imagePathもしくはthis.imageFileが定義されていない場合にsrc属性を削除したいとします。

html`<img src="/images/${this.imagePath ?? nothing}/${this.imageFile ?? nothing}">`;

その場合は上記のようにnothingを使います。 nothingが存在するとその属性は削除されます。 ??nullish coalescing operatorです。 これは左側の値がnullもしくはundefinedの場合、右側の値を返します。

ifDefinedディレクティブはvalue ?? nothingと等価です。

html`<img src="/images/${ifDefined(this.imagePath)}/${ifDefined(this.imageFile)}">`;

エクスプレッションの値がfalseや空文字('')の場合に属性を削除したい場合は以下のようにします。

html`<button aria-label="${this.ariaLabel || nothing}"></button>`

Property expressions

プロパティ名の先頭に.を付けるとプロパティにJavaScriptの値ままセットすることができます。

html`<input .value=${this.itemCount}>`;

上記のコードは下記のようにinput要素のvalueプロパティに直接セットすることと同じです。

inputEl.value = this.itemCount;

この構文を使うと子コンポーネントに複雑なデータを渡すことができます。 下記の例では、listItemsプロパティを持つmy-listコンポーネントにオブジェクトの配列を渡すことができます。

html`<my-list .listItems=${this.items}></my-list>`;

この例ではプロパティ名にlistItemsのように大文字と小文字が混在している点に注意してください。 HTMLは大文字と小文字を区別しませんが、Litはテンプレートを処理する際にプロパティ名の大文字と小文字を区別します。

コンポーネントのプロパティに関する詳しい情報はリアクティブプロパティを見てください。

Event listener expressions

イベント名の先頭に@をつけることで、 テンプレートで宣言的にイベントリスナを設定することができます。

html`<button @click=${this.clickHandler}>Click Me!</button>`;

これはbutton要素でaddEventListener('click', this.clickHandler)を実行することに似ています。

設定するイベントリスナは素の関数もしくはhandleEventメソッドを持つオブジェクトです。 それら関数はaddEventListenerの第1引数と同じです。

Litコンポーネントではコンポーネントはイベントリスナに自動的にバインド(bind)されます。 イベントリスナ内のthisはコンポーネントインスタンスを参照します。

clickHandler() {
  this.clickCount++;
}

コンポーネントイベントに関する詳しい情報はイベントを見てください。

Element expressions

Element expressionsで要素インスタンスにアクセスすることができます。

html`<div ${myDirective()}></div>`

Element expressionsにはディレクティブのみ渡すことができます。 それ以外の値が渡された場合は無視されます。

Element expressionで使うことができるビルドインディレクティブの1つにrefディレクティブがあります。 これはレンダリングされた要素の参照を取得することに使います。

html`<button ${ref(this.myRef)}`;

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

Well-formed HTML

Litテンプレートはwell-formed HTMLである必要があります。 テンプレートは値が挿入される前にブラウザのビルドインHTMLパーサーでパースされます。 有効なテンプレートであるためには下記のルールに従う必要があります。

有効なエクスプレッションの位置

エクスプレッションは属性の値もしくは子コンテンツの位置に置く必要があります。

<!-- 属性の値 -->
<div label=${label}></div>
<button ?disabled=${isDisabled}>Click me!</button>
<input .value=${currentValue}>
<button @click=${this.handleClick()}>

<!-- 子コンテンツ -->
<div>${textContent}</div>

Element expressionsは開始タグのタグ名の後に置く必要があります。

<div ${ref(elementReference)}></div>

無効なエクスプレッションの位置

通常、エクスプレッションを下記の位置に配置してはいけません。

上記の無効なエクスプレッションはStatic expressionsを使用した場合、有効になります。 ただし、それは非効率なのでパフォーマンスが重要な場面で使用しないでください。

Static expressions

LitがテンプレートをHTMLとして処理する前に、static expressionsはテンプレートに埋め込まれる特別な値を返します。 それはテンプレートの静的なHTMLの一部になるので、 タグ名や属性名のような普通は配置することができない位置にエクスプレッションを配置することができます。

static expressionsを使うには、static-htmlモジュールから特別なバージョンのhtmlもしくはsvgをimportする必要があります。

import {html, literal} from 'lit/static-html.js';

static-htmlモジュールはstatic expressionsをサポートするhtml関数とsvg関数を提供します。それらはlitモジュールが提供する通常版の代わりに使います。 literalタグ関数を使ってstatic expressionを作成します。

static expressionは低頻度で変更されるテンプレートの箇所や通常版ではできないテンプレートのカスタマイズに使います。 詳しくは有効なエクスプレッションの位置を見てください。 例えば、my-buttonコンポーネントでは<button>タグをレンダリングしますが、そのサブクラスではそこを<a>タグに置き換えたい場合です。 このHTMLタグは変更されません。更に通常のエクスプレッションではタグ名の位置に配置することはできません。 だから、これはstatic expressionsに適したユースケースです。

import {LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {html, literal} from 'lit/static-html.js';

@customElement('my-button')
class MyButton extends LitElement {
  tag = literal`button`;
  activeAttribute = literal`active`;
  @property() caption = 'Hello static';
  @property({type: Boolean}) active = false;

  render() {
    return html`
      <${this.tag} ${this.activeAttribute}=${this.active}>
        <p>${this.caption}</p>
      </${this.tag}>`;
  }
}
@customElement('my-anchor')
class MyAnchor extends MyButton {
  tag = literal`a`;
}

static expressionsの値を変更することは高いコストを生じさせます。 その変更はテンプレートの再パースを引き起こし、変更したstatic expressionsの値ごとの結果をメモリーに保存するため、 literalの値を使っているエクスプレッションを高頻度で変更するべきではありません。

上記の例では this.captionもしくはthis.activeが変更された場合は 影響を受けるエクスプレッションを変更するのでテンプレートを効率的に更新することができます。 (テンプレートに存在するエクスプレッションの変更と見なされる) しかし、this.tagもしくはthis.activeAttributeが変更された場合、 それらはliteralが付いているのでテンプレート内で静的な値と見なされます。 だから、テンプレート全体が別の新しいテンプレートに置き換わったと見なされます。 それでDOM全体が再レンダリングされるので、この更新は非効率です。 それに加えて、エクスプレッションに渡されるliteralの値を変更することはメモリーの使用量を増加させます。 再レンダリングのパフォーマンスを改善するためにテンプレートの構造ごとにそれをメモリーにキャッシュします。

これらの理由により、 なるべくliteralを使わないことを推奨します。 そして、リアクティブプロパティは値が変更されることを前提としているのでリアクティブプロパティにliteralが付いた値を保存しないことを推奨します。

テンプレートの構造

literalの値が埋め込まれた後のテンプレートは普通のLitテンプレートと同じようにWell-formed_HTMLである必要があります。 そうしないと、テンプレート内の動的なエクスプレッションが適切に機能しない可能性があります。 詳しくはWell-formed HTMLを見てください。

Non-literal statics

稀に、動的な値を静的なHTMLとしてテンプレートに埋め込みたい場合があります。その場合はliteralを使うことができません。 代わりにunsafeStatic()ディレクティブを使います。

import {html, unsafeStatic} from 'lit/static-html.js';

信頼できるコンテンツのみunsafeStatic()に渡します。 unsafeStatic()unsafeという単語に注目してください。 サニタイズなしに直接HTMLとして解釈されるので unsafeStatic()に渡される文字列に信頼できない文字列を渡してはいけません。 信頼できない文字列の例はクエリーパラメータやユーザの入力から得た文字列です。 このディレクティブでレンダリングされた信頼できない文字列によってクロスサイトスクリプティング(XSS)が引き起こされる可能性があります。

@customElement('my-button')
class MyButton extends LitElement {
  @property() caption = 'Hello static';
  @property({type: Boolean}) active = false;

  render() {
    // これらは信頼できる文字列でなければならない、そうでなければXSSの脆弱性があります。
    const tag = getTagName();
    const activeAttribute = getActiveAttribute();
    return html`
      <${unsafeStatic(tag)} ${unsafeStatic(activeAttribute)}=${this.active}>
        <p>${this.caption}</p>
      </${unsafeStatic(tag)}>`;
  }
}

unsafeStaticを使う際はliteralと同じ注意事項があります。 値の変更はテンプレートのパースとメモリーへのキャッシュを引き起こすので、頻繁に変更するべきではありません。


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.