テンプレート

テンプレートの定義

テンプレートを定義するにはクラス内でrender関数を実装してください:

class MyElement extends LitElement {
  render() {
    return html`<p>テンプレートの中身</p>`;
  }
}

例えば

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {

  // テンプレートとして`render`関数を実装する
  render(){
    /** 
     * lit-html `TemplateResult`を返す
     * 
     * `TemplateResult`を作るには、テンプレートリテラルに
     * `html`ヘルパー関数でタグをつける
     */
    return html`
      <div>
        <p>A paragraph</p>
      </div>
    `;
  }
}
customElements.define('my-element', MyElement);

効率的なテンプレートの設計

LitElementはプロパティ変更を一括処理して非同期で描画、再描画します(詳しくはエレメントのアップデートライフサイクルを参照)。

その更新では、変更されたDOMの特定部分のみが再描画されます。このモデルによるパフォーマンスの恩恵を得るには、テンプレートはそのプロパティに対して純粋な関数となるように設計する必要があります。

そのためにrender関数は:

また、 renderの外でDOMを更新しないようにしてください。代わりに、テンプレートを状態関数として作り、その状態(ステート)はプロパティから取得してください。

下記のコードは、非効率的なDOM操作となります:

dom-manip.js

// アンチパターン。こう書いてはいけません!

constructor() {
  super();
  this.addEventListener('stuff-loaded', (e) => {
    this.shadowRoot.getElementById('message').innerHTML=e.detail;
  });
  this.loadStuff();
}
render() {
  return html`
    <p id="message">読み込み中</p>
  `;
}

推奨パターンは「読み込み中」のメッセージをプロパティにして、イベント処理時にそのプロパティを更新します:

update-properties.js

constructor() {
  super();
  this.message = '読み込み中';
  this.addEventListener('stuff-loaded', (e) => { this.message = e.detail } );
  this.loadStuff();
}
render() {
  return html`
    <p>${this.message}</p>
  `;
}

プロパティ、繰り返し、条件分岐をテンプレートで使う

プロパティ

プロパティをテンプレートに追加するには、$ {this.propName}を使います:

static get properties() {
  return { myProp: String };
}
...
render() { 
  return html`<p>${this.myProp}</p>`; 
}

繰り返し

配列の反復処理:

html`<ul>
  ${this.myArray.map(i => html`<li>${i}</li>`)}
</ul>`;

条件分岐

真偽値(Boolean)の条件分岐結果を描画:

html`
  ${this.myBool?
    html`<p>Render some HTML if myBool is true</p>`:
    html`<p>Render some other HTML if myBool is false</p>`}
`;

サンプル

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      myString: { type: String },
      myArray: { type: Array },
      myBool: { type: Boolean }
    };
  }
  constructor() {
    super();
    this.myString = 'Hello World';
    this.myArray = ['an','array','of','test','data'];
    this.myBool = true;
  }
  render() {
    return html`
      <p>${this.myString}</p>
      <ul>
        ${this.myArray.map(i => html`<li>${i}</li>`)}
      </ul>
      ${this.myBool?
        html`<p>myBoolが真の時に描画されるHTML</p>`:
        html`<p>myBoolが偽の時に描画されるHTML</p>`}
    `;
  }
}

customElements.define('my-element', MyElement);

子要素へのプロパティ設定

HTMLテキストコンテンツ、属性、真偽属性、プロパティ、およびイベントハンドラのバインドにJavaScript式を使えます。

JavaScript式を要素のプロパティとして使うことができます。LitElementはプロパティの変更を監視するので、テンプレートは自動的に更新されます。

データバインディングは常に一方向(親から子へ)です。子要素から親要素にデータを送るには、イベントを使います。

テキストコンテンツへのバインド

prop1をテキストコンテンツとしてバインドするには:

html`<div>${this.prop1}</div>`

属性へのバインド

prop2を属性としてバインドするには:

html`<div id="${this.prop2}"></div>`

属性値は常に文字列なので、属性バインディングは文字列に変換できる値を返さなければなりません。

真偽値属性へのバインド

prop2を真偽値属性としてバインドするには:

html`<input type="checkbox" ?checked="${this.prop3}">i like pie</input>`

真偽値属性は、式が真(truthy)と評価される時に追加され、偽(falsy)と評価される時に削除されます。

プロパティへのバインド

prop4をプロパティとしてバインドするには:

html`<input type="checkbox" .value="${this.prop4}"/>`

イベントハンドラへのバインド

clickHandler関数をclickイベントにバインドするには:

html`<button @click="${this.clickHandler}">pie?</button>`

@eventののデフォルトのイベントコンテキストはthisなので、関数をバインドする必要(訳注: constructorで this.clickHandler.bind(this)する)はありません。

サンプル

my-element.js

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      prop1: String,
      prop2: String,
      prop3: Boolean,
      prop4: String
    };
  }
  constructor() {
    super();
    this.prop1 = 'text binding';
    this.prop2 = 'mydiv';
    this.prop3 = true;
    this.prop4 = 'pie';
  }
  render() {
    return html`
      <!-- テキスト -->
      <div>${this.prop1}</div>

      <!-- 属性 -->
      <div id="${this.prop2}">属性バインディング</div>

      <!-- 真偽値属性 -->
      <div>
        真偽値属性バインディング
        <input type="checkbox" ?checked="${this.prop3}"/>
      </div>
      
      <!-- プロパティ -->
      <div>
        プロパティ
        <input type="checkbox" .value="${this.prop4}"/>
      </div>
      
      <!-- イベントハンドラ -->
      <div>イベントハンドラ・バインディング
        <button @click="${this.clickHandler}">click</button>
      </div>
    `;
  }
  clickHandler(e) {
    console.log(e.target);
  }
}

customElements.define('my-element', MyElement);

light DOMの子要素を描画する

Shadow DOM vs light DOM

shadow DOMが導入されてから我々はメインDOMツリーに表示されるノードは”light DOM”という用語で区別しています。

デフォルトでカスタム要素はそのlight DOMの子要素を全く描画しません:

<my-element>
  <p>描画されない</p>
</my-element>

それらを描画するには<slot> elementを使います。

slot要素を使う

エレメントのlight DOMを描画するには、テンプレートの中に<slot>を入れます。

例えば:

render(){
  return html`
    <div>
      <slot></slot>
    </div>
  `;
}

Light DOMの子要素は下記のように描画されるでしょう:

<my-element>
  <p>描画される</p>
</my-element>

複数の子要素でも一つのslotで展開されます:

<my-element>
  <p>描画される</p>
  <p>こちらも</p>
  <p>こっちも</p>
</my-element>

名前付きスロットを使う

light DOMの子要素を特定のスロットに割り当てるには、その子要素のslot属性の値とslotのname属性が一致するようにしてください:

render(){
  return html`
    <div>
      <slot name="one"></slot>
    </div>
  `;
}

index.html

<my-element>
  <p slot="one">slotのoneとして表示</p>
</my-element>

サンプル

my-element.js

<stack-blitz
    folder="/includes/projects/projects/docs/templates/namedslots/my-element.js"
    openFile=""
    label="コードエディタを起動">
</stack-blitz>

index.html

<stack-blitz
    folder="/includes/projects/projects/docs/templates/namedslots/index.html"
    openFile=""
    label="コードエディタを起動">
</stack-blitz>

slotを選択するにはidではなくnameを使ってください

slotid属性を設定してもなんの効果もありません!

my-element.js

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

index.html

<my-element>
  <p slot="one">hoge</p>
  <p>fuga..</p>
</my-element>

他のテンプレートを読み込む

LitElementテンプレートは、他のLitElementテンプレートから作成できます。次の例では、標準のHTML要素 <header><article>、および<footer>のより小さいテンプレートから <my-page>という要素のテンプレートを作成します:

class MyPage extends LitElement {
  render() {
    return html`
      ${this.headerTemplate}
      ${this.articleTemplate}
      ${this.footerTemplate}
    `;
  }
  static get headerTemplate() {
    return html`<header>ヘッダー</header>`;
  }
  static get articleTemplate() {
    return html`<article>内容</article>`;
  }
  static get footerTemplate() {
    return html`<footer>フッター</footer>`;
  }
}

テンプレートを作成するには、他の要素をインポートしてテンプレートとして使用します:

import './my-header.js';
import './my-article.js';
import './my-footer.js';

class MyPage extends LitElement {
  render() {
    return html`
      <my-header></my-header>
      <my-article></my-article>
      <my-footer></my-footer>
    `;
  }
}

レンダールートを指定する

テンプレートが描画されるノード(node)をレンダールートと呼びます。

デフォルトでLitElementはレンダールートにshadowRootを作成し、その下にDOMを構成します。

<my-element>
  #shadow-root
    <p>子要素1</p>
    <p>子要素2</p>

コンポーネントのレンダールートを変更するにはcreateRenderRootを実装し、描画させたいノードを指定してください。

たとえば、テンプレートをメインDOMツリー以下にlight DOMとして描画するには次のようにします:

<my-element>
  <p>子要素1</p>
  <p>子要素2</p>

createRenderRootを実装し、thisを返します:

class LightDom extends LitElement {
  render() {
    return html`
      <p>このテンプレートはlight DOMに描画されます</p>
    `;
  }
  createRenderRoot() {
  /**
   * light DOMに描画。shadow DOMではないので、
   * CSSのカプセル化は行われない。
   */
    return this;
  }
}

テンプレート構文チートシート

描画

render() { return html`<p>テンプレート</p>`; }

プロパティ、繰り返し、条件分岐

// プロパティ
html`<p>${this.myProp}</p>`;

// 繰り返し
html`${this.myArray.map(i => html`<li>${i}</li>`;)}`;

// 条件分岐
html`${this.myBool?html`<p>ほげ</p>`:html`<p>ふが</p>`}`;

データバインディング

// 属性
html`<p id="${...}">`;

// 真偽値属性
html`<input type="checkbox" ?checked="${...}">`;

// プロパティ
html`<input .value="${...}">`;

// イベントハンドラ
html`<button @click="${this.doStuff}"></button>`;

テンプレート生成

// 同じクラスで複数のテンプレートを使う

render() {
  return html`
    ${this.headerTemplate}
    <article>文書</article>
  `;
}
static get headerTemplate() {
  return html`<header>ヘッダ</header>`;
}
// 要素のインポート
import './my-header.js';

class MyPage extends LitElement{
  render() {
    return html`
      <my-header></my-header>
      <article>文書</article>
    `;
  }
}

スロット

render() { return html`<slot name="thing"></slot>`; }
<my-element>
  <p slot="thing">ぴよ</p>
</my-element>