カスタムディレクティブ
ディレクティブとは、テンプレート式がどのようにレンダリングされるかをカスタマイズすることで、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 directiveclass HelloDirective extends Directive { render() { return `Hello!`; }}// Create the directive functionconst hello = directive(HelloDirective);
// Use directiveconst 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 textContentclass 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 textContentclass 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);