🔥

Lit入門 コンポーネント作成編

2022/08/19に公開

Lit入門 コンポーネント作成編

はじめに

前回の記事の続きです、この記事では下記の前提条件で解説を進めていきます。

  • npmの基本的な利用方法
  • Typescriptの基礎的な知識
  • Classに関しての基礎知識

コンポーネントの構成

まずは、シンプルなコンポーネントからコードを確認していきましょう
https://lit.dev/docs/tools/adding-lit/#add-a-component

LitではコンポーネントをClassとして設計していきます。
本来のWebComponentsもこのようにClassとしてカスタム要素を定義します。

my-element.ts
import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-element')
export default class MyElement extends LitElement {
  static styles = css`
    h1 { color: red; }
  `
  @property()
  name: string = 'world';

  render() {
    return html`
      <h1>hello ${this.name}</h1>
    `;
  }
}

基本的にはClassとしてカスタム要素を定義していきます。
こちらについては通常のWebComponentsと同じですが、異なる点としては下記の2点です。

  • HTMLElementではなくLitElementを継承している点
  • @customElement()デコレーターを使用している点

各項目の解説

では、コードの上から順にどのような役割を担っているのか確認していきます。

カスタム要素の宣言

まずは@customElement()デコレーターを使用してクラスを作成します、継承するクラスはHTMLElementではなくLitElementをインポートして継承します。
@customElement()の引数に要素名を渡し、クラス名には要素名をアッパーキャメルケースにしたものを設定します。

@customElement("my-element")
export default class MyElement extends LitElement {
  //...
}

スタイル定義

次の行ではクラスの静的フィールドとしてカスタム要素に適用されるCSSが記述されています。
カスタム要素はへのスタイルの適用はcss関数を利用してstylesフィールドに追加します。
このstylesフィールドへの追加は関数の返り値であるCSSResultもしくはその配列で行えます。

static styles = css`
  /* この中にCSSを記述することでスタイルを定義できる */
  h1 { color: red;}
`;

また、CSSを外部ファイルとして分割したい場合はunsafeCSS関数を利用します。

CSSを外部ファイル化する場合の手順
  1. raw-loaderAsset Modulesなどを利用してCSSを文字列として読み込める環境を作成する。
  2. 適当な名前でCSSをインポートする。
  3. css関数unsafeCSS関数に変更し引数に 2. でインポートした文字列を渡す。
import { LitElement, unsafeCSS, html } from "lit";
import style from "./style.css";

@customElement("my-element")
export default class MyElement extends LitElement {
  static styles = unsafeCSS(style);
  // ...
}

プロパティの定義

次の項目では@property()デコレーターを使用してプロパティの定義を行っています。
名前の通りプロパティはVue.jsやReactのpropsに近い存在ですので、他のフレームワークを利用したことがある方はそれを頭に入れると分かりやすいかと思います。

デフォルトの動作としては、宣言したプロパティはカスタム要素上で属性値として操作可能で、プロパティの変化を検知してプロパティを使用している項目が自動的にアップデートされるリアクティブな値として利用できるようになります。

MyElement.ts
@customElement('my-element')
export default class MyElement extends LitElement {
  // ...
  @property()
  name: string = 'world';
}

として定義されたプロパティは

<my-element name="hi"></my-element>

のように属性として指定可能になります。
また、通常の要素と同じようにDOMとしても取得・変更が行えます。

const myElement = document.querySelector("my-element");
console.log(myElement.name); // world
myElement.name = "hi";
console.log(myElement.name); // hi

propertyデコレーターのオプション

propertyデコレーターには引数としていくつかのオプションをを設定することができます。
簡単な解説のため使用頻度が高そうなオプションをいくつか解説していきます。

https://lit.dev/docs/api/ReactiveElement/#PropertyDeclaration

attribute

attributeオプションはおそらく一番使用頻度が高いと思われます。
boolean | stringが指定可能で初期値はtrueです。

@property({attribute: 'some-property'})
someProperty: string = "";

初期値attribute: true;の場合somePropertyとして宣言した値はsomepropertyという属性値になってしまいます。
通常HTMLの属性値はケバブケースで表されるためattribute: 'some-property';として属性値の値を明示的に指定する必要があります。

If the value is false, the property is not added to observedAttributes.

公式ドキュメントには上記のように書いていますがfalseにした場合は同名の属性値は監視対象から外れ、HTML側から設定を行っても内部的な更新は行われなくなります。
これに関しては実際に挙動を確認してみると理解が早いと思います。

reflect

reflectオプションは内部的にプロパティの値が変わった場合の属性値の扱いを変更します。
booleanが指定可能で、初期値はfalseです。

@property({reflect: true})
name = "";

単純に設定を行っただけでは効果が分かりづらいですが、reflect: trueを設定した場合は内部的なプロパティの更新に合わせてHTMLの属性値も自動的に更新されるようになります。
このオプションはattributeや後述するconverterが設定に従って属性値の更新を行います。

実例としてはchecked属性のようにユーザーのアクションに応じて属性値が変更が変わる仕様を模倣したい場合に使用する場合が多いと感じます。


attribute 及び reflectの挙動についての図

type

typeオプションはTypeScriptを使用している場合に非常に役立ちます。
通常getAttribute()などで取得された属性値はstrign型として受け取りますが、typeオプションを設定することで自動的に型変換を行ってくれます。
String | Number | Boolean | Array | Objectが指定可能で、初期値はStringです。

Numberを指定した場合はNumber()関数によって変換されます。
Booleanを指定した場合はchecked属性のように指定した属性値が存在すればtrueを存在しなければfalseに変換されます。
ArrayObjectを指定した場合はどちらも同じくJSON.parse()を使用して変換が行われます。

@property({type: Array})
someProps = [];

変換方法をカスタマイズしたい場合はconverterオプションを設定することで変換方法を自分で定義できます。

renderメソッド

最後にrenderメソッドですが、このメソッドの返り値をhtml関数を使用してテンプレートを定義できます。
Vue.jsで言えば<template>タグ、Reactの関数コンポーネントの返り値に近い存在でしょうか。

MyElement.ts
@customElement('my-element')
export default class MyElement extends LitElement {
  // ...
  render() {
    return html`
      <h1>hello ${this.name}</h1>
    `;
  }
}

renderメソッド内部に直接処理を書くこともできるので、下記のようにテンプレートの分割や動的な出し分けなども可能です。

MyElement.ts
@customElement('my-element')
export default class MyElement extends LitElement {
  @state()
  isHeading = false;
  // ...
  render() {
    // 通常のメソッドと同じように内部で変数の定義や使用が可能
    const heading = html`<h1>hello ${this.name}</h1>`;
    const paragraph = html`<p>hello ${this.name}</p>`;

    // プロパティやステートを参照して要素の出し分けなども可能
    return html`
      <div>
        ${this.isHeading ? heading : paragraph}
      </div>
    `;
  }
}

まとめ

ざっくりとした解説になってしまいましたが、最低限コンポーネントを作るための方法を解説させていただきました。
実際に機能を持ったコンポーネントを作成する場合にはイベントの定義や各種デコレーターについての知識が必要になりますが文章量が多くなってしまうためこの記事はここで終了とさせていただきます。
次回(があれば)イベントの定義か紹介しきれなかった各種デコレーターについてなど解説しようと思います。

参考ドキュメント

https://lit.dev/docs/components/properties/
https://lit.dev/docs/api/ReactiveElement/

GitHubで編集を提案

Discussion