mcpField
S
svelte

svelte

github.com/vercel-labs開発18.8k1.2k 回インストール5 日前に更新
リポジトリ

svelte について

@json-render/svelteは、json-renderの仕様をSvelte 5のコンポーネントツリーに変換するレンダラーです。動的な画面を作る開発者向けで、状態、イベント、検証、UIストリーミングに対応します。

主な機能

  1. 01UI仕様をコンポーネントツリーに変換
  2. 02カタログとレジストリでコンポーネントを定義
  3. 03状態に応じた表示制御
  4. 04イベントと組み込みアクションの処理
  5. 05動的プロパティと双方向バインディング
  6. 06UIとチャット画面のストリーミング

活用シーン

  1. 01JSON仕様からSvelte画面を描画
  2. 02状態に応じた要素の表示切り替え
  3. 03コンポーネントイベントをアクションに接続
  4. 04仕様のストリーミング中に画面を描画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vercel-labs/json-render --skill svelte

インストール後、Claude Code で/svelteを入力して読み込まれたか確認します。

詳細

@json-render/svelte

json-render の仕様を Svelte コンポーネントツリーに変換する Svelte 5 レンダラーです。

クイックスタート

<script lang="ts">
  import { Renderer, JsonUIProvider } from "@json-render/svelte";
  import type { Spec } from "@json-render/svelte";
  import Card from "./components/Card.svelte";
  import Button from "./components/Button.svelte";

  interface Props {
    spec: Spec | null;
  }

  let { spec }: Props = $props();
  const registry = { Card, Button };
</script>

<JsonUIProvider>
  <Renderer {spec} {registry} />
</JsonUIProvider>

カタログの作成

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/svelte";
import { z } from "zod";

export const catalog = defineCatalog(schema, {
  components: {
    Button: {
      props: z.object({
        label: z.string(),
        variant: z.enum(["primary", "secondary"]).nullable(),
      }),
      description: "Clickable button",
    },
    Card: {
      props: z.object({ title: z.string() }),
      description: "Card container with title",
    },
  },
});

コンポーネントの定義

コンポーネントは BaseComponentProps<TProps> を受け取る必要があります。

interface BaseComponentProps<TProps> {
  props: TProps; // Resolved props for this component
  children?: Snippet; // Child elements (use {@render children()})
  emit: (event: string) => void; // Fire a named event
  bindings?: Record<string, string>; // Map of prop names to state paths (for $bindState)
  loading?: boolean; // True while spec is streaming
}

<script lang="ts">
  import type { BaseComponentProps } from "@json-render/svelte";

  interface Props extends BaseComponentProps<{ label: string; variant?: string }> {}
  let { props, emit }: Props = $props();
</script>

<button class={props.variant} onclick={() => emit("press")}>
  {props.label}
</button>

<script lang="ts">
  import type { Snippet } from "svelte";
  import type { BaseComponentProps } from "@json-render/svelte";

  interface Props extends BaseComponentProps<{ title: string }> {
    children?: Snippet;
  }

  let { props, children }: Props = $props();
</script>

<div class="card">
  <h2>{props.title}</h2>
  {#if children}
    {@render children()}
  {/if}
</div>

レジストリの作成

import { defineRegistry } from "@json-render/svelte";
import { catalog } from "./catalog";
import Card from "./components/Card.svelte";
import Button from "./components/Button.svelte";

const { registry, handlers, executeAction } = defineRegistry(catalog, {
  components: {
    Card,
    Button,
  },
  actions: {
    submit: async (params, setState, state) => {
      // handle action
    },
  },
});

仕様の構造(要素ツリー)

Svelte スキーマでは、要素ツリー形式を使用します。

{
  "root": "card1",
  "elements": {
    "card1": {
      "type": "Card",
      "props": { "title": "Hello" },
      "children": ["btn1"]
    },
    "btn1": {
      "type": "Button",
      "props": { "label": "Click me" }
    }
  }
}

表示条件

要素に visible を指定すると、状態に基づいて表示/非表示を切り替えられます。

  • { "$state": "/path" } - 真偽値のチェック
  • { "$state": "/path", "eq": value } - 等価性のチェック
  • { "$state": "/path", "not": true } - 偽値のチェック
  • { "$and": [cond1, cond2] } - AND 条件
  • { "$or": [cond1, cond2] } - OR 条件

プロバイダー(JsonUIProvider 経由)

JsonUIProvider はすべてのコンテキストをまとめます。個別のコンテキストは次のとおりです。

コンテキスト目的
StateContextコンポーネント間で状態を共有する(JSON Pointer パス)
ActionContextイベントシステム経由でディスパッチされたアクションを処理する
VisibilityContext状態に基づく条件付きレンダリングを有効にする
ValidationContextフォームフィールドの検証

イベントシステム

コンポーネントは emit を使用して名前付きイベントを発生させます。要素の on フィールドでは、イベントをアクションバインディングに対応付けます。


<script lang="ts">
  import type { BaseComponentProps } from "@json-render/svelte";

  interface Props extends BaseComponentProps<{ label: string }> {}

  let { props, emit }: Props = $props();
</script>

<button onclick={() => emit("press")}>{props.label}</button>
{
  "type": "Button",
  "props": { "label": "Submit" },
  "on": { "press": { "action": "submit" } }
}

組み込みアクション

setState アクションは自動的に処理され、状態モデルを更新します。

{
  "action": "setState",
  "actionParams": { "statePath": "/activeTab", "value": "home" }
}

その他の組み込みアクション:pushState、removeState、push、pop。

動的プロパティと双方向バインディング

ネストされたリストでは、囲む repeat の内側で repeat.statePath に { "$item": "field" } を設定できます。

コンポーネントにプロパティが渡される前に解決される式の形式:

  • {"$state": "/state/key"} - 状態から読み取る
  • {"$bindState": "/form/email"} - 状態から読み取り、書き戻す
  • {"$bindItem": "field"} - repeat 項目から読み取り、書き戻す
  • {"$cond": <condition>, "$then": <value>, "$else": <value>} - 条件付きの値

コンポーネント内で書き込み可能なバインディングを使うには、getBoundProp を使用します。

<script lang="ts">
  import { getBoundProp } from "@json-render/svelte";
  import type { BaseComponentProps } from "@json-render/svelte";

  interface Props extends BaseComponentProps<{ value?: string }> {}
  let { props, bindings }: Props = $props();

  let value = getBoundProp<string>(
    () => props.value,
    () => bindings?.value,
  );
</script>

<input bind:value={value.current} />

コンテキストヘルパー

推奨するヘルパー:

  • getStateValue(path) - { current } を返します(読み取り/書き込み)
  • getBoundProp(() => value, () => bindingPath) - { current } を返します(バインドされている場合は読み取り/書き込み)
  • isVisible(condition) - { current } を返します(真偽値)
  • getAction(name) - { current } を返します(登録済みハンドラー)

高度なコンテキストアクセス:

  • getStateContext()
  • getActionContext()
  • getVisibilityContext()
  • getValidationContext()
  • getOptionalValidationContext()
  • getFieldValidation(ctx, path, config?)

UI のストリーミング

仕様をストリーミングするには createUIStream を使用します。

<script lang="ts">
  import { createUIStream, Renderer } from "@json-render/svelte";

  const stream = createUIStream({
    api: "/api/generate-ui",
    onComplete: (spec) => console.log("Done", spec),
  });

  async function generate() {
    await stream.send("Create a login form");
  }
</script>

<button onclick={generate} disabled={stream.isStreaming}>
  {stream.isStreaming ? "Generating..." : "Generate UI"}
</button>

{#if stream.spec}
  <Renderer spec={stream.spec} {registry} loading={stream.isStreaming} />
{/if}

チャットと UI の応答には createChatUI を使用します。

const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");

コメント