ngx-json-render 0.2.0 → 0.3.0
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 +363 -14
- package/fesm2022/ngx-json-render-testing.mjs +389 -0
- package/fesm2022/ngx-json-render-testing.mjs.map +1 -0
- package/fesm2022/ngx-json-render.mjs +882 -403
- package/fesm2022/ngx-json-render.mjs.map +1 -1
- package/package.json +5 -1
- package/types/ngx-json-render-testing.d.ts +197 -0
- package/types/ngx-json-render.d.ts +329 -60
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
Angular renderer for [json-render](https://github.com/vercel-labs/json-render) — give an LLM a catalog of your components, stream back a JSON spec, and render it as real Angular components. No `innerHTML`, no `eval`, no framework lock-in on the wire format.
|
|
6
6
|
|
|
7
|
-
Built on `@json-render/core` (the same spec format, expressions, state store, actions, and streaming compiler used by the React, Vue, Solid, and Svelte renderers) and idiomatic modern Angular: standalone components, signals,
|
|
7
|
+
Built on `@json-render/core` (the same spec format, expressions, state store, actions, and streaming compiler used by the React, Vue, Solid, and Svelte renderers) and idiomatic modern Angular: standalone components, signals, `OnPush` everywhere, and zoneless — no `NgZone`, no Zone.js.
|
|
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
|
|
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 [](https://stackblitz.com/github/shteynu/ngx-json-render)
|
|
12
12
|
|
|
13
13
|

|
|
14
14
|
|
|
@@ -18,7 +18,62 @@ This is an Angular **adapter over the official core**, not a second implementati
|
|
|
18
18
|
npm install ngx-json-render @json-render/core zod
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Requires Angular ≥ 20.
|
|
21
|
+
Requires Angular ≥ 20. CI proves both ends of that range on every push: the
|
|
22
|
+
library builds and its suite passes on Angular 20 (the floor) and on Angular
|
|
23
|
+
22 (the current release), as well as on the 21 the workspace itself pins.
|
|
24
|
+
|
|
25
|
+
**Zoneless, and checked as such.** Zone.js appears in no manifest here, `NgZone`
|
|
26
|
+
in no source file, and neither in the published bundles; every suite runs under
|
|
27
|
+
`provideZonelessChangeDetection()`. CI asserts all four on each commit
|
|
28
|
+
(`npm run check:zoneless`), so this is a tested property rather than a
|
|
29
|
+
statement of intent — which matters because zoneless is the default for new
|
|
30
|
+
applications from Angular 21 on.
|
|
31
|
+
|
|
32
|
+
## The shortest path: a ready-made catalog
|
|
33
|
+
|
|
34
|
+
Writing a catalog is the honest first step, but you do not have to take it to
|
|
35
|
+
see a spec render. [`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)
|
|
36
|
+
ships 28 Angular Material components already registered, so a generated spec
|
|
37
|
+
renders with nothing else wired up:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install ngx-json-render-material @angular/material
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Those are real Material components, so the app needs a Material theme and the
|
|
44
|
+
icon font as usual.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { Component, signal } from '@angular/core';
|
|
48
|
+
import { JsonRenderer, type Spec } from 'ngx-json-render';
|
|
49
|
+
import { materialRegistry } from 'ngx-json-render-material';
|
|
50
|
+
|
|
51
|
+
@Component({
|
|
52
|
+
selector: 'app-root',
|
|
53
|
+
imports: [JsonRenderer],
|
|
54
|
+
template: `<json-render [spec]="spec()" [registry]="registry" />`,
|
|
55
|
+
})
|
|
56
|
+
export class App {
|
|
57
|
+
readonly registry = materialRegistry;
|
|
58
|
+
readonly spec = signal<Spec>({
|
|
59
|
+
root: 'card',
|
|
60
|
+
state: { name: '' },
|
|
61
|
+
elements: {
|
|
62
|
+
card: { type: 'Card', props: { title: 'Profile' }, children: ['name', 'hi'] },
|
|
63
|
+
name: { type: 'Input', props: { label: 'Name', value: { $bindState: '/name' } } },
|
|
64
|
+
hi: {
|
|
65
|
+
type: 'Text',
|
|
66
|
+
props: { content: { $template: 'Hello, ${/name}!' } },
|
|
67
|
+
visible: { $state: '/name', neq: '' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`materialCatalog.prompt()` is the system prompt that teaches a model that
|
|
75
|
+
vocabulary. Everything below is the other path — your own components, which is
|
|
76
|
+
what the catalog API is for.
|
|
22
77
|
|
|
23
78
|
## Quick start
|
|
24
79
|
|
|
@@ -83,7 +138,13 @@ export class ButtonComponent {
|
|
|
83
138
|
|
|
84
139
|
```ts
|
|
85
140
|
import { Component, signal } from '@angular/core';
|
|
86
|
-
import {
|
|
141
|
+
import {
|
|
142
|
+
type ActionHandler,
|
|
143
|
+
JsonRenderer,
|
|
144
|
+
type Spec,
|
|
145
|
+
type StateChange,
|
|
146
|
+
defineRegistry,
|
|
147
|
+
} from 'ngx-json-render';
|
|
87
148
|
import { catalog } from './catalog';
|
|
88
149
|
|
|
89
150
|
const { registry } = defineRegistry(catalog, {
|
|
@@ -119,8 +180,10 @@ export class Page {
|
|
|
119
180
|
},
|
|
120
181
|
},
|
|
121
182
|
});
|
|
122
|
-
handlers
|
|
123
|
-
|
|
183
|
+
readonly handlers: Record<string, ActionHandler> = {
|
|
184
|
+
refresh: async () => { /* ... */ },
|
|
185
|
+
};
|
|
186
|
+
onStateChange(changes: StateChange[]) { console.log(changes); }
|
|
124
187
|
}
|
|
125
188
|
```
|
|
126
189
|
|
|
@@ -135,6 +198,9 @@ import { injectUIStream } from 'ngx-json-render';
|
|
|
135
198
|
template: `
|
|
136
199
|
<json-render [spec]="ui.spec()" [registry]="registry" [loading]="ui.isStreaming()" />
|
|
137
200
|
<button (click)="ui.send('A dashboard for weekly sales')">Generate</button>
|
|
201
|
+
@if (ui.isStreaming()) {
|
|
202
|
+
<button (click)="ui.stop()">Stop</button>
|
|
203
|
+
}
|
|
138
204
|
`,
|
|
139
205
|
imports: [JsonRenderer],
|
|
140
206
|
})
|
|
@@ -175,6 +241,61 @@ app.post('/api/generate', async (req, res) => {
|
|
|
175
241
|
|
|
176
242
|
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
243
|
|
|
244
|
+
### Chat + GenUI
|
|
245
|
+
|
|
246
|
+
When the model answers in prose _and_ renders a UI in the same turn, reach for
|
|
247
|
+
`injectChatUI` instead. Its endpoint takes `{ messages }` and streams prose
|
|
248
|
+
mixed with ` ```spec ` fenced JSONL. Each assistant message carries `text`
|
|
249
|
+
and/or its own `spec`, so earlier turns keep the UI they generated:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { Component } from '@angular/core';
|
|
253
|
+
import { JsonRenderer, injectChatUI } from 'ngx-json-render';
|
|
254
|
+
|
|
255
|
+
@Component({
|
|
256
|
+
imports: [JsonRenderer],
|
|
257
|
+
template: `
|
|
258
|
+
@for (m of chat.messages(); track m.id) {
|
|
259
|
+
<p>{{ m.text }}</p>
|
|
260
|
+
@if (m.spec) {
|
|
261
|
+
<json-render [spec]="m.spec" [registry]="registry" />
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
<button (click)="chat.send('show me revenue for the quarter')">Ask</button>
|
|
265
|
+
`,
|
|
266
|
+
})
|
|
267
|
+
export class ChatPage {
|
|
268
|
+
readonly chat = injectChatUI({ api: '/api/chat' });
|
|
269
|
+
readonly registry = registry;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Reading an AI SDK message
|
|
274
|
+
|
|
275
|
+
If the UI arrives as AI SDK data parts rather than through these hooks,
|
|
276
|
+
`buildSpecFromParts` / `getTextFromParts` / `jsonRenderMessage` read a
|
|
277
|
+
`UIMessage.parts` array directly — pass it as it is, no cast:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
readonly msg = jsonRenderMessage(() => this.message().parts);
|
|
281
|
+
// template: {{ msg.text() }} @if (msg.hasSpec()) { <json-render [spec]="msg.spec()" ... /> }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Two things about the parts themselves, both the SDK's semantics rather than
|
|
285
|
+
this package's, and both silent when you get them wrong:
|
|
286
|
+
|
|
287
|
+
- **Do not give patch parts an `id`.** A data part written with one is
|
|
288
|
+
_replaced_ by the next part carrying the same id, so a spec streamed as
|
|
289
|
+
patches under one id arrives as its last patch alone. An id is for a part
|
|
290
|
+
that is a snapshot of itself — a `flat` or `nested` whole-spec part.
|
|
291
|
+
- **A transient part never reaches `message.parts`.** It goes to `onData` and
|
|
292
|
+
nowhere else, so a spec written transiently cannot be rebuilt from the
|
|
293
|
+
message.
|
|
294
|
+
|
|
295
|
+
`projects/ngx-json-render/src/lib/ai-sdk-parts.spec.ts` runs the real SDK —
|
|
296
|
+
writes the chunks a server route would, reads the message a client would —
|
|
297
|
+
and asserts both of these, so this section cannot quietly go stale.
|
|
298
|
+
|
|
178
299
|
### Supplying the transport
|
|
179
300
|
|
|
180
301
|
By default the request goes through the global `fetch`. Pass your own to add
|
|
@@ -193,13 +314,129 @@ readonly ui = injectUIStream({
|
|
|
193
314
|
|
|
194
315
|
`injectChatUI` takes the same option.
|
|
195
316
|
|
|
317
|
+
### Stopping, clearing and refining
|
|
318
|
+
|
|
319
|
+
`stop()` ends the generation in flight and keeps what has already rendered —
|
|
320
|
+
the user changed their mind, so it is not an error and `error` stays null.
|
|
321
|
+
`clear()` stops it too and then resets `spec`, `error`, `usage` and `rawLines`;
|
|
322
|
+
without the stop the request still running would put its spec back on its very
|
|
323
|
+
next patch. Both are no-ops when nothing is streaming, and `injectChatUI` has
|
|
324
|
+
the same pair.
|
|
325
|
+
|
|
326
|
+
To refine a generated UI instead of starting over, hand `send` the spec to
|
|
327
|
+
build on. It is sent to the endpoint as `currentSpec` and the streamed patches
|
|
328
|
+
apply on top of it:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
ui.send('make the chart a bar chart', { previousSpec: ui.spec()! });
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The second argument also carries `context`, forwarded to the endpoint as-is:
|
|
335
|
+
`ui.send(prompt, { context: { locale }, previousSpec })`.
|
|
336
|
+
|
|
196
337
|
Also available:
|
|
197
338
|
|
|
198
|
-
- `
|
|
199
|
-
- `applyPatch(spec, patch)` — immutably apply one RFC 6902 patch to a spec.
|
|
339
|
+
- `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.
|
|
200
340
|
- `buildSpecFromParts` / `getTextFromParts` / `jsonRenderMessage` — derive specs from AI SDK `message.parts`.
|
|
201
341
|
- `catalog.prompt()` / `buildUserPrompt` (from `@json-render/core`) — generate the system/user prompts for your catalog.
|
|
202
342
|
|
|
343
|
+
### Checking what the model produced
|
|
344
|
+
|
|
345
|
+
A generation can end malformed — a child that never arrived, a `visible` the
|
|
346
|
+
model wrote inside `props` where nothing reads it. Opt into a structural check
|
|
347
|
+
with `validate`, on the renderer or on either hook:
|
|
348
|
+
|
|
349
|
+
```html
|
|
350
|
+
<json-render [spec]="ui.spec()" [registry]="registry" [loading]="ui.isStreaming()" validate="warn" />
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
readonly ui = injectUIStream({ api: '/api/generate', validate: 'strict' });
|
|
355
|
+
// ui.issues() — what was wrong with the finished spec
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- `'off'` (the default) — render whatever arrives, as before.
|
|
359
|
+
- `'warn'` — report what is wrong and render anyway.
|
|
360
|
+
- `'strict'` — a spec with errors does not render, and a generation that ends
|
|
361
|
+
with one fails instead of completing, so it never reaches the `onComplete`
|
|
362
|
+
where apps persist it.
|
|
363
|
+
|
|
364
|
+
Both modes first apply the lossless fixes `autoFixSpec` provides: `visible`,
|
|
365
|
+
`on` and `repeat` misplaced inside `props` move back onto the element, where
|
|
366
|
+
they take effect instead of being ignored. Content is never pruned — the lossy
|
|
367
|
+
fixes are left out on purpose, because re-prompting beats a renderer that
|
|
368
|
+
silently deletes elements.
|
|
369
|
+
|
|
370
|
+
The check waits for the spec to settle. While `loading` is true a missing child
|
|
371
|
+
is a patch that has not arrived yet, not a defect, so nothing is reported and
|
|
372
|
+
`strict` keeps rendering; the hooks check once, when the generation completes.
|
|
373
|
+
|
|
374
|
+
## Testing
|
|
375
|
+
|
|
376
|
+
`ngx-json-render/testing` is a separate entry point, so nothing in it can reach
|
|
377
|
+
an application bundle by accident.
|
|
378
|
+
|
|
379
|
+
### Rendering a spec
|
|
380
|
+
|
|
381
|
+
`renderSpec` mounts a spec against a registry and hands back the few things a
|
|
382
|
+
test does to one — no host component, no TestBed module, no settling by hand:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import { renderSpec } from 'ngx-json-render/testing';
|
|
386
|
+
|
|
387
|
+
it('dispatches the action its spec asked for', async () => {
|
|
388
|
+
const ui = await renderSpec(
|
|
389
|
+
{
|
|
390
|
+
root: 'save',
|
|
391
|
+
elements: {
|
|
392
|
+
save: {
|
|
393
|
+
type: 'Button',
|
|
394
|
+
props: { label: 'Save' },
|
|
395
|
+
on: { press: { action: 'save' } },
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{ registry: { Button: MyButton } },
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(ui.text('button')).toBe('Save');
|
|
403
|
+
await ui.click('button');
|
|
404
|
+
expect(ui.dispatched).toEqual([{ name: 'save', params: {} }]);
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
`dispatched` records every action the spec fired, handled or not. The rest of
|
|
409
|
+
the harness: `text` / `texts` / `find` / `findAll` for the DOM, `click` /
|
|
410
|
+
`fill` for input, `read` / `write` / `state` / `changes` for state, `setSpec` /
|
|
411
|
+
`setLoading` / `settle` for later frames, and `fixture` / `element` /
|
|
412
|
+
`renderer` / `store` / `validation` for everything the harness does not cover.
|
|
413
|
+
Options mirror the renderer's inputs, plus `providers` for anything the
|
|
414
|
+
components under test inject.
|
|
415
|
+
|
|
416
|
+
### Replaying a generation
|
|
417
|
+
|
|
418
|
+
`recordedTransport` is a `fetch` that answers from a recording, so a test runs
|
|
419
|
+
the real client — request body, streamed lines, usage metadata, abort on
|
|
420
|
+
supersede — with no server and no API key:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { recordedTransport, specStream, usageLine } from 'ngx-json-render/testing';
|
|
424
|
+
|
|
425
|
+
const ui = injectUIStream({
|
|
426
|
+
api: '/api/generate',
|
|
427
|
+
fetch: recordedTransport([...specStream(expectedSpec), usageLine({ totalTokens: 15 })]),
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`specStream(spec)` writes the JSONL patch lines a model would emit to build
|
|
432
|
+
that spec, so a test says what it renders rather than how the wire spells it.
|
|
433
|
+
Pass a `Record<prompt, lines>` to answer each prompt differently (anything
|
|
434
|
+
else gets a 404 the hook surfaces as an error), or a function for more. The
|
|
435
|
+
options are `delayMs` for a visible pace, `fail` for an error response — as a
|
|
436
|
+
function, so it can be switched on and off between sends — and `promptOf` for
|
|
437
|
+
a request body neither hook sends. The default reads `injectUIStream`'s
|
|
438
|
+
`prompt` and `injectChatUI`'s last message.
|
|
439
|
+
|
|
203
440
|
## Spec features supported
|
|
204
441
|
|
|
205
442
|
Full parity with the baseline json-render contract:
|
|
@@ -213,14 +450,42 @@ Full parity with the baseline json-render contract:
|
|
|
213
450
|
| Computed / directives | `{ "$computed": "fmtDate", "args": {...} }`, custom `$`-directives |
|
|
214
451
|
| Visibility | `"visible": { "$state": "/count", "gte": 5 }` (incl. `$and`/`$or`, `$item`, `$index`) |
|
|
215
452
|
| Events → actions | `"on": { "press": { "action": "...", "params": {...}, "confirm": {...}, "onSuccess": ..., "onError": ... } }` |
|
|
216
|
-
| Built-in actions | `setState`, `pushState` (with `$id`), `removeState`, `push`/`pop`, `validateForm`
|
|
453
|
+
| Built-in actions | `setState`, `pushState` (with `$id`), `removeState`, `push`/`pop`, `validateForm`, `submitForm` |
|
|
217
454
|
| Repeat | `"repeat": { "statePath": "/todos", "key": "id" }`, nested via `{ "$item": "..." }` |
|
|
218
455
|
| Watch | `"watch": { "/country": { "action": "loadCities" } }` |
|
|
219
456
|
| Slots | `"slots": { "header": ["title-el"] }` + `<jr-children slot="header" />` |
|
|
220
|
-
| Validation | field checks via `ValidationConfig`, `validateForm`, `injectFieldValidation`
|
|
457
|
+
| Validation | field checks via `ValidationConfig`, `validateForm` / `submitForm`, `injectFieldValidation` |
|
|
221
458
|
| Confirm dialogs | built-in `<jr-confirm-dialog>` (auto-rendered) |
|
|
222
459
|
| Devtools hooks | action observer + `data-jr-key` picker attributes |
|
|
223
460
|
|
|
461
|
+
### Ready-made directives
|
|
462
|
+
|
|
463
|
+
`@json-render/directives` is written against the core, not against any one
|
|
464
|
+
renderer, so its `$format`, `$math`, `$concat`, `$count`, `$truncate`,
|
|
465
|
+
`$pluralize`, `$join` and `$t` work here as they are:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
import { createI18nDirective, standardDirectives } from '@json-render/directives';
|
|
469
|
+
|
|
470
|
+
readonly directives = [
|
|
471
|
+
...standardDirectives,
|
|
472
|
+
createI18nDirective({ locale, messages }),
|
|
473
|
+
];
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
```html
|
|
477
|
+
<json-render [spec]="spec()" [registry]="registry" [directives]="directives" />
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
```json
|
|
481
|
+
{ "content": { "$format": "currency", "value": { "$state": "/price" }, "currency": "USD" } }
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
A directive reads state through the same resolution as any other prop, so a
|
|
485
|
+
value it derives updates when that state does.
|
|
486
|
+
`projects/ngx-json-render/src/lib/directives.spec.ts` renders every one of
|
|
487
|
+
them through the renderer, which is what keeps this paragraph honest.
|
|
488
|
+
|
|
224
489
|
## State
|
|
225
490
|
|
|
226
491
|
Each `<json-render>` owns a state store (JSON Pointer addressed). Seeding order: `store` input (controlled) → `state` input → `spec.state`.
|
|
@@ -239,6 +504,78 @@ Share one store across renderers (or drive it from your own state management) by
|
|
|
239
504
|
|
|
240
505
|
If a catalog component renders `<input [value]="ctx.props().value">`, remember that one-way bindings do not re-assert the DOM when the bound value returns to its previously applied value while the user typed in between (e.g. `pushState` + `clearStatePath`). Sync imperatively instead — see `InputComponent` in the demo app for the pattern.
|
|
241
506
|
|
|
507
|
+
## Confirmation dialogs
|
|
508
|
+
|
|
509
|
+
An action binding with a `confirm` field routes through a dialog before its
|
|
510
|
+
handler runs. The packaged one is a real modal — `role="dialog"` with
|
|
511
|
+
`aria-modal`, labelled by its title and described by its message, focus moved
|
|
512
|
+
onto _Cancel_ on open and returned to the trigger on close, Tab kept inside and
|
|
513
|
+
Escape cancelling.
|
|
514
|
+
|
|
515
|
+
Three levels of control, in the order you are likely to need them:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
// 1. Its two words, when the spec does not supply confirmLabel / cancelLabel.
|
|
519
|
+
{ provide: JR_CONFIRM_LABELS, useValue: { confirm: 'Подтвердить', cancel: 'Отмена' } }
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
```css
|
|
523
|
+
/* 2. Its colours. Light and dark defaults ship; these override both. */
|
|
524
|
+
json-render { --jr-confirm-surface: #101418; --jr-confirm-accent: #4f9cf9; }
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Also `--jr-confirm-ink`, `--jr-confirm-muted`, `--jr-confirm-border`,
|
|
528
|
+
`--jr-confirm-scrim`, `--jr-confirm-danger`, `--jr-confirm-on-accent` and
|
|
529
|
+
`--jr-confirm-radius`.
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
// 3. The whole dialog. Your component injects the context instead of taking
|
|
533
|
+
// inputs, the same way catalog components do.
|
|
534
|
+
@Component({
|
|
535
|
+
template: `<my-modal [title]="ctx.config.title" (ok)="ctx.confirm()" (dismiss)="ctx.cancel()" />`,
|
|
536
|
+
})
|
|
537
|
+
export class AppConfirm {
|
|
538
|
+
readonly ctx = injectConfirmContext();
|
|
539
|
+
}
|
|
540
|
+
// providers: [{ provide: JR_CONFIRM_DIALOG, useValue: AppConfirm }]
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Submitting a form
|
|
544
|
+
|
|
545
|
+
`validateForm` validates every bound field at once and writes
|
|
546
|
+
`{ valid, errors }` to `/formValidation` (or the `statePath` you pass). What it
|
|
547
|
+
cannot do is act on the answer: a model writing a submit button had to emit one
|
|
548
|
+
binding for the validation and hope the app's own handler re-checked the form.
|
|
549
|
+
|
|
550
|
+
`submitForm` is both halves in one binding — validate everything, and dispatch
|
|
551
|
+
the submit only if it all passes:
|
|
552
|
+
|
|
553
|
+
```json
|
|
554
|
+
{
|
|
555
|
+
"type": "Button",
|
|
556
|
+
"props": { "label": "Save" },
|
|
557
|
+
"on": {
|
|
558
|
+
"press": {
|
|
559
|
+
"action": "submitForm",
|
|
560
|
+
"params": {
|
|
561
|
+
"action": "saveUser",
|
|
562
|
+
"params": { "email": { "$state": "/email" } }
|
|
563
|
+
},
|
|
564
|
+
"onSuccess": { "navigate": "/thanks" }
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
"children": []
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
An invalid form stops there, with the errors written to state and every field
|
|
572
|
+
marked validated so its message is on screen. A valid one dispatches
|
|
573
|
+
`saveUser` as an ordinary action: the same handler lookup, the same `confirm`
|
|
574
|
+
(asked after validation — there is no point asking about a form that cannot be
|
|
575
|
+
submitted), the same `onSuccess` / `onError`, the same loading state. `params`
|
|
576
|
+
resolves `{ "$state": "/path" }` one level down, the way `pushState`'s `value`
|
|
577
|
+
does.
|
|
578
|
+
|
|
242
579
|
## Security
|
|
243
580
|
|
|
244
581
|
Specs are attacker-shaped input: whatever produced one — a model, a prompt, a
|
|
@@ -255,9 +592,12 @@ text reaches the DOM through Angular interpolation. Script injection through a
|
|
|
255
592
|
spec is not a thing you have to defend against.
|
|
256
593
|
|
|
257
594
|
**A spec can only name actions you registered.** Built-ins (`setState`,
|
|
258
|
-
`pushState`, `removeState`, `push`, `pop`, `validateForm`) are
|
|
259
|
-
the renderer; every other action name is looked up in the
|
|
260
|
-
An unrecognised name logs a warning and does nothing.
|
|
595
|
+
`pushState`, `removeState`, `push`, `pop`, `validateForm`, `submitForm`) are
|
|
596
|
+
handled inside the renderer; every other action name is looked up in the
|
|
597
|
+
`handlers` you pass. An unrecognised name logs a warning and does nothing.
|
|
598
|
+
`submitForm` is no exception to this: the action it is told to submit goes
|
|
599
|
+
through the same lookup, so it gates a handler you registered rather than
|
|
600
|
+
reaching one you did not.
|
|
261
601
|
|
|
262
602
|
The exception is `onAction`, which is a deliberate catch-all: when you pass it,
|
|
263
603
|
**every** action name in the spec reaches it, including ones you never put in
|
|
@@ -291,10 +631,18 @@ handler, on the server.
|
|
|
291
631
|
|
|
292
632
|
Components: `JsonRenderer` (`<json-render>`), `JrChildren`, `JrConfirmDialog`, `JrElement`, `JrRepeatScope`.
|
|
293
633
|
|
|
294
|
-
Injectables/helpers: `injectRenderContext`, `injectRepeatScope`, `injectStateStore`, `injectStateValue`, `injectStateBinding`, `injectBoundProp`, `injectActions`, `injectAction`, `injectValidation`, `injectFieldValidation`, `injectUIStream`, `injectChatUI`, `injectDevtoolsActive`, `jsonRenderMessage`.
|
|
634
|
+
Injectables/helpers: `injectRenderContext`, `injectElementKey`, `injectRepeatScope`, `injectStateStore`, `injectStateValue`, `injectStateBinding`, `injectBoundProp`, `injectActions`, `injectAction`, `injectValidation`, `injectFieldValidation`, `injectUIStream`, `injectChatUI`, `injectDevtoolsActive`, `injectConfirmContext`, `jsonRenderMessage`, `isActionCancelled`, `checkSpec`.
|
|
635
|
+
|
|
636
|
+
Tokens: `JR_CONFIRM_DIALOG` (replace the confirmation dialog), `JR_CONFIRM_LABELS` (its two words), `CONFIRM_CONTEXT`, `RENDER_CONTEXT`, `REPEAT_SCOPE`.
|
|
637
|
+
|
|
638
|
+
`injectActions().execute()` rejects when the user dismisses a `confirm`
|
|
639
|
+
dialog, which is a normal gesture rather than a failure — `isActionCancelled(error)`
|
|
640
|
+
is how you tell the two apart.
|
|
295
641
|
|
|
296
642
|
Registry & schema: `defineRegistry`, `createStoreSetState`, `schema`.
|
|
297
643
|
|
|
644
|
+
Testing (`ngx-json-render/testing`): `renderSpec`, `recordedTransport`, `specStream`, `usageLine`.
|
|
645
|
+
|
|
298
646
|
Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`, prompt builders, spec validators, SpecStream compiler) composes with this package; the most common symbols are re-exported.
|
|
299
647
|
|
|
300
648
|
## Renderer inputs
|
|
@@ -305,6 +653,7 @@ Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`,
|
|
|
305
653
|
| `registry` | `ComponentRegistry` | Catalog type → Angular component |
|
|
306
654
|
| `loading` | `boolean` | Suppress missing-element warnings while streaming |
|
|
307
655
|
| `fallback` | `Type<unknown>` | Component for unknown types |
|
|
656
|
+
| `validate` | `'off' \| 'warn' \| 'strict'` | Check the settled spec's structure (default `'off'`) |
|
|
308
657
|
| `state` | `StateModel` | Initial state (uncontrolled; defaults to `spec.state`) |
|
|
309
658
|
| `store` | `StateStore` | External store (controlled mode) |
|
|
310
659
|
| `handlers` | `Record<string, ActionHandler>` | Action handlers |
|