カスタムディレクティブ
ディレクティブとは、テンプレート式がどのようにレンダリングされるかをカスタマイズすることで、Litを拡張できる関数です。ディレクティブは、ステートフルにでき、DOMにアクセスでき、テンプレートが接続解除および再接続されたときに通知を受け取ることができ、レンダリング呼び出しの外部で式を個別に更新できるため、便利で強力です。
テンプレートでディレクティブを使用するには、テンプレート式で関数を呼び出すだけで簡単です。
html`<div>
${fancyDirective('some text')}
</div>`
Litには、組み込みディレクティブとして、repeat()
やcache()
などが付属しています。ユーザーは独自のカスタムディレクティブを作成することもできます。
ディレクティブには2種類あります。
- 単純な関数
- クラスベースのディレクティブ
単純な関数は、レンダリングする値を返します。任意の数の引数を受け取ることができ、引数なしでもかまいません。
export noVowels = (str) => str.replaceAll(/[aeiou]/ig,'x');
クラスベースのディレクティブを使用すると、単純な関数ではできないことを実行できます。クラスベースのディレクティブを使用して、以下のことを実行できます。
- レンダリングされたDOMに直接アクセスする(たとえば、レンダリングされたDOMノードの追加、削除、並べ替え)。
- レンダリング間で状態を保持する。
- レンダリング呼び出しの外部で、DOMを非同期的に更新する。
- ディレクティブがDOMから切断されたときにリソースをクリーンアップする
このページの残りの部分では、クラスベースのディレクティブについて説明します。
クラスベースのディレクティブの作成
「クラスベースのディレクティブの作成」へのパーマリンククラスベースのディレクティブを作成するには
Directive
クラスを拡張するクラスとしてディレクティブを実装します。directive()
ファクトリにクラスを渡し、Litテンプレート式で使用できるディレクティブ関数を作成します。
import {Directive, directive} from 'lit/directive.js';
// Define directive
class HelloDirective extends Directive {
render() {
return `Hello!`;
}
}
// Create the directive function
const hello = directive(HelloDirective);
// Use directive
const template = html`<div>${hello()}</div>`;
このテンプレートが評価されると、ディレクティブ関数(hello()
)はDirectiveResult
オブジェクトを返し、Litにディレクティブクラス(HelloDirective
)のインスタンスを作成または更新するように指示します。次に、Litはディレクティブインスタンスのメソッドを呼び出して、更新ロジックを実行します。
一部のディレクティブは、通常の更新サイクルの外部でDOMを非同期的に更新する必要があります。非同期ディレクティブを作成するには、Directive
ではなくAsyncDirective
基本クラスを拡張します。詳細については、非同期ディレクティブを参照してください。
クラスベースのディレクティブのライフサイクル
「クラスベースのディレクティブのライフサイクル」へのパーマリンクディレクティブクラスには、いくつかの組み込みライフサイクルメソッドがあります。
- 1回限りの初期化のためのクラスコンストラクター。
- 宣言的レンダリングのための
render()
。 - 命令的なDOMアクセスのための
update()
。
すべてのディレクティブに対してrender()
コールバックを実装する必要があります。update()
の実装はオプションです。update()
のデフォルトの実装では、render()
から値を呼び出して返します。
通常の更新サイクルの外部でDOMを更新できる非同期ディレクティブは、追加のライフサイクルコールバックを使用します。詳細については、非同期ディレクティブを参照してください。
初回設定:constructor()
「初回設定:constructor()」へのパーマリンクLitが式で最初にDirectiveResult
を検出すると、対応するディレクティブクラスのインスタンスを構築します(ディレクティブのコンストラクターとすべてのクラスフィールド初期化子が実行されます)。
class MyDirective extends Directive {
// Class fields will be initialized once and can be used to persist
// state between renders
value = 0;
// Constructor is only run the first time a given directive is used
// in an expression
constructor(partInfo: PartInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
class MyDirective extends Directive {
// Class fields will be initialized once and can be used to persist
// state between renders
value = 0;
// Constructor is only run the first time a given directive is used
// in an expression
constructor(partInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
同じディレクティブ関数が各レンダリングで同じ式で使用されている限り、前のインスタンスが再利用されるため、インスタンスの状態はレンダリング間で保持されます。
コンストラクターは、ディレクティブが使用された式に関するメタデータを提供する単一のPartInfo
オブジェクトを受け取ります。これは、ディレクティブが特定の種類の式でのみ使用されるように設計されている場合に、エラーチェックを提供するのに役立ちます(ディレクティブを1つの式タイプに制限するを参照)。
宣言的レンダリング:render()
「宣言的レンダリング:render()」へのパーマリンクrender()
メソッドは、DOMにレンダリングする値を返す必要があります。別のDirectiveResult
を含め、レンダリング可能な任意の値を返すことができます。
ディレクティブインスタンスの状態を参照することに加えて、render()
メソッドはディレクティブ関数に渡される任意の引数を受け入れることもできます。
const template = html`<div>${myDirective(name, rank)}</div>`
render()
メソッドに定義されたパラメーターによって、ディレクティブ関数の署名が決まります。
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// Define a render method, which may accept arguments:
render(value: number, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// Define a render method, which may accept arguments:
render(value, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
命令的なDOMアクセス:update()
「命令的なDOMアクセス:update()」へのパーマリンクより高度なユースケースでは、ディレクティブで基になるDOMにアクセスし、命令的に読み取りまたは変更する必要がある場合があります。これは、update()
コールバックをオーバーライドすることで実現できます。
update()
コールバックは2つの引数を受け取ります。
- 式に関連付けられたDOMを直接管理するためのAPIを備えた
Part
オブジェクト。 render()
引数を含む配列。
update()
メソッドは、Litがレンダリングできるもの、または再レンダリングが必要ない場合は特別な値noChange
を返す必要があります。update()
コールバックは非常に柔軟ですが、一般的な使用法には以下が含まれます。
- DOMからデータを読み取り、それを使用してレンダリングする値を生成する。
Part
オブジェクトのelement
またはparentNode
参照を使用して、DOMを命令的に更新する。この場合、update()
は通常noChange
を返し、Litがディレクティブをレンダリングするためにさらにアクションを実行する必要がないことを示します。
各式の位置には、独自の特定のPart
オブジェクトがあります。
- HTML子位置の式の場合は
ChildPart
。 - HTML属性値位置の式の場合は
AttributePart
。 - ブール属性値(
?
で始まる名前)の式の場合はBooleanAttributePart
。 - イベントリスナー位置(
@
で始まる名前)の式の場合はEventPart
。 - プロパティ値位置(
.
で始まる名前)の式の場合はPropertyPart
。 - 要素タグの式の場合は
ElementPart
。
PartInfo
に含まれるパーツ固有のメタデータに加えて、すべてのPart
タイプは、式に関連付けられたDOM element
(またはChildPart
の場合はparentNode
)へのアクセスを提供します。これはupdate()
で直接アクセスできます。例:
// 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>`
// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part) {
this.attributeNames = part.parentNode.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-helpers.js
モジュールには、Part
オブジェクトに対して機能し、ディレクティブのChildPart
内でパーツを動的に作成、挿入、移動するために使用できる多数のヘルパー関数が含まれています。
update()からのrender()の呼び出し
「update()からのrender()の呼び出し」へのパーマリンクupdate()
のデフォルトの実装は、単にrender()
から値を呼び出して返します。update()
をオーバーライドし、それでもrender()
を呼び出して値を生成する場合は、render()
を明示的に呼び出す必要があります。
render()
引数は、配列としてupdate()
に渡されます。次のようにrender()
に引数を渡すことができます。
class MyDirective extends Directive {
update(part: Part, [fish, bananas]: DirectiveParameters<this>) {
// ...
return this.render(fish, bananas);
}
render(fish: number, bananas: number) { ... }
}
class MyDirective extends Directive {
update(part, [fish, bananas]) {
// ...
return this.render(fish, bananas);
}
render(fish, bananas) { ... }
}
update()とrender()の違い
「update()とrender()の違い」へのパーマリンクupdate()
コールバックはrender()
コールバックよりも強力ですが、重要な違いが1つあります。サーバー側レンダリング(SSR)に@lit-labs/ssr
パッケージを使用する場合、サーバーで呼び出されるのはrender()
メソッドのみです。SSRとの互換性を保つために、ディレクティブはrender()
から値を返し、DOMへのアクセスが必要なロジックにはupdate()
のみを使用する必要があります。
変更なしの通知
「変更なしの通知」へのパーマリンク場合によっては、ディレクティブにLitがレンダリングする新しいものがない場合があります。この場合は、update()
またはrender()
メソッドからnoChange
を返すことで通知します。これは、undefined
を返すのとは異なり、undefined
を返すと、ディレクティブに関連付けられたPart
がクリアされます。noChange
を返すと、以前にレンダリングされた値がそのまま残ります。
noChange
を返す一般的な理由がいくつかあります。
- 入力値に基づいて、レンダリングする新しいものはありません。
update()
メソッドがDOMを命令的に更新しました。- 非同期ディレクティブでは、レンダリングするものがまだないため、
update()
またはrender()
の呼び出しがnoChange
を返す場合があります。
例えば、ディレクティブは渡された以前の値を追跡し、ディレクティブの出力の更新が必要かどうかを判断するために独自のダーティチェックを実行できます。update()
または render()
メソッドは、ディレクティブの出力の再レンダリングが不要であることを示すために noChange
を返すことができます。
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
a?: string;
b?: string;
render(a: string, b: string) {
if (this.a !== a || this.b !== b) {
this.a = a;
this.b = b;
// Expensive & fancy text diffing algorithm
return calculateDiff(a, b);
}
return noChange;
}
}
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
render(a, b) {
if (this.a !== a || this.b !== b) {
this.a = a;
this.b = b;
// Expensive & fancy text diffing algorithm
return calculateDiff(a, b);
}
return noChange;
}
}
ディレクティブを1つの式タイプに制限する
「ディレクティブを1つの式タイプに制限する」へのパーマリンクディレクティブの中には、属性式や子式など、特定のコンテキストでのみ役立つものがあります。間違ったコンテキストに配置された場合、ディレクティブは適切なエラーをスローする必要があります。
例えば、classMap
ディレクティブは、AttributePart
内でのみ、かつ class
属性に対してのみ使用されることを検証します。
class ClassMap extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'class'
) {
throw new Error('The `classMap` directive must be used in the `class` attribute');
}
}
...
}
class ClassMap extends Directive {
constructor(partInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'class'
) {
throw new Error('The `classMap` directive must be used in the `class` attribute');
}
}
...
}
非同期ディレクティブ
「非同期ディレクティブ」へのパーマリンク前の例のディレクティブは同期的です。つまり、render()
/update()
ライフサイクルコールバックから同期的に値を返すため、その結果はコンポーネントの update()
コールバック中に DOM に書き込まれます。
場合によっては、ネットワークリクエストのような非同期イベントに依存する場合など、ディレクティブが DOM を非同期的に更新できるようにしたい場合があります。
ディレクティブの結果を非同期的に更新するには、ディレクティブは AsyncDirective
基底クラスを拡張する必要があります。このクラスは、setValue()
API を提供します。setValue()
を使用すると、ディレクティブはテンプレートの通常の update
/render
サイクル外で、新しい値をテンプレート式に「プッシュ」できます。
以下に、Promise の値をレンダリングする簡単な非同期ディレクティブの例を示します。
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);
class ResolvePromise extends AsyncDirective {
render(promise) {
Promise.resolve(promise).then((resolvedValue) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
ここでは、レンダリングされたテンプレートは、「Promise の解決を待っています」と表示され、その後に Promise が解決されるたびに、Promise の解決された値が表示されます。
非同期ディレクティブは、外部リソースをサブスクライブする必要があることがよくあります。メモリリークを防ぐため、非同期ディレクティブは、ディレクティブインスタンスが使用されなくなったときに、リソースのサブスクライブを解除または破棄する必要があります。この目的のために、AsyncDirective
は次の追加のライフサイクルコールバックと API を提供します。
disconnected()
: ディレクティブが使用されなくなったときに呼び出されます。ディレクティブインスタンスは、次の3つのケースで切断されます。- ディレクティブが含まれている DOM ツリーが DOM から削除された場合
- ディレクティブのホスト要素が切断された場合
- ディレクティブを生成した式が、同じディレクティブに解決されなくなった場合。
ディレクティブが
disconnected
コールバックを受け取った後、メモリリークを防ぐために、update
またはrender
中にサブスクライブした可能性のあるすべてのリソースを解放する必要があります。reconnected()
: 以前に切断されたディレクティブが再利用されるときに呼び出されます。DOM サブツリーは一時的に切断され、後で再接続される可能性があるため、切断されたディレクティブは再接続に反応する必要がある場合があります。この例としては、DOM が削除され、後で使用するためにキャッシュされる場合や、ホスト要素が移動し、切断と再接続が発生する場合などがあります。切断されたディレクティブを動作状態に戻すために、reconnected()
コールバックは常にdisconnected()
とともに実装する必要があります。isConnected
: ディレクティブの現在の接続状態を反映します。
包含するツリーが再レンダリングされた場合、AsyncDirective
が切断されている間も更新を受信し続ける可能性があることに注意してください。このため、メモリリークを防ぐために、update
および/または render
は、長期にわたるリソースをサブスクライブする前に、常に this.isConnected
フラグを確認する必要があります。
以下は、Observable
をサブスクライブし、切断と再接続を適切に処理するディレクティブの例です。
class ObserveDirective extends AsyncDirective {
observable: Observable<unknown> | undefined;
unsubscribe: (() => void) | undefined;
// When the observable changes, unsubscribe to the old one and
// subscribe to the new one
render(observable: Observable<unknown>) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// Subscribes to the observable, calling the directive's asynchronous
// setValue API each time the value changes
subscribe(observable: Observable<unknown>) {
this.unsubscribe = observable.subscribe((v: unknown) => {
this.setValue(v);
});
}
// When the directive is disconnected from the DOM, unsubscribe to ensure
// the directive instance can be garbage collected
disconnected() {
this.unsubscribe!();
}
// If the subtree the directive is in was disconnected and subsequently
// re-connected, re-subscribe to make the directive operable again
reconnected() {
this.subscribe(this.observable!);
}
}
export const observe = directive(ObserveDirective);
class ObserveDirective extends AsyncDirective {
// When the observable changes, unsubscribe to the old one and
// subscribe to the new one
render(observable) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// Subscribes to the observable, calling the directive's asynchronous
// setValue API each time the value changes
subscribe(observable) {
this.unsubscribe = observable.subscribe((v) => {
this.setValue(v);
});
}
// When the directive is disconnected from the DOM, unsubscribe to ensure
// the directive instance can be garbage collected
disconnected() {
this.unsubscribe();
}
// If the subtree the directive is in was disconneted and subsequently
// re-connected, re-subscribe to make the directive operable again
reconnected() {
this.subscribe(this.observable);
}
}
export const observe = directive(ObserveDirective);