Ullr というライブラリを書きました。
この記事の API は一部古いです。最新版は GitHub でご確認ください。
最近は個人的に、Web Components を書く機会が多いです。
バニラの Web Componentsは HTMLElement
を継承していくのでオブジェクト指向なプログラミングスタイルをとることになります。
ただ、純粋な Web Components だと、コンポーネントの受け取れる値がすべて文字列になるという制約と、テンプレート中に書いた Web Components と実際に呼び出されるコンポーネントの連続性をコードで表現できないという問題があり、とくに後者はストイックでもなんでもなく単純に非効率です。
そこで、テンプレートとコンポーネントの連続性を表現できる Web Components を目指したところ、すべてを関数で表現する、という試みにたどり着きました。
TL;DR
関数型プログラミングによる Web Components の開発のために Ullr というライブラリを書きました。読み方は ウル です。1
基本的な使い方は次のとおりです。
- lit-html を使ったテンプレート関数を組み合わせてコンポーネントを表現する
-
component
を使ってテンプレートをカプセル化する -
customElements
を使って Custom Elements を定義する -
subscribe
を使って RxJS の Observable を購読しビューを更新する
設計段階ではいろいろ考えたんですが、結果的にはシンプルな API に落ち着きました。
インストール
Ullr は lit-html に依存しているので、lit-html も一緒にインストールします。状態管理はなんでもよいですが、RxJS と合わせて使うことを想定します。
npm i ullr lit-html rxjs
Ullr と lit-html と RxJS を npm プロジェクトにインストールします。
APIs
Ullr はビューを担う API を 2 つ、振る舞いを担う API を 1 つエクスポートしています。TypeScript を推奨しているので、以下のコードはすべて TypeScript です。
component
lit-html の戻り値を Shadow DOM でカプセル化する関数です。Shadow DOM なので、CSS のスコープは気にせずテンプレート内にそのまま Style 要素を書きます。PostCSS とセットで使うと便利です。
component
の戻り値も lit-html の戻り値と同じ型なので、他のテンプレートの一部として使うことができます。
import { component } from 'ullr'
import { html } from 'lit-html'
const template = (title: string) => html`
<style>
h1 {
font-weight: 400;
}
</style>
<h1>${title}</h1>
`
const app = (title: string) => component(template(title))
component
には今のところライフサイクルコールバックとかはありません。
また、Atomic Design のようなコンポーネントの階層構造については、lit-html を返す関数と、その関数のカリー化などで完全に表現できるはずです。なので、Ullr ではテンプレートのカプセル化だけを行う API に留めました。
customElements
Custom Elements を定義するのに必要なクラスを返す関数です。この関数の戻り値を window.customElements.define
に渡すことで Custom Elements を定義します。
第1引数には、lit-html のテンプレートを返す関数をとります。第2引数はオプショナルで、Custom Elements が変更を監視する属性名を配列で渡します。第2引数の値が Custom Elements の observedAttributes
になるイメージです。
第1引数の関数の引数には、第2引数の配列の順番通りに属性値が渡ってきます。属性値が変更されたらその都度、自動的に再レンダリングします。
例えば 3 つの属性を監視して、変更時に再レンダリングする場合は次のような感じです。
const XApp = customElements(([attrA, attrB, attrC]) => app(attrA, attrB, attrC), [
'attr-a',
'attr-b',
'attr-c'
])
先の component
の項で作ったコンポーネントを登録するなら次のような感じです。
import { component, customElements } from 'ullr'
import { html } from 'lit-html'
// ... 中略 ...
const XApp = customElements(([title]) => app(title), ['title'])
window.customElements.define('x-app', XApp)
いつ customElements
を使うか?
Custom Elements にする場合、テンプレートとクラスの連続性は失われます。Custom Elements に値をプロキシするだけのテンプレート関数を挟めば問題ありませんが、それだけの理由であれば component
で十分でしょう。
customElements
を使って Custom Elements にする必要性はその要素を外部に公開するかどうか、で変わってきます。それ以外の場合は component
を使うか、DOM へのアタッチであれば lit-html の render
関数で十分です。
subscribe
RxJS の Observable を購読してテンプレートを更新するための、lit-html の directive
です。lit-html のテンプレート内で使います。次のような感じです。
import { subscribe } from 'ullr/directive'
import { timer as _timer } from 'rxjs'
import { take, filter } from 'rxjs/operators'
const timer = _timer(10, 1).pipe(
filter(x => x > 0),
take(10)
)
const template = html`
<main>
${subscribe(
timer,
x => html`<p>${x}</p>`,
html`<p>Default content</p>`
)}
</main>
`
第1引数に Observable、第2引数に更新時のテンプレートを返す関数、第3引数はオプショナルで初期状態のテンプレートを渡します。
この関数の対象ノードが DOM から取り除かれたときには自動的に unsubscribe
します。対象ノードが DOM から取り除かれたときというのは、このテンプレートを含んだ上位のテンプレートによって何らかの条件で、このテンプレートが使われなくなったときです。
開発スタイル
Ullr を使った場合、ほとんどのコードは lit-html の関数か、その関数のカリー化を処理するものになると思います。僕自身がまだプロダクションで使ってないので不都合が出てくるかもしれません。
関数型プログラミングによる恩恵を Web Components でも受けることができるので、これから積極的に使ってバグを見つけていきます。よろしければご協力ください。
Double O は Web Components だけで作ったサービスですが、Ullr を使ってリファクタリングしたいと思ってます。