sobria
ページの一覧

外部からの利用

sobria の要素は Web 標準の Custom Element なので、sobria のアプリの外でも使用できます。

タグとして使用する

外部からは、要素をタグとして使用します。タグ名の決まり方は、タグ名 に記述しています。例えば、Vite プラグインの設定を prefix: "acme" にすると、src/elements/counter.tsx の要素のタグは <acme-counter> になります。

次の要素を例にします。

// src/elements/counter.tsx
type Props = {
  count: number;
  onLikeClick?: (count: number) => void;
};

export default function Counter(props: Props) {
  return <button onClick={() => props.onLikeClick?.(props.count + 1)}>{props.count}</button>;
}

要素のプロパティに値を設定すると、要素はその値を props として受け取ります。props をプロパティと属性のどちらで受け取るかは、props の型と受け取り方 に記述しています。

名前が onX の形の関数の props に使用する側が関数を渡していなければ、要素は、その props を呼んだときに CustomEvent を発行します。例えば、onLikeClick のイベント名は like-click で、イベントの detail は props を呼んだときの最初の引数です。イベント名の決まり方は、関数の props とイベント に記述しています。

const counter = document.querySelector("acme-counter");
counter.count = 5;
counter.addEventListener("like-click", (event) => console.log(event.detail));

他のフレームワークから使用する

フレームワークごとの書き方は次のとおりです。どのフレームワークでも、count はプロパティとして要素に渡り、値を変更すると、同じ要素のまま新しい値が届きます。

フレームワーク書き方必要な設定
React 19<acme-counter count={count} onlike-click={(e) => …}>無し
Vue 3h("acme-counter", { count, onLikeClick })h() では不要
Svelte 5<acme-counter {count} onlike-click={(e) => …}>無し
Angular<acme-counter [count]="count()" (like-click)="…">CUSTOM_ELEMENTS_SCHEMA

React からは、イベントのリスナーをイベント名に合わせて onlike-click と記述します。React は、Custom Element に渡した onX の X を、大文字と小文字を変えずにイベント名として使用します。onLikeClick と記述すると LikeClick のイベントのリスナーになり、like-click のイベントを受け取れません。

Vue では、onLikeClick が like-click のイベントのリスナーになるので、Props の名前のまま記述できます。Vue のテンプレートにタグを記述するときは、@vitejs/plugin-vue の template.compilerOptions.isCustomElement で、acme- のタグを Custom Element として扱わせます。

// vite.config.ts
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: { isCustomElement: (tag) => tag.startsWith("acme-") },
      },
    }),
  ],
});

タグの型

sobria のアプリの中では、sobria prepare がアプリの要素のタグを HTMLElementTagNameMap に追加する型宣言を生成します。そのため、document.querySelector() や document.createElement() で取得した要素の props に型が付きます。

document.createElement("acme-counter").count = 1;

存在しない props を使用すると、型エラーになります。他のフレームワークの JSX やテンプレートの型は生成しないので、必要であれば、そのフレームワークのプロジェクトで宣言してください。

要素が発行するイベントにも型が付きます。addEventListener で受け取る CustomEvent の detail の型は、props の関数の最初の引数の型です。引数の無い関数の detail と、undefined を渡したときの detail は null です。onClick の click のように、イベント名がブラウザのイベントと同じときは、Shadow DOM の中から届くブラウザのイベントもリスナーに渡るので、リスナーが受け取るイベントの型は、CustomEvent とブラウザのイベントの型のユニオン型になります。

document.querySelector("acme-counter")?.addEventListener("like-click", (event) => {
  console.log(event.detail.toFixed());
});

expose したメソッドを外部から呼ぶ

要素が expose で公開したメソッドは、外部からも呼び出せます。外部から呼び出すときは、customElements.whenDefined() でタグの登録を待ちます。

await customElements.whenDefined("acme-dialog");
document.querySelector("acme-dialog")?.open();

whenDefined() が解決した時点で、各要素が起動しているとは限りません。load:visible や load:idle の要素は、起動するまで公開したメソッドを持ちません。expose は要素で、起動のタイミングは読み込みと値の受け渡しで説明します。

要素をライブラリとして配布する

素の HTML や他のフレームワークで使用するときは、ビルドした ES モジュールを配布します。別の sobria のアプリで使用するときは、TSX のソースのまま配布します。

どちらの形でも、prefix には、ほかのライブラリとタグ名が重複しない固有の値を設定します。タグ名が重複すると、ES モジュールの形ではブラウザの customElements.define() が例外を投げ、TSX のソースの形では使用する側のアプリがエラーになります。

ES モジュールとして配布する

Vite プラグインの設定に library: true を指定して vite build を実行します。

// vite.config.ts
import { sobria } from "sobria/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [sobria({ prefix: "acme", library: true })],
});

dist/ には、要素ごとに、自分自身を登録する ES モジュールを <タグ名>.js の名前で出力します。sobria のランタイムと、複数の要素が共通に使用するコードは dist/chunks/ に出力します。要素の CSS はモジュールに含まれるので、CSS のファイルを別に読み込む必要はありません。prefix がデフォルト値の sobria のままのときは、ビルドが警告を表示します。

使用する側のページでは、モジュールを読み込むと、タグを使用できます。

<script type="module" src="https://example.com/acme/acme-rating.js"></script>
<form>
  <label for="score">評価</label>
  <acme-rating id="score" name="score" required></acme-rating>
</form>

フォームの部品、スロット、<label for> は、sobria のアプリの中と同じように動作します。この形にはサーバーが無いので、要素はサーバーで描画されず、サーバー関数も呼び出せません。ライブラリには、サーバーを必要としない要素だけを含めてください。

マニフェスト

ビルドは、要素のタグ名、属性、プロパティ、イベント、スロット、CSS の part とカスタムプロパティを記述した Custom Elements Manifest を、dist/custom-elements.json に出力します。package.json の customElements には、このファイルのパスを記述してください。記述が無いか、出力したファイルと異なるパスを記述していると、ビルドが警告を表示します。

{
  "name": "acme-ui",
  "customElements": "dist/custom-elements.json"
}

マニフェストのプロパティには、スロット以外のすべての props を出力します。属性、イベント、スロットには、要素で説明した規則で、要素が受け付ける属性、発行するイベント、スロットを出力します。要素の関数の JSDoc は、要素の説明になります。Props の型のメンバーの JSDoc は、そのメンバーに対応する属性、プロパティ、イベント、スロットの説明になります。@deprecated は、要素の関数か、Props の型のメンバーに記述すると、その要素かメンバーが非推奨であることを表します。CSS のカスタムプロパティは、要素の関数の JSDoc に @cssprop --名前 - 説明 の形で記述します。デフォルト値を示すときは、@cssprop [--名前=デフォルト値] - 説明 の形で記述します。ビルドは、要素の JSX で、小文字で始まるタグの part に文字列のリテラルで記述した名前を、CSS の part として出力します。

type Props = {
  count: number;
  /** ボタンを押したときに、1 を足したいいねの数を通知します。 */
  onLikeClick?: (count: number) => void;
};

/**
 * いいねの数を表示します。
 * @cssprop [--counter-color=black] - 文字の色です。
 */
export default function Counter(props: Props) {
  return (
    <button part="button" onClick={() => props.onLikeClick?.(props.count + 1)}>
      {props.count}
    </button>
  );
}

属性とプロパティの型は、TypeScript が解決した型を文字列にして出力します。interface、enum、型の別名は、名前のまま出力します。ただし、type Label = string のようなプリミティブの型の別名は展開します。ユニオン型の要素の順序は、ソースの記述と異なることがあります。イベントの型は CustomEvent<detail の型> です。detail の型は、onX の props に渡す関数の、最初の引数の型です。引数を省略できるときと、引数の型が undefined か void を含むときは、detail の型は undefined を除いて null を含みます。引数の無い関数では、detail の型は null です。expose で公開するメソッドとプロパティは、Ctx<M> の M から取得します。M には、別のファイルから import した型も記述できます。M が object のときは、メソッドとプロパティを出力しません。

TSX のソースのまま配布する

別の sobria のアプリで使用するライブラリは、要素を TSX のソースのまま配布します。使用する側のアプリが、ライブラリの要素をアプリの要素と同じようにコンパイルします。そのため、ライブラリの要素もサーバーで描画され、アプリの要素と同じように動作します。

ライブラリの package.json には、sobria のフィールドに接頭辞と要素のディレクトリを記述し、exports で要素のファイルと型宣言を公開します。

{
  "name": "acme-ui",
  "type": "module",
  "sobria": { "prefix": "acme", "elements": "src/elements" },
  "exports": {
    "./*": { "types": "./types/*.d.ts", "default": "./src/elements/*.tsx" }
  },
  "files": ["src", "types"],
  "scripts": {
    "types": "sobria prepare && tsc -p .sobria/tsconfig.elements.json --composite false --declaration --rootDir src/elements --outDir types"
  },
  "peerDependencies": { "sobria": "*" }
}

types のスクリプトは、要素の型宣言を types/ に出力します。出力した型宣言を、そのまま配布します。

使用する側のアプリでは、アプリの要素と同じように import します。

// src/routes/page.tsx
import Badge from "acme-ui/badge";

export default function Page() {
  return <Badge label="new" />;
}

アプリの Vite プラグインは、アプリの依存のうち package.json に sobria のフィールドを持つパッケージを、コンパイルの対象にします。pnpm の link: のように、プロジェクトの外のディレクトリを指す依存のライブラリも使用できます。