ngx-json-render 0.9.0 → 0.9.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 +62 -4
- package/fesm2022/ngx-json-render-router.mjs +71 -0
- package/fesm2022/ngx-json-render-router.mjs.map +1 -0
- package/fesm2022/ngx-json-render.mjs +146 -21
- package/fesm2022/ngx-json-render.mjs.map +1 -1
- package/package.json +10 -2
- package/types/ngx-json-render-router.d.ts +39 -0
- package/types/ngx-json-render.d.ts +75 -7
package/README.md
CHANGED
|
@@ -250,6 +250,38 @@ export class Page {
|
|
|
250
250
|
}
|
|
251
251
|
```
|
|
252
252
|
|
|
253
|
+
### Defaults for the whole app
|
|
254
|
+
|
|
255
|
+
A chat renders a spec in every message, and binding the same registry and handlers on each one gets old. `provideJsonRender` sets them once:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
bootstrapApplication(App, {
|
|
259
|
+
providers: [
|
|
260
|
+
provideJsonRender(() => {
|
|
261
|
+
const orders = inject(OrderService); // the function form runs in an injection context
|
|
262
|
+
return {
|
|
263
|
+
registry,
|
|
264
|
+
catalog,
|
|
265
|
+
handlers: { cancelOrder: (params) => orders.cancel(params['id']) },
|
|
266
|
+
};
|
|
267
|
+
}),
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```html
|
|
273
|
+
<json-render [spec]="m.spec" />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Every option input can be provided: `registry`, `fallback`, `validate`, `renderLimits`, `catalog`, `handlers`, `onAction`, `navigate`, `validationFunctions`, `functions` and `directives`. Per-instance inputs (`spec`, `loading`, `state`, `store`) cannot. The rules:
|
|
277
|
+
|
|
278
|
+
- An input bound on the element wins over the provided value. One left `null` or `undefined` falls through to it.
|
|
279
|
+
- `handlers`, `functions`, `validationFunctions` and `directives` merge by name instead, the element's entry winning a name both define. A chat can provide app-wide handlers and still add one for a single message.
|
|
280
|
+
- Everything else replaces. A `registry` belongs to its catalog, so two are never mixed behind your back; to combine them, spread them yourself (`{ ...registry, ...extraRegistry }`).
|
|
281
|
+
- It works in application, route and component `providers`. A provider below another extends it by the same rules.
|
|
282
|
+
|
|
283
|
+
With no registry bound and none provided, the renderer throws, naming both ways to supply one.
|
|
284
|
+
|
|
253
285
|
## Streaming a UI from an LLM
|
|
254
286
|
|
|
255
287
|
Specs stream as JSONL patch lines (RFC 6902). Render partial specs as they arrive — the renderer tolerates missing elements while `loading` is true:
|
|
@@ -391,15 +423,16 @@ import { JsonRenderer, injectChatUI } from 'ngx-json-render';
|
|
|
391
423
|
@for (m of chat.messages(); track m.id) {
|
|
392
424
|
<p>{{ m.text }}</p>
|
|
393
425
|
@if (m.spec) {
|
|
394
|
-
<json-render [spec]="m.spec"
|
|
426
|
+
<json-render [spec]="m.spec" />
|
|
395
427
|
}
|
|
396
428
|
}
|
|
397
429
|
<button (click)="chat.send('show me revenue for the quarter')">Ask</button>
|
|
398
430
|
`,
|
|
399
431
|
})
|
|
400
432
|
export class ChatPage {
|
|
433
|
+
// The registry comes from provideJsonRender({ registry }) in the app's
|
|
434
|
+
// providers: see "Defaults for the whole app".
|
|
401
435
|
readonly chat = injectChatUI({ api: '/api/chat' });
|
|
402
|
-
readonly registry = registry;
|
|
403
436
|
}
|
|
404
437
|
```
|
|
405
438
|
|
|
@@ -965,7 +998,26 @@ rather than the one holding session, entitlement or billing state.
|
|
|
965
998
|
**A spec chooses the navigation target.** `onSuccess: { navigate }` passes its
|
|
966
999
|
string to the `navigate` callback you provide, verbatim. Treat it as untrusted:
|
|
967
1000
|
match it against known routes, and never hand it to `window.location` or
|
|
968
|
-
`router.navigateByUrl` unchecked.
|
|
1001
|
+
`router.navigateByUrl` unchecked. `ngx-json-render/router` does the checking
|
|
1002
|
+
for an app on the Angular `Router`:
|
|
1003
|
+
|
|
1004
|
+
```ts
|
|
1005
|
+
import { injectRouterNavigate } from 'ngx-json-render/router';
|
|
1006
|
+
|
|
1007
|
+
provideJsonRender(() => ({
|
|
1008
|
+
registry,
|
|
1009
|
+
navigate: injectRouterNavigate({ allow: ['/thanks', /^\/orders\/\d+$/] }),
|
|
1010
|
+
}));
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
It passes a path to `router.navigateByUrl` only when `allow` lets it through
|
|
1014
|
+
(strings match the path exactly, before any `?` or `#`; a `RegExp` is tested
|
|
1015
|
+
against it; a function decides). `allow` is required, because only the app
|
|
1016
|
+
knows which of its routes a generated UI may open. Whatever `allow` says, a
|
|
1017
|
+
path that does not start with a single `/` is refused: no other origin, no
|
|
1018
|
+
`javascript:` or other scheme, no relative path, no backslash or control
|
|
1019
|
+
character. A refused path, or a navigation the router rejects, is reported
|
|
1020
|
+
with `console.warn` and goes nowhere.
|
|
969
1021
|
|
|
970
1022
|
**A spec cannot render forever.** Two elements naming each other as children,
|
|
971
1023
|
or one naming itself, would recurse until the tab died. The renderer refuses to
|
|
@@ -1013,6 +1065,8 @@ Components: `JsonRenderer` (`<json-render>`), `JrChildren`, `JrConfirmDialog`, `
|
|
|
1013
1065
|
|
|
1014
1066
|
Injectables/helpers: `injectRenderContext`, `injectElementKey`, `injectRepeatScope`, `injectStateStore`, `injectStateValue`, `injectStateBinding`, `injectBoundProp`, `injectActions`, `injectAction`, `injectValidation`, `injectFieldValidation`, `injectUIStream`, `injectChatUI`, `injectDevtoolsActive`, `injectConfirmContext`, `jsonRenderMessage`, `isActionCancelled`, `checkSpec`.
|
|
1015
1067
|
|
|
1068
|
+
App-wide defaults: `provideJsonRender`, the `JSON_RENDER_CONFIG` token it fills, and the type `JsonRenderConfig`.
|
|
1069
|
+
|
|
1016
1070
|
Tokens: `JR_CONFIRM_DIALOG` (replace the confirmation dialog), `JR_CONFIRM_LABELS` (its two words), `CONFIRM_CONTEXT`, `RENDER_CONTEXT`, `REPEAT_SCOPE`.
|
|
1017
1071
|
|
|
1018
1072
|
Spec checking: `checkSpec`, `formatSpecCheckIssues`, and the types
|
|
@@ -1031,6 +1085,8 @@ Schema alone (`ngx-json-render/schema`): `schema`, `AngularSchema`, `AngularSpec
|
|
|
1031
1085
|
|
|
1032
1086
|
MCP Apps (`ngx-json-render/mcp`): `injectJsonRenderApp`, `parseSpecFromToolResult`, `messageText`, and the types `JsonRenderApp`, `JsonRenderAppOptions`. Needs the optional peers `@modelcontextprotocol/ext-apps` and `@modelcontextprotocol/sdk`.
|
|
1033
1087
|
|
|
1088
|
+
Router (`ngx-json-render/router`): `injectRouterNavigate`, and the type `RouterNavigateOptions`. Needs the optional peer `@angular/router`, which an Angular app on the router already has.
|
|
1089
|
+
|
|
1034
1090
|
Devtools (`ngx-json-render/devtools`): `JsonRenderDevtools` (`<json-render-devtools>`), and the types `DevtoolsEvent`, `PanelPosition`. Needs the optional peer `@json-render/devtools`.
|
|
1035
1091
|
|
|
1036
1092
|
Testing (`ngx-json-render/testing`): `renderSpec`, `renderComponent`, `recordedTransport`, `specStream`, `usageLine`.
|
|
@@ -1042,7 +1098,7 @@ Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`,
|
|
|
1042
1098
|
| Input | Type | Purpose |
|
|
1043
1099
|
| --------------------- | ------------------------------------ | ------------------------------------------------------- |
|
|
1044
1100
|
| `spec` | `Spec \| null` | The UI spec (may be partial while streaming) |
|
|
1045
|
-
| `registry` | `ComponentRegistry` | Catalog type →
|
|
1101
|
+
| `registry` | `ComponentRegistry` | Catalog type → component; required unless provided |
|
|
1046
1102
|
| `loading` | `boolean` | Suppress missing-element warnings while streaming |
|
|
1047
1103
|
| `fallback` | `Type<unknown>` | Component for unknown types |
|
|
1048
1104
|
| `validate` | `'off' \| 'warn' \| 'strict'` | Check the settled spec's structure (default `'off'`) |
|
|
@@ -1059,6 +1115,8 @@ Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`,
|
|
|
1059
1115
|
|
|
1060
1116
|
Output: `(stateChange)` — batched `{ path, value }[]` in uncontrolled mode.
|
|
1061
1117
|
|
|
1118
|
+
Every input but `spec`, `loading`, `state` and `store` can come from `provideJsonRender` instead; see [Defaults for the whole app](#defaults-for-the-whole-app).
|
|
1119
|
+
|
|
1062
1120
|
## License
|
|
1063
1121
|
|
|
1064
1122
|
Apache-2.0
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { inject } from '@angular/core';
|
|
2
|
+
import { Router } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A `navigate` for `<json-render>` that hands a spec's
|
|
6
|
+
* `onSuccess: { navigate }` path to the Angular `Router`, but only a path
|
|
7
|
+
* inside the app that `allow` lets through. Anything else is refused with a
|
|
8
|
+
* warning and goes nowhere.
|
|
9
|
+
*
|
|
10
|
+
* Call it in an injection context — in the function form of
|
|
11
|
+
* `provideJsonRender`, or in a field initializer:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* provideJsonRender(() => ({
|
|
15
|
+
* registry,
|
|
16
|
+
* navigate: injectRouterNavigate({ allow: ['/thanks', /^\/orders\/\d+$/] }),
|
|
17
|
+
* }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Whatever `allow` says, a path is refused unless it starts with a single
|
|
21
|
+
* `/`: no other origin (`https://…`, `//host`), no scheme (`javascript:…`),
|
|
22
|
+
* no path relative to wherever the user happens to be. A backslash or a
|
|
23
|
+
* control character refuses it too, since browsers read `/\host` as
|
|
24
|
+
* `//host`.
|
|
25
|
+
*/
|
|
26
|
+
function injectRouterNavigate(options) {
|
|
27
|
+
const router = inject(Router);
|
|
28
|
+
const allowed = toPredicate(options.allow);
|
|
29
|
+
return (path) => {
|
|
30
|
+
if (!isAppPath(path)) {
|
|
31
|
+
console.warn(`[ngx-json-render] Refused to navigate to ${JSON.stringify(path)}: a spec may only navigate to a path inside the app, starting with a single "/".`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const pathname = path.split(/[?#]/, 1)[0];
|
|
35
|
+
if (!allowed(pathname)) {
|
|
36
|
+
console.warn(`[ngx-json-render] Refused to navigate to "${path}": "${pathname}" is not in the allow list given to injectRouterNavigate.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
router.navigateByUrl(path).catch((error) => {
|
|
40
|
+
console.warn(`[ngx-json-render] Navigation to "${path}" failed.`, error);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isAppPath(path) {
|
|
45
|
+
return (typeof path === 'string' &&
|
|
46
|
+
/^\/(?![/\\])/.test(path) &&
|
|
47
|
+
!/[\\\u0000-\u001f\u007f]/.test(path));
|
|
48
|
+
}
|
|
49
|
+
function toPredicate(allow) {
|
|
50
|
+
if (typeof allow === 'function')
|
|
51
|
+
return allow;
|
|
52
|
+
return (path) => allow.some((rule) => {
|
|
53
|
+
if (typeof rule === 'string')
|
|
54
|
+
return rule === path;
|
|
55
|
+
// A global or sticky RegExp keeps `lastIndex` between calls; reset it so
|
|
56
|
+
// one rule answers the same way for the same path every time.
|
|
57
|
+
rule.lastIndex = 0;
|
|
58
|
+
return rule.test(path);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Public API Surface of ngx-json-render/router
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generated bundle index. Do not edit.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
export { injectRouterNavigate };
|
|
71
|
+
//# sourceMappingURL=ngx-json-render-router.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-json-render-router.mjs","sources":["../../../projects/ngx-json-render/router/src/router-navigate.ts","../../../projects/ngx-json-render/router/src/public-api.ts","../../../projects/ngx-json-render/router/src/ngx-json-render-router.ts"],"sourcesContent":["import { inject } from '@angular/core';\nimport { Router } from '@angular/router';\n\n/** Which paths a spec may send the app to. */\nexport interface RouterNavigateOptions {\n /**\n * The paths a spec may navigate to. A string must equal the path exactly; a\n * `RegExp` is tested against it; a function decides on its own. The path is\n * what comes before any `?` or `#`, so a query string or fragment neither\n * helps nor hurts a match, and is passed on to the router as written.\n *\n * Required: a spec is model output, and the app is what knows which of its\n * routes a generated UI has any business opening.\n */\n readonly allow: readonly (string | RegExp)[] | ((path: string) => boolean);\n}\n\n/**\n * A `navigate` for `<json-render>` that hands a spec's\n * `onSuccess: { navigate }` path to the Angular `Router`, but only a path\n * inside the app that `allow` lets through. Anything else is refused with a\n * warning and goes nowhere.\n *\n * Call it in an injection context — in the function form of\n * `provideJsonRender`, or in a field initializer:\n *\n * ```ts\n * provideJsonRender(() => ({\n * registry,\n * navigate: injectRouterNavigate({ allow: ['/thanks', /^\\/orders\\/\\d+$/] }),\n * }));\n * ```\n *\n * Whatever `allow` says, a path is refused unless it starts with a single\n * `/`: no other origin (`https://…`, `//host`), no scheme (`javascript:…`),\n * no path relative to wherever the user happens to be. A backslash or a\n * control character refuses it too, since browsers read `/\\host` as\n * `//host`.\n */\nexport function injectRouterNavigate(\n options: RouterNavigateOptions,\n): (path: string) => void {\n const router = inject(Router);\n const allowed = toPredicate(options.allow);\n return (path) => {\n if (!isAppPath(path)) {\n console.warn(\n `[ngx-json-render] Refused to navigate to ${JSON.stringify(path)}: a spec may only navigate to a path inside the app, starting with a single \"/\".`,\n );\n return;\n }\n const pathname = path.split(/[?#]/, 1)[0];\n if (!allowed(pathname)) {\n console.warn(\n `[ngx-json-render] Refused to navigate to \"${path}\": \"${pathname}\" is not in the allow list given to injectRouterNavigate.`,\n );\n return;\n }\n router.navigateByUrl(path).catch((error: unknown) => {\n console.warn(`[ngx-json-render] Navigation to \"${path}\" failed.`, error);\n });\n };\n}\n\nfunction isAppPath(path: unknown): path is string {\n return (\n typeof path === 'string' &&\n /^\\/(?![/\\\\])/.test(path) &&\n !/[\\\\\\u0000-\\u001f\\u007f]/.test(path)\n );\n}\n\nfunction toPredicate(\n allow: RouterNavigateOptions['allow'],\n): (path: string) => boolean {\n if (typeof allow === 'function') return allow;\n return (path) =>\n allow.some((rule) => {\n if (typeof rule === 'string') return rule === path;\n // A global or sticky RegExp keeps `lastIndex` between calls; reset it so\n // one rule answers the same way for the same path every time.\n rule.lastIndex = 0;\n return rule.test(path);\n });\n}\n","/*\n * Public API Surface of ngx-json-render/router\n */\n\nexport {\n injectRouterNavigate,\n type RouterNavigateOptions,\n} from './router-navigate';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;AAiBA;;;;;;;;;;;;;;;;;;;;;AAqBG;AACG,SAAU,oBAAoB,CAClC,OAA8B,EAAA;AAE9B,IAAA,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;IAC1C,OAAO,CAAC,IAAI,KAAI;AACd,QAAA,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;AACpB,YAAA,OAAO,CAAC,IAAI,CACV,CAAA,yCAAA,EAA4C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA,gFAAA,CAAkF,CACnJ;YACD;QACF;AACA,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACzC,QAAA,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;YACtB,OAAO,CAAC,IAAI,CACV,CAAA,0CAAA,EAA6C,IAAI,CAAA,IAAA,EAAO,QAAQ,CAAA,yDAAA,CAA2D,CAC5H;YACD;QACF;QACA,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,KAAI;YAClD,OAAO,CAAC,IAAI,CAAC,CAAA,iCAAA,EAAoC,IAAI,CAAA,SAAA,CAAW,EAAE,KAAK,CAAC;AAC1E,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC;AACH;AAEA,SAAS,SAAS,CAAC,IAAa,EAAA;AAC9B,IAAA,QACE,OAAO,IAAI,KAAK,QAAQ;AACxB,QAAA,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;AACzB,QAAA,CAAC,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC;AAEzC;AAEA,SAAS,WAAW,CAClB,KAAqC,EAAA;IAErC,IAAI,OAAO,KAAK,KAAK,UAAU;AAAE,QAAA,OAAO,KAAK;AAC7C,IAAA,OAAO,CAAC,IAAI,KACV,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,KAAI;QAClB,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,KAAK,IAAI;;;AAGlD,QAAA,IAAI,CAAC,SAAS,GAAG,CAAC;AAClB,QAAA,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACxB,IAAA,CAAC,CAAC;AACN;;ACpFA;;AAEG;;ACFH;;AAEG;;"}
|
|
@@ -2377,6 +2377,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2377
2377
|
`, styles: [":host{display:contents}\n"] }]
|
|
2378
2378
|
}], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
|
|
2379
2379
|
|
|
2380
|
+
/**
|
|
2381
|
+
* The defaults the nearest {@link provideJsonRender} supplies, already merged
|
|
2382
|
+
* with any provider above it.
|
|
2383
|
+
*/
|
|
2384
|
+
const JSON_RENDER_CONFIG = new InjectionToken('JSON_RENDER_CONFIG');
|
|
2385
|
+
/**
|
|
2386
|
+
* Set renderer defaults once instead of on every `<json-render>`:
|
|
2387
|
+
*
|
|
2388
|
+
* ```ts
|
|
2389
|
+
* bootstrapApplication(App, { providers: [provideJsonRender({ registry })] });
|
|
2390
|
+
* ```
|
|
2391
|
+
*
|
|
2392
|
+
* ```html
|
|
2393
|
+
* <json-render [spec]="m.spec" />
|
|
2394
|
+
* ```
|
|
2395
|
+
*
|
|
2396
|
+
* Pass a function to build the defaults with `inject()`, for handlers that
|
|
2397
|
+
* call your services:
|
|
2398
|
+
*
|
|
2399
|
+
* ```ts
|
|
2400
|
+
* provideJsonRender(() => {
|
|
2401
|
+
* const orders = inject(OrderService);
|
|
2402
|
+
* return { registry, handlers: { cancelOrder: (p) => orders.cancel(p['id']) } };
|
|
2403
|
+
* });
|
|
2404
|
+
* ```
|
|
2405
|
+
*
|
|
2406
|
+
* Works in application, route and component `providers`. A provider below
|
|
2407
|
+
* another extends it with the same rules an element input does: its own
|
|
2408
|
+
* values replace, its named vocabularies merge.
|
|
2409
|
+
*/
|
|
2410
|
+
function provideJsonRender(config) {
|
|
2411
|
+
return [
|
|
2412
|
+
{
|
|
2413
|
+
provide: JSON_RENDER_CONFIG,
|
|
2414
|
+
useFactory: () => {
|
|
2415
|
+
const outer = inject(JSON_RENDER_CONFIG, {
|
|
2416
|
+
optional: true,
|
|
2417
|
+
skipSelf: true,
|
|
2418
|
+
});
|
|
2419
|
+
const own = typeof config === 'function' ? config() : config;
|
|
2420
|
+
return outer ? mergeJsonRenderConfig(outer, own) : own;
|
|
2421
|
+
},
|
|
2422
|
+
},
|
|
2423
|
+
];
|
|
2424
|
+
}
|
|
2425
|
+
/** `inner` over `outer`, by the rules {@link JsonRenderConfig} describes. */
|
|
2426
|
+
function mergeJsonRenderConfig(outer, inner) {
|
|
2427
|
+
const merged = { ...outer };
|
|
2428
|
+
for (const [key, value] of Object.entries(inner)) {
|
|
2429
|
+
if (value !== undefined && value !== null) {
|
|
2430
|
+
merged[key] = value;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
merged.handlers = mergeByName(outer.handlers, inner.handlers);
|
|
2434
|
+
merged.functions = mergeByName(outer.functions, inner.functions);
|
|
2435
|
+
merged.validationFunctions = mergeByName(outer.validationFunctions, inner.validationFunctions);
|
|
2436
|
+
merged.directives = mergeDirectives(outer.directives, inner.directives);
|
|
2437
|
+
return merged;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Two name → value maps as one, `inner` winning a name both define. Returns
|
|
2441
|
+
* whichever is given when only one is, so an unmerged map keeps its identity.
|
|
2442
|
+
*
|
|
2443
|
+
* @internal
|
|
2444
|
+
*/
|
|
2445
|
+
function mergeByName(outer, inner) {
|
|
2446
|
+
if (!outer)
|
|
2447
|
+
return inner ?? undefined;
|
|
2448
|
+
if (!inner)
|
|
2449
|
+
return outer;
|
|
2450
|
+
return { ...outer, ...inner };
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Two directive lists as one, `inner` winning a name both define — the
|
|
2454
|
+
* directive registry keeps the last definition of a name.
|
|
2455
|
+
*
|
|
2456
|
+
* @internal
|
|
2457
|
+
*/
|
|
2458
|
+
function mergeDirectives(outer, inner) {
|
|
2459
|
+
if (!outer)
|
|
2460
|
+
return inner ?? undefined;
|
|
2461
|
+
if (!inner)
|
|
2462
|
+
return outer;
|
|
2463
|
+
return [...outer, ...inner];
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2380
2466
|
const CLEAN = {
|
|
2381
2467
|
spec: null,
|
|
2382
2468
|
issues: [],
|
|
@@ -2526,8 +2612,11 @@ function reportSpecCheck(check, mode) {
|
|
|
2526
2612
|
class JsonRenderer {
|
|
2527
2613
|
/** The UI spec to render (may be partial while streaming). */
|
|
2528
2614
|
spec = input.required(...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
|
|
2529
|
-
/**
|
|
2530
|
-
|
|
2615
|
+
/**
|
|
2616
|
+
* Component registry mapping catalog type names to Angular components.
|
|
2617
|
+
* Required unless a {@link provideJsonRender} above supplies one.
|
|
2618
|
+
*/
|
|
2619
|
+
registry = input(undefined, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
|
|
2531
2620
|
/** Whether the spec is currently loading/streaming. */
|
|
2532
2621
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2533
2622
|
/** Fallback component for unknown types. */
|
|
@@ -2544,7 +2633,7 @@ class JsonRenderer {
|
|
|
2544
2633
|
* The check is skipped while `loading` is true: a spec that is still
|
|
2545
2634
|
* arriving is expected to reference children that have not streamed in yet.
|
|
2546
2635
|
*/
|
|
2547
|
-
validate = input(
|
|
2636
|
+
validate = input(undefined, ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
|
|
2548
2637
|
/**
|
|
2549
2638
|
* Caps on what the spec may cost the browser: `maxElements`, `maxDepth` and
|
|
2550
2639
|
* `maxRepeatItems`. Unset by default, and each field is independently
|
|
@@ -2601,34 +2690,52 @@ class JsonRenderer {
|
|
|
2601
2690
|
stateStore;
|
|
2602
2691
|
/** The action dispatcher of this renderer. */
|
|
2603
2692
|
actions;
|
|
2693
|
+
/**
|
|
2694
|
+
* The nearest {@link provideJsonRender}'s defaults. Every option input
|
|
2695
|
+
* falls through to these when left `null` or `undefined`; see
|
|
2696
|
+
* {@link JsonRenderConfig} for the merge rules.
|
|
2697
|
+
*/
|
|
2698
|
+
config = inject(JSON_RENDER_CONFIG, { optional: true }) ?? {};
|
|
2699
|
+
mode = computed(() => this.validate() ?? this.config.validate ?? 'off', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
2700
|
+
limits = computed(() => this.renderLimits() ?? this.config.renderLimits ?? null, ...(ngDevMode ? [{ debugName: "limits" }] : /* istanbul ignore next */ []));
|
|
2701
|
+
specCatalog = computed(() => this.catalog() ?? this.config.catalog ?? null, ...(ngDevMode ? [{ debugName: "specCatalog" }] : /* istanbul ignore next */ []));
|
|
2604
2702
|
/**
|
|
2605
2703
|
* The spec as checked, and what the check found. While `loading`, the mode
|
|
2606
2704
|
* is forced off: the spec is still arriving, and the missing children it
|
|
2607
2705
|
* refers to are the normal state of a stream, not a defect.
|
|
2608
2706
|
*/
|
|
2609
|
-
checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.
|
|
2610
|
-
limits: this.
|
|
2611
|
-
catalog: this.
|
|
2707
|
+
checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.mode(), {
|
|
2708
|
+
limits: this.limits(),
|
|
2709
|
+
catalog: this.specCatalog(),
|
|
2612
2710
|
}), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
|
|
2613
2711
|
constructor() {
|
|
2614
2712
|
const root = inject(JsonRenderRootContext);
|
|
2615
2713
|
// The fixed spec, not the input: a `visible` the check moved out of
|
|
2616
2714
|
// `props` has to be the one the tree renders, or the fix is cosmetic.
|
|
2617
2715
|
root.spec = computed(() => this.checked().spec, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
|
|
2618
|
-
|
|
2716
|
+
const config = this.config;
|
|
2717
|
+
// Read only once an element needs a component, like a required input:
|
|
2718
|
+
// a renderer with no spec yet has no use for one.
|
|
2719
|
+
root.registry = computed(() => {
|
|
2720
|
+
const registry = this.registry() ?? config.registry;
|
|
2721
|
+
if (!registry) {
|
|
2722
|
+
throw new Error('[ngx-json-render] <json-render> has no registry. Bind [registry], or supply one for the whole app with provideJsonRender({ registry }).');
|
|
2723
|
+
}
|
|
2724
|
+
return registry;
|
|
2725
|
+
}, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
|
|
2619
2726
|
root.loading = this.loading;
|
|
2620
|
-
root.fallback = this.fallback;
|
|
2621
|
-
root.limits = this.
|
|
2622
|
-
root.catalog = this.
|
|
2727
|
+
root.fallback = computed(() => this.fallback() ?? config.fallback ?? null, ...(ngDevMode ? [{ debugName: "fallback" }] : /* istanbul ignore next */ []));
|
|
2728
|
+
root.limits = this.limits;
|
|
2729
|
+
root.catalog = this.specCatalog;
|
|
2623
2730
|
root.store = this.store;
|
|
2624
2731
|
root.initialState = computed(() => this.state() ?? this.spec()?.state ?? {}, ...(ngDevMode ? [{ debugName: "initialState" }] : /* istanbul ignore next */ []));
|
|
2625
|
-
root.handlers = this.handlers;
|
|
2626
|
-
root.onAction = this.onAction;
|
|
2627
|
-
root.navigate = this.navigate;
|
|
2628
|
-
root.validationFunctions = this.validationFunctions;
|
|
2629
|
-
root.functions = this.functions;
|
|
2732
|
+
root.handlers = computed(() => mergeByName(config.handlers, this.handlers()), ...(ngDevMode ? [{ debugName: "handlers" }] : /* istanbul ignore next */ []));
|
|
2733
|
+
root.onAction = computed(() => this.onAction() ?? config.onAction ?? null, ...(ngDevMode ? [{ debugName: "onAction" }] : /* istanbul ignore next */ []));
|
|
2734
|
+
root.navigate = computed(() => this.navigate() ?? config.navigate ?? null, ...(ngDevMode ? [{ debugName: "navigate" }] : /* istanbul ignore next */ []));
|
|
2735
|
+
root.validationFunctions = computed(() => mergeByName(config.validationFunctions, this.validationFunctions()), ...(ngDevMode ? [{ debugName: "validationFunctions" }] : /* istanbul ignore next */ []));
|
|
2736
|
+
root.functions = computed(() => mergeByName(config.functions, this.functions()), ...(ngDevMode ? [{ debugName: "functions" }] : /* istanbul ignore next */ []));
|
|
2630
2737
|
root.directiveRegistry = computed(() => {
|
|
2631
|
-
const definitions = this.directives();
|
|
2738
|
+
const definitions = mergeDirectives(config.directives, this.directives());
|
|
2632
2739
|
return definitions ? createDirectiveRegistry(definitions) : undefined;
|
|
2633
2740
|
}, ...(ngDevMode ? [{ debugName: "directiveRegistry" }] : /* istanbul ignore next */ []));
|
|
2634
2741
|
root.emitStateChange = (changes) => this.stateChange.emit(changes);
|
|
@@ -2644,7 +2751,25 @@ class JsonRenderer {
|
|
|
2644
2751
|
effect(() => {
|
|
2645
2752
|
if (this.loading())
|
|
2646
2753
|
return;
|
|
2647
|
-
reportSpecCheck(this.checked(), this.
|
|
2754
|
+
reportSpecCheck(this.checked(), this.mode());
|
|
2755
|
+
});
|
|
2756
|
+
// With validation off, a `root` that names no element renders nothing and
|
|
2757
|
+
// nothing says why — the first mistake a hand-written spec tends to have.
|
|
2758
|
+
// Skipped while loading (a stream sends `/root` before its element) and
|
|
2759
|
+
// under `warn`/`strict`, where the check already reports `root_not_found`.
|
|
2760
|
+
// Once per root key, not once per change detection run.
|
|
2761
|
+
const warnedRoots = new Set();
|
|
2762
|
+
effect(() => {
|
|
2763
|
+
if (this.loading() || this.mode() !== 'off')
|
|
2764
|
+
return;
|
|
2765
|
+
const spec = this.checked().spec;
|
|
2766
|
+
if (!spec?.root)
|
|
2767
|
+
return;
|
|
2768
|
+
const root = spec.root;
|
|
2769
|
+
if (spec.elements?.[root] || warnedRoots.has(root))
|
|
2770
|
+
return;
|
|
2771
|
+
warnedRoots.add(root);
|
|
2772
|
+
console.warn(`[ngx-json-render] root "${root}" is not in elements; nothing is rendered.`);
|
|
2648
2773
|
});
|
|
2649
2774
|
}
|
|
2650
2775
|
rootKey = computed(() => {
|
|
@@ -2657,7 +2782,7 @@ class JsonRenderer {
|
|
|
2657
2782
|
// A spec whose errors survived the fixes does not render under `strict`.
|
|
2658
2783
|
// Reporting it and drawing half of it anyway is the behaviour the mode
|
|
2659
2784
|
// exists to refuse.
|
|
2660
|
-
if (this.
|
|
2785
|
+
if (this.mode() === 'strict' && checked.hasErrors)
|
|
2661
2786
|
return null;
|
|
2662
2787
|
const spec = checked.spec;
|
|
2663
2788
|
return spec?.root && spec.elements?.[spec.root] ? spec.root : null;
|
|
@@ -2696,7 +2821,7 @@ class JsonRenderer {
|
|
|
2696
2821
|
});
|
|
2697
2822
|
}, ...(ngDevMode ? [{ debugName: "confirmInjector" }] : /* istanbul ignore next */ []));
|
|
2698
2823
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2699
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JsonRenderer, isStandalone: true, selector: "json-render", inputs: { spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: true, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired:
|
|
2824
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JsonRenderer, isStandalone: true, selector: "json-render", inputs: { spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: true, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fallback: { classPropertyName: "fallback", publicName: "fallback", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, renderLimits: { classPropertyName: "renderLimits", publicName: "renderLimits", isSignal: true, isRequired: false, transformFunction: null }, catalog: { classPropertyName: "catalog", publicName: "catalog", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, store: { classPropertyName: "store", publicName: "store", isSignal: true, isRequired: false, transformFunction: null }, handlers: { classPropertyName: "handlers", publicName: "handlers", isSignal: true, isRequired: false, transformFunction: null }, onAction: { classPropertyName: "onAction", publicName: "onAction", isSignal: true, isRequired: false, transformFunction: null }, navigate: { classPropertyName: "navigate", publicName: "navigate", isSignal: true, isRequired: false, transformFunction: null }, validationFunctions: { classPropertyName: "validationFunctions", publicName: "validationFunctions", isSignal: true, isRequired: false, transformFunction: null }, functions: { classPropertyName: "functions", publicName: "functions", isSignal: true, isRequired: false, transformFunction: null }, directives: { classPropertyName: "directives", publicName: "directives", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stateChange: "stateChange" }, providers: [
|
|
2700
2825
|
JsonRenderRootContext,
|
|
2701
2826
|
JsonRenderStateService,
|
|
2702
2827
|
JsonRenderValidationService,
|
|
@@ -2745,7 +2870,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2745
2870
|
}
|
|
2746
2871
|
}
|
|
2747
2872
|
`, styles: [":host{display:contents}\n"] }]
|
|
2748
|
-
}], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required:
|
|
2873
|
+
}], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], fallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallback", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], renderLimits: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderLimits", required: false }] }], catalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "catalog", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], store: [{ type: i0.Input, args: [{ isSignal: true, alias: "store", required: false }] }], handlers: [{ type: i0.Input, args: [{ isSignal: true, alias: "handlers", required: false }] }], onAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "onAction", required: false }] }], navigate: [{ type: i0.Input, args: [{ isSignal: true, alias: "navigate", required: false }] }], validationFunctions: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationFunctions", required: false }] }], functions: [{ type: i0.Input, args: [{ isSignal: true, alias: "functions", required: false }] }], directives: [{ type: i0.Input, args: [{ isSignal: true, alias: "directives", required: false }] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }] } });
|
|
2749
2874
|
|
|
2750
2875
|
/**
|
|
2751
2876
|
* Provides the repeat scope (item, index, absolute base path) to the elements
|
|
@@ -3886,5 +4011,5 @@ function injectChatUI(options) {
|
|
|
3886
4011
|
* Generated bundle index. Do not edit.
|
|
3887
4012
|
*/
|
|
3888
4013
|
|
|
3889
|
-
export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JrChildren, JrConfirmDialog, JrElement, JrRepeatScope, JsonRenderActionsService, JsonRenderRootContext, JsonRenderStateService, JsonRenderValidationService, JsonRenderer, RENDER_CONTEXT, REPEAT_SCOPE, applyPatch, buildSpecFromParts, checkSpec, createStoreSetState, defineRegistry, flatToTree, formatSpecCheckIssues, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage, ɵregisterStreamObserver };
|
|
4014
|
+
export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JSON_RENDER_CONFIG, JrChildren, JrConfirmDialog, JrElement, JrRepeatScope, JsonRenderActionsService, JsonRenderRootContext, JsonRenderStateService, JsonRenderValidationService, JsonRenderer, RENDER_CONTEXT, REPEAT_SCOPE, applyPatch, buildSpecFromParts, checkSpec, createStoreSetState, defineRegistry, flatToTree, formatSpecCheckIssues, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage, provideJsonRender, ɵregisterStreamObserver };
|
|
3890
4015
|
//# sourceMappingURL=ngx-json-render.mjs.map
|