how-to-create-html-web-components-with-dart について
Dartとjs_interopを使ったHTML Web Componentsの作成方法を解説する、Dart開発者向けのチュートリアルです。複数のJavaScriptフレームワークで使えるカスタム要素の定義方法を扱い、Flutter webの代替を検討する開発者にも適しています。
主な機能
- 01DartでカスタムHTML要素を作成
- 02js_interopとwebパッケージの利用
- 03ライフサイクルコールバックと属性の登録
- 04複数のJavaScriptフレームワークでコンポーネントを共有
活用シーン
- 01Dartで再利用可能なカスタム要素を構築
- 02サーバー側レンダリングHTMLにインタラクションを追加
- 03Flutter webの代替案を検討
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rodydavis/ skills --skill how-to-create-html-web-components-with-dartインストール後、Claude Code で/how-to-create-html-web-components-with-dartを入力して読み込まれたか確認します。
詳細
Dart で HTML Web Components を作成する
私は長年 Web Components のファンです(DevRel として lit.dev や Material Web Components を支援して以来)。また、Flutter アプリケーションやフルスタックアプリで Dart を書くことも大好きです。
多くの企業で使われているにもかかわらず、Web Components は、ウェブ向けにフレームワークを使う JavaScript 開発者から多くの反発を受けてきました。☹️
ご存じないかもしれませんが、ウェブには新しい HTML タグを作成する方法があり、HTML を返す あらゆる JS フレームワークや場所で使えます。また、アプリケーションを段階的に拡張できます。🤩
カスタム HTML タグなので、実装を入れ替えても使用箇所を更新する必要はありません。また、コンポーネントをひとつの大きなバンドルではなく、個別のファイルとして配布できます。こちらをご覧ください。
Dart はかつて Web Components をサポートしており、Polymer という製品では Lit の前身となるものにも使われていました。
JavaScript で Web Component を作成する
JavaScript で Web Component を作成するには、HTML 要素を継承し、コンポーネントがマウントされたときのコールバックを用意するだけです。
class HelloWorld extends HTMLElement {
static observedAttributes = ["name"];
constructor() {
super();
}
update() {
this.innerHTML = `Hello: ${this.getAttribute('name')}`;
}
connectedCallback() {
console.log("Custom element added to page.");
this. update();
}
disconnectedCallback() {
console.log("Custom element removed from page.");
}
adoptedCallback() {
console.log("Custom element moved to new page.");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} has changed.`);
if (name === 'name') {
this. update();
}
}
}
customElements.define("hello-world", HelloWorld);
次のように HTML で使用できます。
<html>
<body>
<hello-world name="Rody"></hello-world>
<script src="./index.js"></script>
</body>
</html>
これは非常にうまく機能し、作成にビルド手順すら必要ありません。
Dart で Web Components を作成する
Dart 側で作成するには、js_interop パッケージと新しい web パッケージを使う必要があります。
通常の方法ではクラスを作成できないため(JS と Dart ではクラスが異なります)、Dart 側でこれらの JS クラスを作成できるファクトリーを用意する必要があります。
便利な API Reflect.construct() を使うと、通常の関数をクラスコンストラクターとして呼び出せます。JavaScript は常にネイティブクラスをサポートしていたわけではなく、ES6 で初めて追加されました。
この組み込み API を使えば、純粋な Dart だけでクラスを作成できます。
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:web/web.dart';
class WebComponent<T extends HTMLElement> {
late T element;
final String extendsType = 'HTMLElement';
void connectedCallback() {}
void disconnectedCallback() {}
void adoptedCallback() {}
void attributeChangedCallback(
String name,
String? oldValue,
String? newValue,
) {}
Iterable<String> get observedAttributes => [];
bool get formAssociated => false;
ElementInternals? get internals => element['_internals'] as ElementInternals?;
set internals(ElementInternals? value) {
element['_internals'] = value;
}
R getRoot<R extends JSObject>() {
final hasShadow = element.shadowRoot != null;
return (hasShadow ? element.shadowRoot! : element) as R;
}
static void define(String tag, WebComponent Function() create) {
final obj = _factory(create);
window.customElements.define(tag, obj);
}
}
@JS('Reflect.construct')
external JSAny _reflectConstruct(
JSObject target,
JSAny args,
JSFunction constructor,
);
final _instances = <HTMLElement, WebComponent>{};
JSFunction _factory(WebComponent Function() create) {
final base = create();
final elemProto = globalContext[base.extendsType] as JSObject;
late JSAny obj;
JSAny constructor() {
final args = <String>[].jsify()!;
final self = _reflectConstruct(elemProto, args, obj as JSFunction);
final el = self as HTMLElement;
_instances.putIfAbsent(el, () => create()..element = el);
return self;
}
obj = constructor.toJS;
obj = obj as JSObject;
final observedAttributes = base.observedAttributes;
final formAssociated = base.formAssociated;
obj['prototype'] = elemProto['prototype'];
obj['observedAttributes'] = observedAttributes.toList().jsify()!;
obj['formAssociated'] = formAssociated.jsify()!;
final prototype = obj['prototype'] as JSObject;
prototype['connectedCallback'] = (HTMLElement instance) {
_instances[instance]?.connectedCallback();
}.toJSCaptureThis;
prototype['disconnectedCallback'] = (HTMLElement instance) {
_instances[instance]?.disconnectedCallback();
_instances.remove(instance);
}.toJSCaptureThis;
prototype['adoptedCallback'] = (HTMLElement instance) {
_instances[instance]?.adoptedCallback();
}.toJSCaptureThis;
prototype['attributeChangedCallback'] = (
HTMLElement instance,
String name,
String? oldName,
String? newName,
) {
_instances[instance]?.attributeChangedCallback(name, oldName, newName);
}.toJSCaptureThis;
return obj as JSFunction;
}
理解するのに少し時間がかかるかもしれませんが、大丈夫です。関数をクラスに変換する JS の仕組みを使い、Web Components を作成するための適切なコールバックを設定しているだけです。
これを代わりに行うパッケージをお探しなら、pub.dev に html_web_components があります。
先ほどと同じように Web Component を作成するには、クラスを継承してコンポーネントを定義するだけです。
import 'package:html_web_components/html_web_components.dart';
class HelloWorld extends WebComponent {
@override
List<String> observedAttributes = ['name'];
void update() {
element.innerText = "Hello: ${element.getAttribute('name')}!";
}
@override
void connectedCallback() {
super.connectedCallback();
update();
}
@override
void attributeChangedCallback(
String name,
String? oldValue,
String? newValue,
) {
super.attributeChangedCallback(name, oldValue, newValue);
if (observedAttributes.contains(name)) {
update();
}
}
}
void main() {
WebComponent.define('hello-world', HelloWorld.new);
}
これは非常によく似ているはずです(それが目標です)。コンポーネントの公開や、それを使ったウェブアプリケーション全体の構築がとても簡単になります。
まとめ
Web Components を使うと、クライアント側のインタラクティブ性を高めながら、サーバーレンダリングでテンプレートファイルを作成したり、フロントエンドで SPA のみを使ったりする自由も得られます。これらのコンポーネントは、あらゆる JS フレームワークで使用できます!🤯
Web Components を使わないと決めつける前に、ぜひご自身で試してみることを強くおすすめします。これは特に、Flutter Web の代替を求めている Flutter 開発者に当てはまります(Jaspr と一緒に使うこともできます)。
Dart の優れたパッケージエコシステムと、WASM および JS にコンパイルできる機能を活用できます。peanut のようなビルダーを使えば、WASM の読み込みを試み、読み込めない場合に JS にフォールバックするスクリプトまで作成してくれます🔥
コードをご覧になりたい場合は、GitHub で確認できます。ご質問がある方や、これらを使って作った素敵なものを紹介したい方は、ぜひご連絡ください!