svelte

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

@json-render/svelte

@json-render/svelte

Svelte 5 renderer that converts json-render specs into Svelte component trees.
这是一款Svelte 5渲染器,可将json-render规范转换为Svelte组件树。

Quick Start

快速开始

svelte
<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>
svelte
<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>

Creating a Catalog

创建组件目录

typescript
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",
    },
  },
});
typescript
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",
    },
  },
});

Defining Components

定义组件

Components should accept
BaseComponentProps<TProps>
:
typescript
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
}
svelte
<!-- Button.svelte -->
<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>
svelte
<!-- Card.svelte -->
<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>
组件应接受
BaseComponentProps<TProps>
类型:
typescript
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
}
svelte
<!-- Button.svelte -->
<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>
svelte
<!-- Card.svelte -->
<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>

Creating a Registry

创建注册表

typescript
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
    },
  },
});
typescript
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
    },
  },
});

Spec Structure (Element Tree)

规范结构(元素树)

The Svelte schema uses the element tree format:
json
{
  "root": "card1",
  "elements": {
    "card1": {
      "type": "Card",
      "props": { "title": "Hello" },
      "children": ["btn1"]
    },
    "btn1": {
      "type": "Button",
      "props": { "label": "Click me" }
    }
  }
}
Svelte 架构使用元素树格式:
json
{
  "root": "card1",
  "elements": {
    "card1": {
      "type": "Card",
      "props": { "title": "Hello" },
      "children": ["btn1"]
    },
    "btn1": {
      "type": "Button",
      "props": { "label": "Click me" }
    }
  }
}

Visibility Conditions

可见性条件

Use
visible
on elements to show/hide based on state:
  • { "$state": "/path" }
    - truthy check
  • { "$state": "/path", "eq": value }
    - equality check
  • { "$state": "/path", "not": true }
    - falsy check
  • { "$and": [cond1, cond2] }
    - AND conditions
  • { "$or": [cond1, cond2] }
    - OR conditions
在元素上使用
visible
属性,可基于状态控制显示/隐藏:
  • { "$state": "/path" }
    - 真值检查
  • { "$state": "/path", "eq": value }
    - 相等性检查
  • { "$state": "/path", "not": true }
    - 假值检查
  • { "$and": [cond1, cond2] }
    - 逻辑与条件
  • { "$or": [cond1, cond2] }
    - 逻辑或条件

Providers (via JsonUIProvider)

提供者(通过JsonUIProvider)

JsonUIProvider
composes all contexts. Individual contexts:
ContextPurpose
StateContext
Share state across components (JSON Pointer paths)
ActionContext
Handle actions dispatched via the event system
VisibilityContext
Enable conditional rendering based on state
ValidationContext
Form field validation
JsonUIProvider
整合了所有上下文。各个独立上下文的作用如下:
上下文用途
StateContext
在组件间共享状态(使用JSON指针路径)
ActionContext
处理通过事件系统分发的操作
VisibilityContext
支持基于状态的条件渲染
ValidationContext
表单字段验证

Event System

事件系统

Components use
emit
to fire named events. The element's
on
field maps events to action bindings:
svelte
<!-- Button.svelte -->
<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>
json
{
  "type": "Button",
  "props": { "label": "Submit" },
  "on": { "press": { "action": "submit" } }
}
组件使用
emit
方法触发命名事件。元素的
on
字段可将事件映射到操作绑定:
svelte
<!-- Button.svelte -->
<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>
json
{
  "type": "Button",
  "props": { "label": "Submit" },
  "on": { "press": { "action": "submit" } }
}

Built-in Actions

内置操作

The
setState
action is handled automatically and updates the state model:
json
{
  "action": "setState",
  "actionParams": { "statePath": "/activeTab", "value": "home" }
}
Other built-in actions:
pushState
,
removeState
,
push
,
pop
.
setState
操作会被自动处理,用于更新状态模型:
json
{
  "action": "setState",
  "actionParams": { "statePath": "/activeTab", "value": "home" }
}
其他内置操作包括:
pushState
removeState
push
pop

Dynamic Props and Two-Way Binding

动态属性与双向绑定

Expression forms resolved before your component receives props:
  • {"$state": "/state/key"}
    - read from state
  • {"$bindState": "/form/email"}
    - read + write-back to state
  • {"$bindItem": "field"}
    - read + write-back for repeat items
  • {"$cond": <condition>, "$then": <value>, "$else": <value>}
    - conditional value
For writable bindings inside components, use
getBoundProp
:
svelte
<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} />
表达式形式会在组件接收属性前解析:
  • {"$state": "/state/key"}
    - 从状态中读取
  • {"$bindState": "/form/email"}
    - 读取并写回状态
  • {"$bindItem": "field"}
    - 针对重复项的读取与写回
  • {"$cond": <condition>, "$then": <value>, "$else": <value>}
    - 条件值
若要在组件内使用可写绑定,请使用
getBoundProp
svelte
<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} />

Context Helpers

上下文工具

Preferred helpers:
  • getStateValue(path)
    - returns
    { current }
    (read/write)
  • getBoundProp(() => value, () => bindingPath)
    - returns
    { current }
    (read/write when bound)
  • isVisible(condition)
    - returns
    { current }
    (boolean)
  • getAction(name)
    - returns
    { current }
    (registered handler)
Advanced context access:
  • getStateContext()
  • getActionContext()
  • getVisibilityContext()
  • getValidationContext()
  • getOptionalValidationContext()
  • getFieldValidation(ctx, path, config?)
推荐使用的工具:
  • getStateValue(path)
    - 返回
    { current }
    (可读可写)
  • getBoundProp(() => value, () => bindingPath)
    - 返回
    { current }
    (绑定后可读可写)
  • isVisible(condition)
    - 返回
    { current }
    (布尔值)
  • getAction(name)
    - 返回
    { current }
    (已注册的处理函数)
高级上下文访问:
  • getStateContext()
  • getActionContext()
  • getVisibilityContext()
  • getValidationContext()
  • getOptionalValidationContext()
  • getFieldValidation(ctx, path, config?)

Streaming UI

流式UI

Use
createUIStream
for spec streaming:
svelte
<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}
Use
createChatUI
for chat + UI responses:
typescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");
使用
createUIStream
实现规范流式传输:
svelte
<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}
使用
createChatUI
实现聊天式UI响应:
typescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");