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 mixin
export 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';
/* ... */
}