Skip to content

Commit 35e9f3c

Browse files
committed
docs(examples): make the client-only json-render dock interactive
Turns the inline client-authored view into a small interactive playground: `$bindState` text/switch inputs write into the view's own state, `$state` reads mirror them live, and Add / Clear buttons drive the built-in `pushState` / `setState` actions to mutate a notes list that re-renders — all client-side, with no server and no shared state. Applied to both the Vite (Vue) and Next (React) hub shells.
1 parent 174939c commit 35e9f3c

5 files changed

Lines changed: 125 additions & 58 deletions

File tree

docs/examples/minimal-next-devframe-hub.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Package: `minimal-next-devframe-hub` · framework: **React (Next.js)**
1616
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
1717
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json`, which starts the singleton host on demand.
1818
- The [JSON-render](/guide/json-render) hub integration with **registry replacement**: the host authors a view and projects it onto a `json-render` dock, and the React client renders it with a small in-example React registry (rather than the Vue `@devframes/json-render-ui`) — the path a non-Vue host uses.
19-
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and a `json-render` dock whose spec is authored in the browser and carried inline in the dock entry (`view: { spec }`) — rendered by the same React registry as the server-authored view, with no shared state, yet never syncing to the hub or other viewers.
19+
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and an interactive `json-render` dock whose spec is authored in the browser and carried inline in the dock entry (`view: { spec }`) — its inputs, toggles, and `pushState`/`setState` buttons drive the view's own state (no shared state, nothing synced to the hub), rendered by the same React registry as the server-authored view.
2020

2121
## Run it
2222

docs/examples/minimal-vite-devframe-hub.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Package: `minimal-vite-devframe-hub` · framework: **Vanilla TypeScript (Vite)**
1616
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
1717
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware.
1818
- The opt-in [JSON-render](/guide/json-render) hub integration end to end: the host authors a view on its hub context and projects it onto a `json-render` dock, and the client host renders it via `@devframes/json-render-ui` (registered through `createDevframeClientHost({ renderers })`).
19-
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and a `json-render` dock whose spec is authored in the browser and carried inline in the dock entry (`view: { spec }`) — rendered by the same renderer as the server-authored view, with no shared state, yet never syncing to the hub or other viewers.
19+
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and an interactive `json-render` dock whose spec is authored in the browser and carried inline in the dock entry (`view: { spec }`) — its inputs, toggles, and `pushState`/`setState` buttons drive the view's own state (no shared state, nothing synced to the hub), rendered by the same renderer as the server-authored view.
2020

2121
## Run it
2222

docs/guide/client-context.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,15 @@ A client-only dock can render a [JSON-render](./json-render) view the page autho
9696
const spec = { /* a DevframeJsonRenderSpec built in the browser */ }
9797

9898
ctx.docks.register({
99-
id: 'client-metrics',
100-
title: 'Client Metrics',
101-
icon: 'ph:gauge-duotone',
99+
id: 'client-playground',
100+
title: 'Client Playground',
101+
icon: 'ph:sliders-horizontal-duotone',
102102
type: 'json-render',
103103
view: { spec },
104104
})
105105
```
106106

107-
The `view` field accepts either `{ spec }` (rendered inline, static) or `{ stateKey }` (subscribed to a live shared state, the shape `createJsonRenderView` produces server-side).
107+
The `view` field accepts either `{ spec }` (the spec rendered inline) or `{ stateKey }` (subscribed to a live shared state, the shape `createJsonRenderView` produces server-side). An inline view still runs its own state: `{ $bindState }` inputs and `{ $state }` reads work against the spec's `state`, and the built-in `setState` / `pushState` / `removeState` actions mutate it — so a client-authored view is interactive with no server and no shared state. What `{ spec }` lacks versus `{ stateKey }` is a server-driven update stream.
108108

109109
## Dock client scripts
110110

examples/minimal-next-devframe-hub/src/client/app/page.tsx

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,44 +58,76 @@ function createClientNotesUrl(): string {
5858
return URL.createObjectURL(new Blob([html], { type: 'text/html' }))
5959
}
6060

61-
// A json-render spec synthesized entirely in the browser — the client-only
62-
// counterpart to a server-authored view. It reads real values captured from the
63-
// page at registration time and uses the same base-catalog components the hub's
64-
// server view does, rendered here by the mini React registry.
65-
function createClientMetricsSpec(clientType: string): DevframeJsonRenderSpec {
61+
// An *interactive* json-render spec synthesized entirely in the browser — the
62+
// client-only counterpart to a server-authored view. Interactivity needs no
63+
// server and no shared state: `{ $bindState }` inputs write straight into the
64+
// view's own `state`, `{ $state }` reads mirror it live, and the buttons use the
65+
// framework's built-in state actions (`pushState` / `setState`) to mutate that
66+
// state — every change re-renders through the mini React registry.
67+
function createClientPlaygroundSpec(clientType: string): DevframeJsonRenderSpec {
6668
return {
6769
root: 'root',
6870
elements: {
69-
root: { type: 'Stack', props: { gap: 14 }, children: ['head', 'about', 'env'] },
71+
root: { type: 'Stack', props: { gap: 14 }, children: ['head', 'hello', 'notes', 'env'] },
72+
7073
head: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center' }, children: ['icon', 'title', 'badge'] },
71-
icon: { type: 'Icon', props: { name: 'ph:gauge-duotone', size: 22 }, children: [] },
72-
title: { type: 'Text', props: { text: 'Client Metrics', variant: 'heading' }, children: [] },
74+
icon: { type: 'Icon', props: { name: 'ph:sliders-horizontal-duotone', size: 22 }, children: [] },
75+
title: { type: 'Text', props: { text: 'Client Playground', variant: 'heading' }, children: [] },
7376
badge: { type: 'Badge', props: { text: 'client-only', variant: 'info' }, children: [] },
74-
about: { type: 'Card', props: { title: 'About this dock' }, children: ['aboutText'] },
75-
aboutText: {
76-
type: 'Text',
77-
props: {
78-
text: 'This json-render view was authored in the browser and seeded into a client-local shared state — it never reaches the hub server or other viewers, yet renders through the same dock renderer as a server-authored view.',
79-
variant: 'body',
80-
color: 'muted',
81-
},
77+
78+
// ── Two-way binding: type a name, see it echoed live; toggle a switch ──
79+
hello: { type: 'Card', props: { title: 'Say hello' }, children: ['helloBody'] },
80+
helloBody: { type: 'Stack', props: { gap: 10 }, children: ['nameInput', 'greetRow', 'compact'] },
81+
nameInput: { type: 'TextInput', props: { label: 'Your name', placeholder: 'Type your name…', value: { $bindState: '/form/name' } }, children: [] },
82+
greetRow: { type: 'Stack', props: { direction: 'row', gap: 6, align: 'center' }, children: ['greetLabel', 'greetName'] },
83+
greetLabel: { type: 'Text', props: { text: 'Hello,', variant: 'body', color: 'muted' }, children: [] },
84+
greetName: { type: 'Text', props: { text: { $state: '/form/name' }, variant: 'body', color: 'primary' }, children: [] },
85+
compact: { type: 'Switch', props: { label: 'Compact mode', value: { $bindState: '/prefs/compact' } }, children: [] },
86+
87+
// ── Actions mutate state → the DataTable re-renders ──
88+
notes: { type: 'Card', props: { title: 'Notes' }, children: ['notesBody'] },
89+
notesBody: { type: 'Stack', props: { gap: 10 }, children: ['draftRow', 'notesTable', 'clearBtn'] },
90+
draftRow: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'end' }, children: ['draftInput', 'addBtn'] },
91+
draftInput: { type: 'TextInput', props: { label: 'New note', placeholder: 'Write something…', value: { $bindState: '/draft' } }, children: [] },
92+
addBtn: {
93+
type: 'Button',
94+
props: { label: 'Add', variant: 'primary', icon: 'ph:plus' },
95+
// Built-in `pushState`: append the typed draft to /notes, then clear the input.
96+
on: { press: { action: 'pushState', params: { statePath: '/notes', value: { text: { $state: '/draft' } }, clearStatePath: '/draft' } } },
97+
children: [],
98+
},
99+
notesTable: {
100+
type: 'DataTable',
101+
props: { columns: [{ key: 'text', label: 'Note' }], rows: { $state: '/notes' }, height: 160 },
102+
children: [],
103+
},
104+
clearBtn: {
105+
type: 'Button',
106+
props: { label: 'Clear all', variant: 'ghost', icon: 'ph:trash' },
107+
// Built-in `setState`: replace /notes with an empty array.
108+
on: { press: { action: 'setState', params: { statePath: '/notes', value: [] } } },
82109
children: [],
83110
},
84-
env: { type: 'Card', props: { title: 'Environment' }, children: ['envTable'] },
111+
112+
env: { type: 'Card', props: { title: 'Environment', collapsible: true, defaultCollapsed: true }, children: ['envTable'] },
85113
envTable: {
86114
type: 'KeyValueTable',
87115
props: {
88116
data: {
89117
clientType,
90118
language: navigator.language,
91119
viewport: `${window.innerWidth}×${window.innerHeight}`,
92-
online: navigator.onLine ? 'yes' : 'no',
93120
},
94121
},
95122
children: [],
96123
},
97124
},
98-
state: {},
125+
state: {
126+
form: { name: '' },
127+
prefs: { compact: false },
128+
draft: '',
129+
notes: [{ text: 'Authored entirely in the browser' }],
130+
},
99131
}
100132
}
101133

@@ -166,16 +198,17 @@ export default function Page() {
166198
// Register a second client-only dock — this one a *json-render* view the
167199
// page authors itself, the richer sibling of the iframe dock above. Its
168200
// spec is carried **inline** in the dock entry (`view.spec`), so it needs
169-
// no shared state at all: it lives only in this page yet renders through
170-
// the same `json-render` dock renderer (the mini React registry) as a
171-
// server-authored view. `force` lets React StrictMode re-run this effect
172-
// safely.
201+
// no shared state at all: it lives only in this page yet renders — and
202+
// stays fully interactive (inputs, toggles, and buttons that mutate its
203+
// state) — through the same `json-render` dock renderer (the mini React
204+
// registry) as a server-authored view. `force` lets React StrictMode
205+
// re-run this effect safely.
173206
const clientJsonRenderDock = clientHost.context.docks.register<DevframeJsonRenderDockEntry>({
174-
id: 'client-metrics',
175-
title: 'Client Metrics',
176-
icon: 'ph:gauge-duotone',
207+
id: 'client-playground',
208+
title: 'Client Playground',
209+
icon: 'ph:sliders-horizontal-duotone',
177210
type: 'json-render',
178-
view: { spec: createClientMetricsSpec(clientHost.context.clientType) },
211+
view: { spec: createClientPlaygroundSpec(clientHost.context.clientType) },
179212
category: 'app',
180213
}, true)
181214

examples/minimal-vite-devframe-hub/src/client/main.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -82,44 +82,76 @@ function createClientNotesUrl(): string {
8282
return URL.createObjectURL(new Blob([html], { type: 'text/html' }))
8383
}
8484

85-
// A json-render spec synthesized entirely in the browser — the client-only
86-
// counterpart to a server-authored view. It reads real values captured from the
87-
// page at registration time and uses the same base-catalog components the hub's
88-
// server view does, rendered by the very same `createJsonRenderDockRenderer`.
89-
function createClientMetricsSpec(clientType: string): DevframeJsonRenderSpec {
85+
// An *interactive* json-render spec synthesized entirely in the browser — the
86+
// client-only counterpart to a server-authored view. Interactivity needs no
87+
// server and no shared state: `{ $bindState }` inputs write straight into the
88+
// view's own `state`, `{ $state }` reads mirror it live, and the buttons use the
89+
// framework's built-in state actions (`pushState` / `setState`) to mutate that
90+
// state — every change re-renders through the same `createJsonRenderDockRenderer`.
91+
function createClientPlaygroundSpec(clientType: string): DevframeJsonRenderSpec {
9092
return {
9193
root: 'root',
9294
elements: {
93-
root: { type: 'Stack', props: { gap: 14 }, children: ['head', 'about', 'env'] },
95+
root: { type: 'Stack', props: { gap: 14 }, children: ['head', 'hello', 'notes', 'env'] },
96+
9497
head: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center' }, children: ['icon', 'title', 'badge'] },
95-
icon: { type: 'Icon', props: { name: 'ph:gauge-duotone', size: 22 }, children: [] },
96-
title: { type: 'Text', props: { text: 'Client Metrics', variant: 'heading' }, children: [] },
98+
icon: { type: 'Icon', props: { name: 'ph:sliders-horizontal-duotone', size: 22 }, children: [] },
99+
title: { type: 'Text', props: { text: 'Client Playground', variant: 'heading' }, children: [] },
97100
badge: { type: 'Badge', props: { text: 'client-only', variant: 'info' }, children: [] },
98-
about: { type: 'Card', props: { title: 'About this dock' }, children: ['aboutText'] },
99-
aboutText: {
100-
type: 'Text',
101-
props: {
102-
text: 'This json-render view was authored in the browser and seeded into a client-local shared state — it never reaches the hub server or other viewers, yet renders through the same dock renderer as a server-authored view.',
103-
variant: 'body',
104-
color: 'muted',
105-
},
101+
102+
// ── Two-way binding: type a name, see it echoed live; toggle a switch ──
103+
hello: { type: 'Card', props: { title: 'Say hello' }, children: ['helloBody'] },
104+
helloBody: { type: 'Stack', props: { gap: 10 }, children: ['nameInput', 'greetRow', 'compact'] },
105+
nameInput: { type: 'TextInput', props: { label: 'Your name', placeholder: 'Type your name…', value: { $bindState: '/form/name' } }, children: [] },
106+
greetRow: { type: 'Stack', props: { direction: 'row', gap: 6, align: 'center' }, children: ['greetLabel', 'greetName'] },
107+
greetLabel: { type: 'Text', props: { text: 'Hello,', variant: 'body', color: 'muted' }, children: [] },
108+
greetName: { type: 'Text', props: { text: { $state: '/form/name' }, variant: 'body', color: 'primary' }, children: [] },
109+
compact: { type: 'Switch', props: { label: 'Compact mode', value: { $bindState: '/prefs/compact' } }, children: [] },
110+
111+
// ── Actions mutate state → the DataTable re-renders ──
112+
notes: { type: 'Card', props: { title: 'Notes' }, children: ['notesBody'] },
113+
notesBody: { type: 'Stack', props: { gap: 10 }, children: ['draftRow', 'notesTable', 'clearBtn'] },
114+
draftRow: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'end' }, children: ['draftInput', 'addBtn'] },
115+
draftInput: { type: 'TextInput', props: { label: 'New note', placeholder: 'Write something…', value: { $bindState: '/draft' } }, children: [] },
116+
addBtn: {
117+
type: 'Button',
118+
props: { label: 'Add', variant: 'primary', icon: 'ph:plus' },
119+
// Built-in `pushState`: append the typed draft to /notes, then clear the input.
120+
on: { press: { action: 'pushState', params: { statePath: '/notes', value: { text: { $state: '/draft' } }, clearStatePath: '/draft' } } },
121+
children: [],
122+
},
123+
notesTable: {
124+
type: 'DataTable',
125+
props: { columns: [{ key: 'text', label: 'Note' }], rows: { $state: '/notes' }, height: 160 },
126+
children: [],
127+
},
128+
clearBtn: {
129+
type: 'Button',
130+
props: { label: 'Clear all', variant: 'ghost', icon: 'ph:trash' },
131+
// Built-in `setState`: replace /notes with an empty array.
132+
on: { press: { action: 'setState', params: { statePath: '/notes', value: [] } } },
106133
children: [],
107134
},
108-
env: { type: 'Card', props: { title: 'Environment' }, children: ['envTable'] },
135+
136+
env: { type: 'Card', props: { title: 'Environment', collapsible: true, defaultCollapsed: true }, children: ['envTable'] },
109137
envTable: {
110138
type: 'KeyValueTable',
111139
props: {
112140
data: {
113141
clientType,
114142
language: navigator.language,
115143
viewport: `${window.innerWidth}×${window.innerHeight}`,
116-
online: navigator.onLine ? 'yes' : 'no',
117144
},
118145
},
119146
children: [],
120147
},
121148
},
122-
state: {},
149+
state: {
150+
form: { name: '' },
151+
prefs: { compact: false },
152+
draft: '',
153+
notes: [{ text: 'Authored entirely in the browser' }],
154+
},
123155
}
124156
}
125157

@@ -161,14 +193,16 @@ async function main() {
161193
// Register a second client-only dock — this one a *json-render* view the page
162194
// authors itself, the richer sibling of the iframe dock above. Its spec is
163195
// carried **inline** in the dock entry (`view.spec`), so it needs no shared
164-
// state at all: it lives only in this page yet renders through the very same
165-
// `json-render` dock renderer registered above as a server-authored view.
196+
// state at all: it lives only in this page yet renders — and stays fully
197+
// interactive (inputs, toggles, and buttons that mutate its state) — through
198+
// the very same `json-render` dock renderer registered above as a
199+
// server-authored view.
166200
host.context.docks.register<DevframeJsonRenderDockEntry>({
167-
id: 'client-metrics',
168-
title: 'Client Metrics',
169-
icon: 'ph:gauge-duotone',
201+
id: 'client-playground',
202+
title: 'Client Playground',
203+
icon: 'ph:sliders-horizontal-duotone',
170204
type: 'json-render',
171-
view: { spec: createClientMetricsSpec(host.context.clientType) },
205+
view: { spec: createClientPlaygroundSpec(host.context.clientType) },
172206
category: 'app',
173207
})
174208

0 commit comments

Comments
 (0)