Mixin
クラスmixinは、標準のJavaScriptを使用してクラス間でコードを共有するためのパターンです。「has-a」の構成パターン(リアクティブコントローラーなど)とは対照的に、クラスが動作を追加するためにコントローラーを所有できる場合、mixinは「is-a」の構成を実装します。mixinによって、クラス自体が共有される動作のインスタンスとしてなるようにします。
mixinを使用して、APIを追加したり、ライフサイクルコールバックをオーバーライドしたりすることで、Litコンポーネントをカスタマイズできます。
Mixinの基本
「Mixinの基本」へのパーマリンクmixinは、適用先のクラスをオーバーライドし、mixinの動作で拡張されたサブクラスを返す「サブクラスファクトリー」と考えることができます。mixinは標準のJavaScriptクラス式を使用して実装されているため、新しいフィールド/メソッドの追加、既存のスーパークラスメソッドのオーバーライド、superの使用など、サブクラス化で使用できるすべてのイディオムを使用できます。
読みやすくするために、このページのサンプルでは、mixin関数のTypeScript型の一部を省略しています。TypeScriptでのmixinの適切な型付けの詳細については、「TypeScriptでのmixin」を参照してください。
mixinを定義するには、superClassを受け取り、それを拡張する新しいクラスを返す関数を記述し、必要に応じてフィールドとメソッドを追加します。
const MyMixin = (superClass) => class extends superClass { /* class fields & methods to extend superClass with */};mixinを適用するには、mixinが適用されたサブクラスを生成するために、クラスを渡すだけです。ほとんどの場合、ユーザーは新しいクラスを定義するときに、mixinを基本クラスに直接適用します。
class MyElement extends MyMixin(LitElement) { /* user code */}mixinを使用して、ユーザーが通常のクラスのように拡張できる具象サブクラスを作成することもできます。この場合、mixinは実装の詳細です。
export const LitElementWithMixin = MyMixin(LitElement);import {LitElementWithMixin} from './lit-element-with-mixin.js';
class MyElement extends LitElementWithMixin { /* user code */}クラスmixinはLit固有ではなく、標準のJavaScriptパターンであるため、コードの再利用のためにmixinを活用する方法に関する多くの情報がコミュニティに存在します。mixinに関する詳細については、次の参考文献を参照してください。
- MDNのクラスmixin
- Justin FagnaniによるJavaScriptクラスを使用した実際のmixin
- TypeScriptハンドブックのMixin。
- open-wcによるDedupe mixinライブラリ。mixinの使用が重複につながる可能性がある場合や、重複排除ライブラリを使用して重複を回避する方法に関する説明が含まれています。
- Elix Webコンポーネントライブラリが従うMixinの規則。Lit固有ではありませんが、Webコンポーネントのmixinを定義する際に規則を適用するための思慮深い提案が含まれています。
LitElement用のmixinの作成
「LitElement用のmixinの作成」へのパーマリンクLitElementに適用されるmixinは、constructor()やconnectedCallback()などの標準のカスタム要素ライフサイクルコールバック、およびrender()やupdated()などのリアクティブ更新ライフサイクルコールバックを実装またはオーバーライドできます。
たとえば、次のmixinは、要素が作成、接続、および更新されたときにログを記録します。
const LoggingMixin = (superClass) => class extends superClass { constructor() { super(); console.log(`${this.localName} was created`); } connectedCallback() { super.connectedCallback(); console.log(`${this.localName} was connected`); } updated(changedProperties) { super.updated?.(changedProperties); console.log(`${this.localName} was updated`); }}mixinは、常にLitElementによって実装される標準のカスタム要素ライフサイクルメソッドへのスーパーコールを行う必要があることに注意してください。リアクティブ更新ライフサイクルコールバックをオーバーライドする場合は、スーパークラスに既に存在する場合は、スーパーメソッドを呼び出すことをお勧めします(上記のオプションチェーン呼び出しsuper.updated?.()のように)。
また、mixinは、スーパーコールを行うタイミングの選択を通じて、標準のライフサイクルコールバックの基本実装の前または後のいずれかで作業を行うことを選択できることにも注意してください。
mixinは、サブクラス化された要素にリアクティブプロパティ、スタイル、およびAPIを追加することもできます。
次の例のmixinは、要素にhighlightリアクティブプロパティと、ユーザーがコンテンツをラップするために呼び出すことができるrenderHighlight()メソッドを追加します。highlightプロパティ/属性が設定されている場合、ラップされたコンテンツは黄色でスタイル設定されます。
上記の例では、mixinのユーザーは、render()メソッドからrenderHighlight()メソッドを呼び出すこと、およびmixinによって定義されたstatic stylesをサブクラスのスタイルに追加することに注意する必要があります。mixinとユーザー間のこのコントラクトの性質は、mixinの定義次第であり、mixinの作成者がドキュメント化する必要があります。
TypeScriptでのMixin
「TypeScriptでのMixin」へのパーマリンクTypeScriptでLitElement mixinを記述する場合は、注意すべき点がいくつかあります。
スーパークラスの型付け
「スーパークラスの型付け」へのパーマリンクsuperClass引数を、ユーザーが拡張することを期待するクラスの型(存在する場合)に制約する必要があります。これは、以下に示すようにジェネリックConstructorヘルパー型を使用して実現できます。
import {LitElement} from 'lit';
type Constructor<T = {}> = new (...args: any[]) => T;
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => { class MyMixinClass extends superClass { /* ... */ }; return MyMixinClass as /* see "typing the subclass" below */;}上記の例では、mixinに渡されるクラスがLitElementから拡張されていることを保証しているため、mixinはLitが提供するコールバックやその他のAPIに依存できます。
サブクラスの型付け
「サブクラスの型付け」へのパーマリンクTypeScriptには、mixinパターンを使用して生成されたサブクラスの戻り値を推論するための基本的なサポートがありますが、推論されたクラスにprivateまたはprotectedアクセス修飾子を持つメンバーを含めることはできないという深刻な制限があります。
LitElement自体にプライベートメンバーとプロテクトメンバーがあるため、デフォルトでは、LitElementを拡張するクラスを返す場合、TypeScriptは「エクスポートされたクラス式のプロパティ'...'は、プライベートまたはプロテクトにすることはできません。」というエラーを返します。
上記のエラーを回避するために、mixin関数からの戻り値をキャストするという2つの回避策があります。
mixinが新しいパブリック/プロテクトAPIを追加しない場合
「mixinが新しいパブリック/プロテクトAPIを追加しない場合」へのパーマリンクmixinがLitElementのメソッドまたはプロパティのみをオーバーライドし、独自の新しいAPIを追加しない場合は、生成されたクラスを渡されたスーパークラスの型Tにキャストするだけで済みます。
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => { class MyMixinClass extends superClass { connectedCallback() { super.connectedCallback(); this.doSomethingPrivate(); } private doSomethingPrivate() { /* does not need to be part of the interface */ } }; // Cast return type to the superClass type passed in return MyMixinClass as T;}mixinが新しいパブリック/プロテクトAPIを追加する場合
「mixinが新しいパブリック/プロテクトAPIを追加する場合」へのパーマリンクmixinが、クラスで使用できるようにする必要のある新しいプロテクトまたはパブリックAPIを追加する場合は、実装とは別にmixinのインターフェイスを定義し、mixinインターフェイスとスーパークラス型の交差として戻り値をキャストする必要があります。
// Define the interface for the mixinexport declare class MyMixinInterface { highlight: boolean; protected renderHighlight(): unknown;}
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => { class MyMixinClass extends superClass { @property() highlight = false; protected renderHighlight() { /* ... */ } }; // Cast return type to your mixin's interface intersected with the superClass type return MyMixinClass as Constructor<MyMixinInterface> & T;}mixinでのデコレータの適用
「mixinでのデコレータの適用」へのパーマリンクTypeScriptの型システムの制限により、デコレータ(@property()など)は、クラス式ではなくクラス宣言ステートメントに適用する必要があります。
実際には、これはTypeScriptのmixinが、クラスを宣言してから返す必要があり、アロー関数からクラス式を直接返す必要がないことを意味します。
サポートされている
export const MyMixin = <T extends LitElementConstructor>(superClass: T) => { // ✅ Defining a class in a function body, and then returning it class MyMixinClass extends superClass { @property() mode = 'on'; /* ... */ }; return MyMixinClass;}サポートされていない
export const MyMixin = <T extends LitElementConstructor>(superClass: T) => // ❌ Returning class expression directly using arrow-function shorthand class extends superClass { @property() mode = 'on'; /* ... */ }