ngx-json-render 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Built on `@json-render/core` (the same spec format, expressions, state store, ac
8
8
 
9
9
  This is an Angular **adapter over the official core**, not a second implementation of it — `@json-render/core` is a peer dependency, and the spec your model emits is the same one the React, Vue, Solid and Svelte renderers consume. A catalog and a spec written here move to another framework unchanged.
10
10
 
11
- **[Live demo](https://shteynu.github.io/ngx-json-render/)** — interactive spec (bindings, repeat, confirm, watch) and a replayable SpecStream showing progressive rendering ([source](https://github.com/shteynu/ngx-json-render/tree/main/projects/demo)) — or [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/shteynu/ngx-json-render)
11
+ **[Live demo](https://shteynu.github.io/ngx-json-render/)** — interactive spec (bindings, repeat, confirm, watch), a SpecStream rendering progressively that you can stop mid-generation, and a chat where prose and UI patches arrive in one reply; both streaming tabs replay a recording, or call a real model on your own key ([source](https://github.com/shteynu/ngx-json-render/tree/main/projects/demo)) — or [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/shteynu/ngx-json-render)
12
12
 
13
13
  ![A SpecStream of RFC 6902 patches rendering progressively into an Angular dashboard](https://raw.githubusercontent.com/shteynu/ngx-json-render/main/docs/streaming.gif)
14
14
 
@@ -135,6 +135,9 @@ import { injectUIStream } from 'ngx-json-render';
135
135
  template: `
136
136
  <json-render [spec]="ui.spec()" [registry]="registry" [loading]="ui.isStreaming()" />
137
137
  <button (click)="ui.send('A dashboard for weekly sales')">Generate</button>
138
+ @if (ui.isStreaming()) {
139
+ <button (click)="ui.stop()">Stop</button>
140
+ }
138
141
  `,
139
142
  imports: [JsonRenderer],
140
143
  })
@@ -175,10 +178,48 @@ app.post('/api/generate', async (req, res) => {
175
178
 
176
179
  The patches apply to the `spec` signal as each line arrives, so the UI assembles on screen while the model is still generating — exactly what the [demo's Streaming tab](https://shteynu.github.io/ngx-json-render/) replays. Prefer structured output? `catalog.jsonSchema()` exports a JSON Schema for `streamObject`/tool calls, and `catalog.validate(spec)` checks a finished spec against the catalog.
177
180
 
181
+ ### Supplying the transport
182
+
183
+ By default the request goes through the global `fetch`. Pass your own to add
184
+ auth headers, route through your app's HTTP layer, or replay a recorded
185
+ generation in a test — it is called with the endpoint and a request carrying
186
+ the JSON body and the abort signal, and must resolve to a `Response` whose
187
+ `body` streams the JSONL:
188
+
189
+ ```ts
190
+ readonly ui = injectUIStream({
191
+ api: '/api/generate',
192
+ fetch: (url, init) =>
193
+ fetch(url, { ...init, headers: { ...init?.headers, Authorization: token } }),
194
+ });
195
+ ```
196
+
197
+ `injectChatUI` takes the same option.
198
+
199
+ ### Stopping, clearing and refining
200
+
201
+ `stop()` ends the generation in flight and keeps what has already rendered —
202
+ the user changed their mind, so it is not an error and `error` stays null.
203
+ `clear()` stops it too and then resets `spec`, `error`, `usage` and `rawLines`;
204
+ without the stop the request still running would put its spec back on its very
205
+ next patch. Both are no-ops when nothing is streaming, and `injectChatUI` has
206
+ the same pair.
207
+
208
+ To refine a generated UI instead of starting over, hand `send` the spec to
209
+ build on. It is sent to the endpoint as `currentSpec` and the streamed patches
210
+ apply on top of it:
211
+
212
+ ```ts
213
+ ui.send('make the chart a bar chart', { previousSpec: ui.spec()! });
214
+ ```
215
+
216
+ The second argument also carries `context`, forwarded to the endpoint as-is:
217
+ `ui.send(prompt, { context: { locale }, previousSpec })`.
218
+
178
219
  Also available:
179
220
 
180
- - `injectChatUI({ api })` — chat + GenUI: the endpoint takes `{ messages }` and streams prose mixed with ` ```spec ` fenced JSONL; each assistant message carries `text` and/or a `spec`.
181
- - `applyPatch(spec, patch)` — immutably apply one RFC 6902 patch to a spec.
221
+ - `injectChatUI({ api, fetch? })` — chat + GenUI: the endpoint takes `{ messages }` and streams prose mixed with ` ```spec ` fenced JSONL; each assistant message carries `text` and/or a `spec`.
222
+ - `applyPatch(spec, patch)` — immutably apply one RFC 6902 patch to a spec, sharing every subtree the patch did not touch. Both hooks and `buildSpecFromParts` apply through it, so the same stream produces the same spec whichever one you reach for. One deliberate deviation from the RFC: a failing `test` op is a no-op rather than an abort, because these patches come off a model's output and dropping a bad line beats killing the generation.
182
223
  - `buildSpecFromParts` / `getTextFromParts` / `jsonRenderMessage` — derive specs from AI SDK `message.parts`.
183
224
  - `catalog.prompt()` / `buildUserPrompt` (from `@json-render/core`) — generate the system/user prompts for your catalog.
184
225
 
@@ -186,22 +227,22 @@ Also available:
186
227
 
187
228
  Full parity with the baseline json-render contract:
188
229
 
189
- | Feature | Example |
190
- | --- | --- |
191
- | Dynamic props | `{ "$state": "/user/name" }` |
192
- | Two-way binding | `{ "$bindState": "/form/email" }`, `{ "$bindItem": "done" }` |
193
- | Conditionals | `{ "$cond": {...}, "$then": ..., "$else": ... }` |
194
- | Templates | `{ "$template": "Hello, ${/user/name}" }` |
195
- | Computed / directives | `{ "$computed": "fmtDate", "args": {...} }`, custom `$`-directives |
196
- | Visibility | `"visible": { "$state": "/count", "gte": 5 }` (incl. `$and`/`$or`, `$item`, `$index`) |
197
- | Events → actions | `"on": { "press": { "action": "...", "params": {...}, "confirm": {...}, "onSuccess": ..., "onError": ... } }` |
198
- | Built-in actions | `setState`, `pushState` (with `$id`), `removeState`, `push`/`pop`, `validateForm` |
199
- | Repeat | `"repeat": { "statePath": "/todos", "key": "id" }`, nested via `{ "$item": "..." }` |
200
- | Watch | `"watch": { "/country": { "action": "loadCities" } }` |
201
- | Slots | `"slots": { "header": ["title-el"] }` + `<jr-children slot="header" />` |
202
- | Validation | field checks via `ValidationConfig`, `validateForm`, `injectFieldValidation` |
203
- | Confirm dialogs | built-in `<jr-confirm-dialog>` (auto-rendered) |
204
- | Devtools hooks | action observer + `data-jr-key` picker attributes |
230
+ | Feature | Example |
231
+ | --------------------- | ------------------------------------------------------------------------------------------------------------- |
232
+ | Dynamic props | `{ "$state": "/user/name" }` |
233
+ | Two-way binding | `{ "$bindState": "/form/email" }`, `{ "$bindItem": "done" }` |
234
+ | Conditionals | `{ "$cond": {...}, "$then": ..., "$else": ... }` |
235
+ | Templates | `{ "$template": "Hello, ${/user/name}" }` |
236
+ | Computed / directives | `{ "$computed": "fmtDate", "args": {...} }`, custom `$`-directives |
237
+ | Visibility | `"visible": { "$state": "/count", "gte": 5 }` (incl. `$and`/`$or`, `$item`, `$index`) |
238
+ | Events → actions | `"on": { "press": { "action": "...", "params": {...}, "confirm": {...}, "onSuccess": ..., "onError": ... } }` |
239
+ | Built-in actions | `setState`, `pushState` (with `$id`), `removeState`, `push`/`pop`, `validateForm` |
240
+ | Repeat | `"repeat": { "statePath": "/todos", "key": "id" }`, nested via `{ "$item": "..." }` |
241
+ | Watch | `"watch": { "/country": { "action": "loadCities" } }` |
242
+ | Slots | `"slots": { "header": ["title-el"] }` + `<jr-children slot="header" />` |
243
+ | Validation | field checks via `ValidationConfig`, `validateForm`, `injectFieldValidation` |
244
+ | Confirm dialogs | built-in `<jr-confirm-dialog>` (auto-rendered) |
245
+ | Devtools hooks | action observer + `data-jr-key` picker attributes |
205
246
 
206
247
  ## State
207
248
 
@@ -250,7 +291,7 @@ rest.
250
291
  and `onSuccess.set` all take a `statePath` straight from the spec, so a
251
292
  generated UI can write anywhere in the state model it is rendered against —
252
293
  and `push`/`pop` write `/currentScreen` and `/navStack`. In controlled mode
253
- this is *your* store. Give the renderer a store scoped to the generated view
294
+ this is _your_ store. Give the renderer a store scoped to the generated view
254
295
  rather than the one holding session, entitlement or billing state.
255
296
 
256
297
  **A spec chooses the navigation target.** `onSuccess: { navigate }` passes its
@@ -265,7 +306,7 @@ one.
265
306
 
266
307
  **`confirm` is a UX affordance, not a security control.** It routes an action
267
308
  through the confirmation dialog before the handler runs, but it is set on the
268
- action binding *inside the spec* (`on.press.confirm`) — so the same party that
309
+ action binding _inside the spec_ (`on.press.confirm`) — so the same party that
269
310
  chose the action also chose whether to ask. Real authorization belongs in the
270
311
  handler, on the server.
271
312
 
@@ -281,20 +322,20 @@ Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`,
281
322
 
282
323
  ## Renderer inputs
283
324
 
284
- | Input | Type | Purpose |
285
- | --- | --- | --- |
286
- | `spec` | `Spec \| null` | The UI spec (may be partial while streaming) |
287
- | `registry` | `ComponentRegistry` | Catalog type → Angular component |
288
- | `loading` | `boolean` | Suppress missing-element warnings while streaming |
289
- | `fallback` | `Type<unknown>` | Component for unknown types |
290
- | `state` | `StateModel` | Initial state (uncontrolled; defaults to `spec.state`) |
291
- | `store` | `StateStore` | External store (controlled mode) |
292
- | `handlers` | `Record<string, ActionHandler>` | Action handlers |
293
- | `onAction` | `(name, params) => unknown` | Catch-all action handler |
294
- | `navigate` | `(path) => void` | Used by `onSuccess: { navigate }` |
295
- | `validationFunctions` | `Record<string, ValidationFunction>` | Custom validation |
296
- | `functions` | `Record<string, ComputedFunction>` | `$computed` functions |
297
- | `directives` | `DirectiveDefinition[]` | Custom `$`-prefixed expressions |
325
+ | Input | Type | Purpose |
326
+ | --------------------- | ------------------------------------ | ------------------------------------------------------ |
327
+ | `spec` | `Spec \| null` | The UI spec (may be partial while streaming) |
328
+ | `registry` | `ComponentRegistry` | Catalog type → Angular component |
329
+ | `loading` | `boolean` | Suppress missing-element warnings while streaming |
330
+ | `fallback` | `Type<unknown>` | Component for unknown types |
331
+ | `state` | `StateModel` | Initial state (uncontrolled; defaults to `spec.state`) |
332
+ | `store` | `StateStore` | External store (controlled mode) |
333
+ | `handlers` | `Record<string, ActionHandler>` | Action handlers |
334
+ | `onAction` | `(name, params) => unknown` | Catch-all action handler |
335
+ | `navigate` | `(path) => void` | Used by `onSuccess: { navigate }` |
336
+ | `validationFunctions` | `Record<string, ValidationFunction>` | Custom validation |
337
+ | `functions` | `Record<string, ComputedFunction>` | `$computed` functions |
338
+ | `directives` | `DirectiveDefinition[]` | Custom `$`-prefixed expressions |
298
339
 
299
340
  Output: `(stateChange)` — batched `{ path, value }[]` in uncontrolled mode.
300
341