Litチームは先頃、Lit 2.0をリリースした。Lit 1のリリースから2年以上を経たリリースだ。Lit 2には非同期ディレクティブなど、カスタムディレクティブ用の新たなAPIがフィーチャーされている。リアクティブコントローラを使用して、再利用可能なリアクティブロジックをカプセル化することも可能だ。
Lit 2は、2019年2月にリリースされた最初のLit 1から、2年以上を経たメジャーアップデートである。bundlephobiaによると、Lit 2.0のサイズは6KBをわずかに下回る。新たな機能や強化点が含まれる一方で、Litチームは、後方互換性がほぼ確保されている点を強調している。
Lit 2にはたくさんの新機能と強化点が追加されていますが、後方互換性は維持されています — ほとんどのケースにおいて、以前のバージョンをそのまま置き換えることが可能なはずです。
Lit 2は、カスタムディレクティブ用の新APIを備える。カスタムディレクティブは、Litのレンダリング方法をAPI経由でコントロール可能なメソッドを公開するオブジェクトである。
// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part: ChildPart) {
this.attributeNames = (part.parentNode as Element).getAttributeNames?.().join(' ');
return this.render();
}
render() {
return this.attributeNames;
}
}
const attributeLogger = directive(AttributeLogger);
const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
上記の例は、Directive
クラスをユーザが定義したディレクティブで拡張して、render
メソッドとupdate
メソッドを使用する方法について説明したものだ。update
メソッドが命令型のDOMアクセスを可能にするのに対して、render
メソッドは宣言型レンダリングに使用することができる。Litはタグ付きテンプレートを利用しているので、update
メソッドには、基盤にあるテンプレートに関連付けられたDOM要素を直接管理するAPIを備えたPart
オブジェクトが渡される。
Lit 2の非同期ディレクティブでは、DOMを非同期に更新することができる。
class ResolvePromise extends AsyncDirective {
render(promise: Promise<unknown>) {
Promise.resolve(promise).then((resolvedValue) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
非同期ディレクティブはライフサイクル管理を必要とする外部リソースとの接続に使用される場合があるので、AsyncDirective
クラスはライフサイクルに関するメソッド(disconnected
、reconnected
など)も公開している。
Lit 2では、"リアクティブコントローラ"と呼ばれる、新たな再利用および構成ストラテジが追加された。Lit 2の資料には、リアクティブコントローラに関する次のような説明がある。
リアクティブコントローラは、コンポーネントのリアクティブ更新サイクル内にフック可能なオブジェクトです。特定の機能に関する状態や振る舞いをバンドルして、複数のコンポーネント間で再利用することができます。
マウスイベントなどグローバルイベントの処理、ネットワークを越えてデータをフェッチするような非同期タスクの管理、アニメーションの実行といった、自身の状態管理を行いながらコンポーネントのライフサイクルにアクセスする機能の実装に使用可能です。
他のソースコンポーネントからターゲットコンポーネントの構築を行うものではないという意味では、リアクティブコントローラは、厳密な意味でのコンポジションではない。既存のコンポーネントをユーザの定義した振る舞いによって拡張するという点からは、プラグインに近い存在だ。さらに、特定のロジックあるいは振る舞いをカプセル化して再利用を容易にするということでは、React Hooksとも同じである。
リアクティブコントローラとホストコンポーネントは双方向APIで接続される — コントローラがコンポーネントのメソッドやフィールドにアクセス可能であるのと同時に、コンポーネントもコントローラのメソッドやフィールドにアクセスすることができる。ホストコンポーネントにマウストラッキング機能を追加するリアクティブコントローラの例を以下に示す。
import {LitElement, html} from 'lit';
import {MouseController} from './mouse-controller.js';
class MyElement extends LitElement {
mouse = new MouseController(this);
render() {
return html`
<h3>The mouse is at:</h3>
<pre>
x: ${this.mouse.pos.x}
y: ${this.mouse.pos.y}
</pre>
`;
}
}
customElements.define('my-element', MyElement);
リアクティブコントローラMouseController
は、コントローラ内に実装されたマウストラッキング機能でmy-element
を拡張するために使用される。ホストコンポーネントは、リアクティブコントローラが公開するpos.x
およびpos.y
APIにアクセスできる。コントローラのコードは下記のようになる。
export class MouseController {
host;
pos = {x: 0, y: 0};
_onMouseMove = ({clientX, clientY}) => {
this.pos = {x: clientX, y: clientY};
this.host.requestUpdate();
};
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener('mousemove', this._onMouseMove);
}
hostDisconnected() {
window.removeEventListener('mousemove', this._onMouseMove);
}
}
上記のコードは、ホストコンポーネントのライフサイクルに結び付けられたメソッド(hostConnected
、hostDisconnected
など)と、リアクティブコントローラがホストコンポーネントのレンダリングを起動するために実行する命令的アップデートthis.host.requestUpdate()
の使用例である。リアクティブコントローラはコンストラクタの引数を通じて、ホストコンポーネントへのリファレンスを保持する。ホストコンポーネントとリアクティブコントローラは相互に参照を保持することで、所定の動作を共同で実現することが可能になる。
Litは自身を、高速で軽量なWebコンポーネントを構築するためのシンプルなライブラリである、と称している。Litの最新資料はオンラインで参照することができる。ドキュメントサイトには、アップグレードガイド、チュートリアル、対話形式のプレイグラウンドが用意されている。
LitはBSD-3条項ライセンスの下で提供されるオープンソースソフトウェアである。コントリビューションを募集しており、その際にはコントリビューションガイドに従う必要がある。