LitElementをつかってみている~

使い勝手のいいコンポーネントをあちらこちらで使えるようにしたくなり、WebComponentsの技術をつかってつくりたいなーどうしようかなーと思っています。 というのも、いまつくっているものたちのJavaScript開発環境は、概ねノンフレームワークもしくはVue.js製なのですが、特定のフレームワークに依存しない形でコンポーネントを定義して共通化して、 じぶんのサイトのいろいろなところで使いたいなという気持ちになっているのでした。

また、コンポーネントを仮に広く一般に公開するときも、WebComponentsの形になっていればどの環境でも基本的には使えそうなので便利そう~と思っています。

そんなこんなを考えているとき、LitElementというものがあることを知りました。

lit-element.polymer-project.org

WebComponentsをつくるには、Shadow Treeにつかいたい要素を放り込んだり、CSSを入れたり、イベントハンドリングは~...。といろいろやらないといけないことがありそうです。 それらを比較的簡単に、つくりやすくしてくれるのがLitElementです。ライブラリがコンパクトな点がよさそうかもと思っています。

とりあえずLitElementの開発環境を スタートガイド に従って準備します。

npm install -g polymer-cli

これをインストールしておけば、ビルド環境を用意しなくても開発ができます。ちゃんとコンポーネントとしてつかうときにはビルド環境はあったほうがいいかもですが、とりあえずパパっと試してみたい~というときには便利。

こんなかんじの雑プロジェクトを用意して、素振り実装をしてみました。

github.com

まずはHello Worldっぽいことをやってみる

基本的に、 LitElement クラスを継承する形でコンポーネントのクラスを定義し、最後に customElements.define でカスタム要素を登録すればよさそうです。 まずは適当にテキストを表示するコンポーネントをつくってみます。

// src/sample-element.js

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

class SampleElement extends LitElement {
  constructor() {
    super();
    this.name = 'default name';
  }

  static get properties() {
    return {
      name: { type: String },
    };
  }

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

customElements.define('sample-element', SampleElement);

カスタム要素の属性は static get properties() が返すオブジェクトで定義できるようです。このカスタム要素は name というString型を受け取ります。 属性値は、ふつうのHTML要素と同じく基本的には文字列しか受け取れませんが、type に指定したコンバータによって LitElement側 で指定した型に変換されます

render() でテンプレートを定義してコンポーネントのDOMを構築していきます。上記の場合は <p> タグで name 属性値を表示するだけの簡単なもの。

では、つくったカスタム要素を使ってみます。これをつかいたいHTMLで読み込めばよいので、以下のようにしてみます。

<!-- sample/index.html -->

<!DOCTYPE html>
<html>
  <head>
    <title>Sample page</title>
    <meta charset="utf-8">
  </head>
  <body>
    <sample-element></sample-element>
    <sample-element name="hoge fuga"></sample-element>
    <script src="../src/sample-element.js" type="module"></script>
  </body>
</html>

ここで、 polymer-cli をつかってサーバを立ち上げておきます。 polymer serve すると、開発サーバが立ち上がります。

私のプロジェクトはこんな感じのディレクトリ構成なので、lit-element-playground 上で polymer serve を実行すると、 http://localhost:8081/sample で上記HTMLにアクセスできます。

lit-element-playground
├── README.md
├── node_modules
├── package.json
├── sample
│   └── index.html
└── src
    └── sample-element.js

表示してみるとこんなかんじ。 name 属性を設定したほうは hoge fuga とプリントされています。かんたん!

f:id:piyorinpa:20191201233232p:plain
はじめてのLitElement

リストコンポーネントをつくってみる

こんどはリストコンポーネントをつくってみます。ここで、 list 属性値の typeArray にしているのがポイント。

// src/list-element.js

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

class SampleElement extends LitElement {
  constructor() {
    super();
    this.list = []; 
  }

  static get properties() {
    return {
      list: { type: Array },
    };
  }

  render(){
    return html`
      <ul>
        ${this.list.map(item => html`<li>${item}</li>`)}
      </ul>
    `;
  }
}

customElements.define('sample-list', SampleElement);

同じようにHTMLを書いて実行してみます。

<!-- sample/index.html -->

<!DOCTYPE html>
<html>
  <head>
    <title>Sample page</title>
    <meta charset="utf-8">
  </head>
  <body>
    <sample-list list='["hoge","fuga","piyo"]'></sample-list>
    <script src="../src/sample-list.js" type="module"></script>
  </body>
</html>

sample-list 要素の属性値 list["hoge","fuga","piyo"] という文字列を指定していますが、先ほど定義した list プロパティの typeArray なので、SampleElement クラスでは配列値として取り扱うことができます。ちなみに、['hoge','fuga','piyo'] のように、シングルクォートで属性値を書くとパースエラーとなってしまうようです。

表示してみるとこんなかんじ。

f:id:piyorinpa:20191201233351p:plain
リスト表示ができた~

属性値の変更を監視することもできそうなので、たとえば list 属性を更新したら自動で表示を更新する、みたいなこともできそう(試していません)。

スタイルを当てつつテキスト入力要素をつくってみる

こんどはテキスト入力要素をつくってみます。指定した正規表現にマッチしなかったらメッセージを表示する機能もつけてみます。 また、そろそろスタイルもつけてみたい~ということで適当なスタイルを当ててみます。

// src/text-element.js

import { LitElement, html, css } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';

class SampleTextbox extends LitElement {
  constructor() {
    super();
    this.regexp = '';
    this.message = '';
    this.messageClasses = {
      hidemsg: false
    }
  }

  static get properties() {
    return {
      regexp: { type: String },
      message: { type: String },
    };
  }

  static get styles() {
    return css`
      input {
        border: 1px solid gainsboro;
        padding: 10px 20px;
        border-radius: 3px;
      }
      .hidemsg {
        display: none;
      }
    `;
  }

  isValid(value) {
    const re = new RegExp(this.regexp);
    return !!value.match(re);
  }

  validate(e) {
    if( !this.regexp ) return;
    this.messageClasses = {
      hidemsg: this.isValid(e.target.value.toString())
    }
    this.requestUpdate();
  }

  render(){
    return html`
      <div>
        <input @input="${this.validate}" type="textbox"></input>
        <span class="${classMap(this.messageClasses)}">${this.message}</span>
      </div>
    `;
  }
}

customElements.define('sample-textbox', SampleTextbox);

スタイルは static get styles() で定義します。デフォルトでShadow TreeにCSS定義が入るので、基本的にはCSSのスコープは要素内で閉じています。つかうときは、クラス名を <input class="hoge"> のように指定してもいいですが、 classMap をつかえば、動的にスタイルを変更できます。ただし、classMap に指定するObjectのプロパティを変更したとき(たとえは this.messageClasses.hidemsg = true のように変更する)はスタイルが反映されず、Object全体を変更したとき(たとえば this.messageClasses = { hidemsg: true } )にスタイルが反映されました。このへんの挙動はまだよく理解できてないかもですが、めちゃめちゃハマってしまった。。。今回はメッセージの表示・非表示切り替えに classMap をつかってみています。

render() で返すテンプレートの @inputoninput の意で、入力内容が変更されたら呼び出されるイベントです。イベント系は基本的に @hoge のように表記すればよいようです。(もちろん addEventListener とかもつかえる)

入力値を捕捉して、指定された正規表現regexp 属性)を満たさない場合はメッセージ ( message 属性)を表示します。メッセージの表示・非表示は @input イベントを捕捉して hidemsg クラスをあてるかどうかで制御していますが、メッセージ表示のクラスの適用を管理している this.messageClasses は変更されても何も起きないので、じぶんで this.requestUpdate(); を呼び出して描画に反映させる必要があります。

<!-- sample/index.html -->

<!DOCTYPE html>
<html>
  <head>
    <title>Sample page</title>
    <meta charset="utf-8">
  </head>
  <body>
    <sample-textbox regexp="^\d+$", message="数字をいれてね"></sample-textbox>
    <script src="../src/sample-textbox.js" type="module"></script>
  </body>
</html>

こんなかんじにつくったカスタム要素をつかってみます。数字にマッチしないときは「数字を入れてね」と表示されるはず。

f:id:piyorinpa:20191201233841g:plain
それっぽくなった

それっぽくなった!また、input text にも(わかりづらいですが)ちゃんとスタイルが当たっています。

まとめ

とりあえずいろいろさわってみたけど、もうちょっとさわってみたい~となっています。ではでは~。