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 on elements to show/hide based on state:
visible- - truthy check
{ "$state": "/path" } - - equality check
{ "$state": "/path", "eq": value } - - falsy check
{ "$state": "/path", "not": true } - - AND conditions
{ "$and": [cond1, cond2] } - - OR conditions
{ "$or": [cond1, cond2] }
在元素上使用属性,可基于状态控制显示/隐藏:
visible- - 真值检查
{ "$state": "/path" } - - 相等性检查
{ "$state": "/path", "eq": value } - - 假值检查
{ "$state": "/path", "not": true } - - 逻辑与条件
{ "$and": [cond1, cond2] } - - 逻辑或条件
{ "$or": [cond1, cond2] }
Providers (via JsonUIProvider)
提供者(通过JsonUIProvider)
JsonUIProvider| Context | Purpose |
|---|---|
| Share state across components (JSON Pointer paths) |
| Handle actions dispatched via the event system |
| Enable conditional rendering based on state |
| Form field validation |
JsonUIProvider| 上下文 | 用途 |
|---|---|
| 在组件间共享状态(使用JSON指针路径) |
| 处理通过事件系统分发的操作 |
| 支持基于状态的条件渲染 |
| 表单字段验证 |
Event System
事件系统
Components use to fire named events. The element's field maps events to action bindings:
emitonsvelte
<!-- 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" } }
}组件使用方法触发命名事件。元素的字段可将事件映射到操作绑定:
emitonsvelte
<!-- 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 action is handled automatically and updates the state model:
setStatejson
{
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
}Other built-in actions: , , , .
pushStateremoveStatepushpopsetStatejson
{
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
}其他内置操作包括:、、、。
pushStateremoveStatepushpopDynamic Props and Two-Way Binding
动态属性与双向绑定
Expression forms resolved before your component receives props:
- - read from state
{"$state": "/state/key"} - - read + write-back to state
{"$bindState": "/form/email"} - - read + write-back for repeat items
{"$bindItem": "field"} - - conditional value
{"$cond": <condition>, "$then": <value>, "$else": <value>}
For writable bindings inside components, use :
getBoundPropsvelte
<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>}
若要在组件内使用可写绑定,请使用:
getBoundPropsvelte
<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:
- - returns
getStateValue(path)(read/write){ current } - - returns
getBoundProp(() => value, () => bindingPath)(read/write when bound){ current } - - returns
isVisible(condition)(boolean){ current } - - returns
getAction(name)(registered handler){ current }
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 for spec streaming:
createUIStreamsvelte
<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 for chat + UI responses:
createChatUItypescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");使用实现规范流式传输:
createUIStreamsvelte
<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响应:
createChatUItypescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");