mcpField
H
how-to-create-html-web-components-with-dart

how-to-create-html-web-components-with-dart

github.com/rodydavis開発6043 回インストール2 週間前に更新
リポジトリ

how-to-create-html-web-components-with-dart について

Dartとjs_interopを使ったHTML Web Componentsの作成方法を解説する、Dart開発者向けのチュートリアルです。複数のJavaScriptフレームワークで使えるカスタム要素の定義方法を扱い、Flutter webの代替を検討する開発者にも適しています。

主な機能

  1. 01DartでカスタムHTML要素を作成
  2. 02js_interopとwebパッケージの利用
  3. 03ライフサイクルコールバックと属性の登録
  4. 04複数のJavaScriptフレームワークでコンポーネントを共有

活用シーン

  1. 01Dartで再利用可能なカスタム要素を構築
  2. 02サーバー側レンダリングHTMLにインタラクションを追加
  3. 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 で確認できます。ご質問がある方や、これらを使って作った素敵なものを紹介したい方は、ぜひご連絡ください!

コメント