カスタムディレクティブ

ディレクティブとは、テンプレート式がどのようにレンダリングされるかをカスタマイズすることで、Litを拡張できる関数です。ディレクティブは、ステートフルにでき、DOMにアクセスでき、テンプレートが接続解除および再接続されたときに通知を受け取ることができ、レンダリング呼び出しの外部で式を個別に更新できるため、便利で強力です。

テンプレートでディレクティブを使用するには、テンプレート式で関数を呼び出すだけで簡単です。

Litには、組み込みディレクティブとして、repeat()cache()などが付属しています。ユーザーは独自のカスタムディレクティブを作成することもできます。

ディレクティブには2種類あります。

  • 単純な関数
  • クラスベースのディレクティブ

単純な関数は、レンダリングする値を返します。任意の数の引数を受け取ることができ、引数なしでもかまいません。

クラスベースのディレクティブを使用すると、単純な関数ではできないことを実行できます。クラスベースのディレクティブを使用して、以下のことを実行できます。

  • レンダリングされたDOMに直接アクセスする(たとえば、レンダリングされたDOMノードの追加、削除、並べ替え)。
  • レンダリング間で状態を保持する。
  • レンダリング呼び出しの外部で、DOMを非同期的に更新する。
  • ディレクティブがDOMから切断されたときにリソースをクリーンアップする

このページの残りの部分では、クラスベースのディレクティブについて説明します。

クラスベースのディレクティブを作成するには

  • Directiveクラスを拡張するクラスとしてディレクティブを実装します。
  • directive()ファクトリにクラスを渡し、Litテンプレート式で使用できるディレクティブ関数を作成します。

このテンプレートが評価されると、ディレクティブ関数hello())はDirectiveResultオブジェクトを返し、LitにディレクティブクラスHelloDirective)のインスタンスを作成または更新するように指示します。次に、Litはディレクティブインスタンスのメソッドを呼び出して、更新ロジックを実行します。

一部のディレクティブは、通常の更新サイクルの外部でDOMを非同期的に更新する必要があります。非同期ディレクティブを作成するには、DirectiveではなくAsyncDirective基本クラスを拡張します。詳細については、非同期ディレクティブを参照してください。

クラスベースのディレクティブのライフサイクル

「クラスベースのディレクティブのライフサイクル」へのパーマリンク

ディレクティブクラスには、いくつかの組み込みライフサイクルメソッドがあります。

  • 1回限りの初期化のためのクラスコンストラクター。
  • 宣言的レンダリングのためのrender()
  • 命令的なDOMアクセスのためのupdate()

すべてのディレクティブに対してrender()コールバックを実装する必要があります。update()の実装はオプションです。update()のデフォルトの実装では、render()から値を呼び出して返します。

通常の更新サイクルの外部でDOMを更新できる非同期ディレクティブは、追加のライフサイクルコールバックを使用します。詳細については、非同期ディレクティブを参照してください。

Litが式で最初にDirectiveResultを検出すると、対応するディレクティブクラスのインスタンスを構築します(ディレクティブのコンストラクターとすべてのクラスフィールド初期化子が実行されます)。

同じディレクティブ関数が各レンダリングで同じ式で使用されている限り、前のインスタンスが再利用されるため、インスタンスの状態はレンダリング間で保持されます。

コンストラクターは、ディレクティブが使用された式に関するメタデータを提供する単一のPartInfoオブジェクトを受け取ります。これは、ディレクティブが特定の種類の式でのみ使用されるように設計されている場合に、エラーチェックを提供するのに役立ちます(ディレクティブを1つの式タイプに制限するを参照)。

render()メソッドは、DOMにレンダリングする値を返す必要があります。別のDirectiveResultを含め、レンダリング可能な任意の値を返すことができます。

ディレクティブインスタンスの状態を参照することに加えて、render()メソッドはディレクティブ関数に渡される任意の引数を受け入れることもできます。

render()メソッドに定義されたパラメーターによって、ディレクティブ関数の署名が決まります。

より高度なユースケースでは、ディレクティブで基になる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()で直接アクセスできます。例:

さらに、directive-helpers.jsモジュールには、Partオブジェクトに対して機能し、ディレクティブのChildPart内でパーツを動的に作成、挿入、移動するために使用できる多数のヘルパー関数が含まれています。

update()のデフォルトの実装は、単にrender()から値を呼び出して返します。update()をオーバーライドし、それでもrender()を呼び出して値を生成する場合は、render()を明示的に呼び出す必要があります。

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 を返すことができます。

ディレクティブの中には、属性式や子式など、特定のコンテキストでのみ役立つものがあります。間違ったコンテキストに配置された場合、ディレクティブは適切なエラーをスローする必要があります。

例えば、classMap ディレクティブは、AttributePart 内でのみ、かつ class 属性に対してのみ使用されることを検証します。

前の例のディレクティブは同期的です。つまり、render()/update() ライフサイクルコールバックから同期的に値を返すため、その結果はコンポーネントの update() コールバック中に DOM に書き込まれます。

場合によっては、ネットワークリクエストのような非同期イベントに依存する場合など、ディレクティブが DOM を非同期的に更新できるようにしたい場合があります。

ディレクティブの結果を非同期的に更新するには、ディレクティブは AsyncDirective 基底クラスを拡張する必要があります。このクラスは、setValue() API を提供します。setValue() を使用すると、ディレクティブはテンプレートの通常の update/render サイクル外で、新しい値をテンプレート式に「プッシュ」できます。

以下に、Promise の値をレンダリングする簡単な非同期ディレクティブの例を示します。

ここでは、レンダリングされたテンプレートは、「Promise の解決を待っています」と表示され、その後に Promise が解決されるたびに、Promise の解決された値が表示されます。

非同期ディレクティブは、外部リソースをサブスクライブする必要があることがよくあります。メモリリークを防ぐため、非同期ディレクティブは、ディレクティブインスタンスが使用されなくなったときに、リソースのサブスクライブを解除または破棄する必要があります。この目的のために、AsyncDirective は次の追加のライフサイクルコールバックと API を提供します。

  • disconnected(): ディレクティブが使用されなくなったときに呼び出されます。ディレクティブインスタンスは、次の3つのケースで切断されます。

    • ディレクティブが含まれている DOM ツリーが DOM から削除された場合
    • ディレクティブのホスト要素が切断された場合
    • ディレクティブを生成した式が、同じディレクティブに解決されなくなった場合。

    ディレクティブが disconnected コールバックを受け取った後、メモリリークを防ぐために、update または render 中にサブスクライブした可能性のあるすべてのリソースを解放する必要があります。

  • reconnected(): 以前に切断されたディレクティブが再利用されるときに呼び出されます。DOM サブツリーは一時的に切断され、後で再接続される可能性があるため、切断されたディレクティブは再接続に反応する必要がある場合があります。この例としては、DOM が削除され、後で使用するためにキャッシュされる場合や、ホスト要素が移動し、切断と再接続が発生する場合などがあります。切断されたディレクティブを動作状態に戻すために、reconnected() コールバックは常に disconnected() とともに実装する必要があります。

  • isConnected: ディレクティブの現在の接続状態を反映します。

包含するツリーが再レンダリングされた場合、AsyncDirective が切断されている間も更新を受信し続ける可能性があることに注意してください。このため、メモリリークを防ぐために、update および/または render は、長期にわたるリソースをサブスクライブする前に、常に this.isConnected フラグを確認する必要があります。

以下は、Observable をサブスクライブし、切断と再接続を適切に処理するディレクティブの例です。