ngx-json-render 0.3.0 → 0.5.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 +146 -20
- package/fesm2022/ngx-json-render-testing.mjs +232 -56
- package/fesm2022/ngx-json-render-testing.mjs.map +1 -1
- package/fesm2022/ngx-json-render.mjs +516 -49
- package/fesm2022/ngx-json-render.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ngx-json-render-testing.d.ts +110 -17
- package/types/ngx-json-render.d.ts +223 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NgComponentOutlet } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
import { signal, Injectable, inject, computed, untracked, effect, isDevMode, DestroyRef, InjectionToken, input, output, viewChild, afterNextRender, ChangeDetectionStrategy, Component, Injector, forwardRef } from '@angular/core';
|
|
4
|
-
import { createStateStore, getByPath, runValidation, resolveAction, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, executeAction, isDevtoolsActive, subscribeDevtoolsActive, evaluateVisibility, resolveElementProps, resolveBindings, resolveActionParam, autoFixSpec, validateSpec,
|
|
4
|
+
import { createStateStore, getByPath, runValidation, resolveAction, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, executeAction, isDevtoolsActive, subscribeDevtoolsActive, evaluateVisibility, resolveElementProps, resolveBindings, resolveActionParam, autoFixSpec, validateSpec, createDirectiveRegistry, resolveRepeatStatePath, resolveRepeatItemStatePath, removeByPath, addByPath, setByPath, SPEC_DATA_PART_TYPE, nestedToFlat, createMixedStreamParser, defineSchema } from '@json-render/core';
|
|
5
5
|
export { autoFixSpec, createStateStore, formatSpecIssues, nestedToFlat, validateSpec } from '@json-render/core';
|
|
6
6
|
import { flattenToPointers } from '@json-render/core/store-utils';
|
|
7
7
|
|
|
@@ -18,6 +18,11 @@ class JsonRenderRootContext {
|
|
|
18
18
|
registry = EMPTY;
|
|
19
19
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
20
20
|
fallback = EMPTY;
|
|
21
|
+
/**
|
|
22
|
+
* Caps on what the spec may cost the browser. Read as the tree renders, so
|
|
23
|
+
* they apply whatever `validate` is set to.
|
|
24
|
+
*/
|
|
25
|
+
limits = EMPTY;
|
|
21
26
|
/** External store (controlled mode). */
|
|
22
27
|
store = EMPTY;
|
|
23
28
|
/** Initial state (uncontrolled mode); falls back to `spec.state`. */
|
|
@@ -977,6 +982,16 @@ const ELEMENT_KEY = new InjectionToken('ngx-json-render ELEMENT_KEY');
|
|
|
977
982
|
* `repeat` block.
|
|
978
983
|
*/
|
|
979
984
|
const REPEAT_SCOPE = new InjectionToken('ngx-json-render REPEAT_SCOPE');
|
|
985
|
+
/**
|
|
986
|
+
* This element's position in the render tree. Provided by `<jr-element>` for
|
|
987
|
+
* everything it renders below itself, so each element can see its own depth
|
|
988
|
+
* and its own ancestry.
|
|
989
|
+
*
|
|
990
|
+
* @internal Not part of the public API: it exists so the renderer can refuse
|
|
991
|
+
* to draw an element that is its own ancestor, and to enforce
|
|
992
|
+
* `renderLimits.maxDepth`.
|
|
993
|
+
*/
|
|
994
|
+
const RENDER_PATH = new InjectionToken('ngx-json-render RENDER_PATH');
|
|
980
995
|
/**
|
|
981
996
|
* Inject the render context inside a catalog component.
|
|
982
997
|
*
|
|
@@ -1037,6 +1052,53 @@ function reportActionError(error) {
|
|
|
1037
1052
|
return;
|
|
1038
1053
|
console.error(error);
|
|
1039
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Whether `inner` reads strictly deeper into state than `outer` — the state
|
|
1057
|
+
* path of one repeat item nested inside another's.
|
|
1058
|
+
*
|
|
1059
|
+
* `null` is the world outside any repeat, so stepping into one is always
|
|
1060
|
+
* progress. Below that it is a prefix test on the resolved path, and the
|
|
1061
|
+
* trailing separator is what keeps `/tree/10` from counting as a step inside
|
|
1062
|
+
* `/tree/1`.
|
|
1063
|
+
*/
|
|
1064
|
+
function descendsInto(outer, inner) {
|
|
1065
|
+
if (inner === null)
|
|
1066
|
+
return false;
|
|
1067
|
+
if (outer === null)
|
|
1068
|
+
return true;
|
|
1069
|
+
return inner.startsWith(`${outer}/`);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Whether this element renders itself again without getting anywhere — the
|
|
1073
|
+
* one arrangement that cannot terminate.
|
|
1074
|
+
*
|
|
1075
|
+
* An element reaching itself is not by itself a defect. It is how a tree gets
|
|
1076
|
+
* drawn — a comment thread, a file browser, a nested menu — where each pass
|
|
1077
|
+
* repeats over a path relative to the item it is already inside and so reads
|
|
1078
|
+
* one level further into the data. That ends when the data does.
|
|
1079
|
+
*
|
|
1080
|
+
* What separates the two is whether the repeat descended. Compare against the
|
|
1081
|
+
* nearest occurrence above: `/tree/0` then `/tree/0/children/1` is a tree
|
|
1082
|
+
* walking down, while `/items/0` then `/items/1` is a spec repeating over a
|
|
1083
|
+
* fixed array inside itself, and `/items/0` twice is one with no repeat
|
|
1084
|
+
* between the passes at all. Only the first can run out.
|
|
1085
|
+
*/
|
|
1086
|
+
function repeatsItself(path) {
|
|
1087
|
+
for (let step = path.parent; step !== null; step = step.parent) {
|
|
1088
|
+
if (step.key !== path.key)
|
|
1089
|
+
continue;
|
|
1090
|
+
return !descendsInto(step.scopePath, path.scopePath);
|
|
1091
|
+
}
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
/** The chain from the root down to `path`, as a line a human can follow. */
|
|
1095
|
+
function describePath(path) {
|
|
1096
|
+
const keys = [];
|
|
1097
|
+
for (let step = path; step !== null; step = step.parent) {
|
|
1098
|
+
keys.push(step.key);
|
|
1099
|
+
}
|
|
1100
|
+
return keys.reverse().join(' \u2192 ');
|
|
1101
|
+
}
|
|
1040
1102
|
/**
|
|
1041
1103
|
* Renders a single spec element: evaluates visibility, resolves prop
|
|
1042
1104
|
* expressions against the state model, wires event/watch bindings, and
|
|
@@ -1050,6 +1112,7 @@ class JrElement {
|
|
|
1050
1112
|
state = inject(JsonRenderStateService);
|
|
1051
1113
|
actions = inject(JsonRenderActionsService);
|
|
1052
1114
|
repeatScope = inject(REPEAT_SCOPE, { optional: true });
|
|
1115
|
+
parentPath = inject(RENDER_PATH, { optional: true });
|
|
1053
1116
|
devtoolsActive = injectDevtoolsActive();
|
|
1054
1117
|
/** The raw (unresolved) element from the spec. */
|
|
1055
1118
|
rawElement = computed(() => this.root.spec()?.elements?.[this.elementKey()], ...(ngDevMode ? [{ debugName: "rawElement" }] : /* istanbul ignore next */ []));
|
|
@@ -1098,6 +1161,39 @@ class JrElement {
|
|
|
1098
1161
|
}, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
|
|
1099
1162
|
component = computed(() => this.entry()?.component ?? null, ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
|
|
1100
1163
|
devtoolsKey = computed(() => this.devtoolsActive() ? this.elementKey() : null, ...(ngDevMode ? [{ debugName: "devtoolsKey" }] : /* istanbul ignore next */ []));
|
|
1164
|
+
/**
|
|
1165
|
+
* Where this element sits in the tree. Passed down to everything it renders
|
|
1166
|
+
* below itself, which is what lets each element see its own ancestry
|
|
1167
|
+
* without the renderer holding a registry of who is drawing what.
|
|
1168
|
+
*/
|
|
1169
|
+
path = computed(() => {
|
|
1170
|
+
const parent = this.parentPath?.() ?? null;
|
|
1171
|
+
return {
|
|
1172
|
+
key: this.elementKey(),
|
|
1173
|
+
depth: (parent?.depth ?? 0) + 1,
|
|
1174
|
+
scopePath: this.repeatScope?.basePath() ?? null,
|
|
1175
|
+
parent,
|
|
1176
|
+
};
|
|
1177
|
+
}, ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
1178
|
+
/**
|
|
1179
|
+
* Why this element is not drawn, or null when it is.
|
|
1180
|
+
*
|
|
1181
|
+
* The cycle half is unconditional — not something `validate` turns on. An
|
|
1182
|
+
* element that renders itself without reading any deeper renders forever,
|
|
1183
|
+
* and a spec that never stops rendering takes the tab with it; refusing to
|
|
1184
|
+
* draw the repetition is the only outcome that leaves anything on screen.
|
|
1185
|
+
* It costs one walk up a chain of parents, bounded by `maxDepth` where one
|
|
1186
|
+
* is set and by the spec's own nesting where it is not.
|
|
1187
|
+
*/
|
|
1188
|
+
refusal = computed(() => {
|
|
1189
|
+
const path = this.path();
|
|
1190
|
+
if (repeatsItself(path))
|
|
1191
|
+
return 'cycle';
|
|
1192
|
+
const maxDepth = this.root.limits()?.maxDepth;
|
|
1193
|
+
if (maxDepth !== undefined && path.depth > maxDepth)
|
|
1194
|
+
return 'depth';
|
|
1195
|
+
return null;
|
|
1196
|
+
}, ...(ngDevMode ? [{ debugName: "refusal" }] : /* istanbul ignore next */ []));
|
|
1101
1197
|
renderCtx = {
|
|
1102
1198
|
element: this.resolvedElement,
|
|
1103
1199
|
props: computed(() => this.resolvedElement()?.props ?? {}),
|
|
@@ -1119,10 +1215,28 @@ class JrElement {
|
|
|
1119
1215
|
providers: [
|
|
1120
1216
|
{ provide: RENDER_CONTEXT, useValue: this.renderCtx },
|
|
1121
1217
|
{ provide: ELEMENT_KEY, useValue: this.elementKey },
|
|
1218
|
+
{ provide: RENDER_PATH, useValue: this.path },
|
|
1122
1219
|
],
|
|
1123
1220
|
parent: inject(Injector),
|
|
1124
1221
|
});
|
|
1125
1222
|
constructor() {
|
|
1223
|
+
// Warn (once) when this element is refused. Both reasons are silent
|
|
1224
|
+
// failures on screen — something the spec asked for is missing — so
|
|
1225
|
+
// neither should be silent in the console.
|
|
1226
|
+
let warnedRefusal = false;
|
|
1227
|
+
effect(() => {
|
|
1228
|
+
const reason = this.refusal();
|
|
1229
|
+
if (!reason || warnedRefusal)
|
|
1230
|
+
return;
|
|
1231
|
+
warnedRefusal = true;
|
|
1232
|
+
const path = untracked(this.path);
|
|
1233
|
+
if (reason === 'cycle') {
|
|
1234
|
+
console.warn(`[ngx-json-render] Cycle in the spec: ${describePath(path)}. "${path.key}" renders itself again without reading any deeper into state${path.scopePath === null ? '' : ` (still at "${path.scopePath}")`}, so it never ends. It is not rendered again; everything above it still is. A tree that recurses needs a repeat with a relative statePath, like {"$item": "children"}.`);
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
console.warn(`[ngx-json-render] renderLimits.maxDepth (${untracked(this.root.limits)?.maxDepth}) reached at ${describePath(path)}. "${path.key}" and anything below it do not render.`);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1126
1240
|
// Warn (once per type) about unknown component types.
|
|
1127
1241
|
const warnedTypes = new Set();
|
|
1128
1242
|
effect(() => {
|
|
@@ -1155,6 +1269,14 @@ class JrElement {
|
|
|
1155
1269
|
});
|
|
1156
1270
|
// Watch effect: fire actions when watched state paths change.
|
|
1157
1271
|
effect((onCleanup) => {
|
|
1272
|
+
// A refused element does not act, not just draw nothing. `watch` is the
|
|
1273
|
+
// one thing an element does without being on screen, so leaving it wired
|
|
1274
|
+
// would let the element past `maxDepth` — or the one that closes a cycle
|
|
1275
|
+
// — keep dispatching actions from behind a cap that was supposed to have
|
|
1276
|
+
// stopped it. Reading the signal here also unwires it if the refusal
|
|
1277
|
+
// arrives later, when the limits change.
|
|
1278
|
+
if (this.refusal())
|
|
1279
|
+
return;
|
|
1158
1280
|
const watchConfig = this.rawElement()?.watch;
|
|
1159
1281
|
if (!watchConfig)
|
|
1160
1282
|
return;
|
|
@@ -1168,21 +1290,8 @@ class JrElement {
|
|
|
1168
1290
|
if (!changedPaths.has(path))
|
|
1169
1291
|
continue;
|
|
1170
1292
|
const binding = watchConfig[path];
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
1173
|
-
const bindings = Array.isArray(binding) ? binding : [binding];
|
|
1174
|
-
for (const b of bindings) {
|
|
1175
|
-
if (!b.params) {
|
|
1176
|
-
await this.actions.execute(b);
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1179
|
-
const liveCtx = this.liveResolutionCtx();
|
|
1180
|
-
const resolved = {};
|
|
1181
|
-
for (const [key, val] of Object.entries(b.params)) {
|
|
1182
|
-
resolved[key] = resolveActionParam(val, liveCtx);
|
|
1183
|
-
}
|
|
1184
|
-
await this.actions.execute({ ...b, params: resolved });
|
|
1185
|
-
}
|
|
1293
|
+
if (binding)
|
|
1294
|
+
await this.dispatch(binding);
|
|
1186
1295
|
}
|
|
1187
1296
|
})().catch(reportActionError);
|
|
1188
1297
|
});
|
|
@@ -1208,12 +1317,17 @@ class JrElement {
|
|
|
1208
1317
|
this.emitEvent(eventName).catch(reportActionError);
|
|
1209
1318
|
}
|
|
1210
1319
|
async emitEvent(eventName) {
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1320
|
+
const binding = untracked(this.rawElement)?.on?.[eventName];
|
|
1321
|
+
if (binding)
|
|
1322
|
+
await this.dispatch(binding);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Run an `on` or `watch` binding's actions in order. Params are resolved
|
|
1326
|
+
* per action, just before it runs, so a `$state` read in a later action of
|
|
1327
|
+
* the chain sees what an earlier one wrote.
|
|
1328
|
+
*/
|
|
1329
|
+
async dispatch(binding) {
|
|
1330
|
+
for (const b of Array.isArray(binding) ? binding : [binding]) {
|
|
1217
1331
|
if (!b.params) {
|
|
1218
1332
|
await this.actions.execute(b);
|
|
1219
1333
|
continue;
|
|
@@ -1241,7 +1355,7 @@ class JrElement {
|
|
|
1241
1355
|
}
|
|
1242
1356
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1243
1357
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JrElement, isStandalone: true, selector: "jr-element", inputs: { elementKey: { classPropertyName: "elementKey", publicName: "elementKey", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
1244
|
-
@if (rawElement() && visible() && component()) {
|
|
1358
|
+
@if (!refusal() && rawElement() && visible() && component()) {
|
|
1245
1359
|
@if (devtoolsKey(); as dk) {
|
|
1246
1360
|
<span [attr.data-jr-key]="dk" style="display: contents">
|
|
1247
1361
|
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
@@ -1255,7 +1369,7 @@ class JrElement {
|
|
|
1255
1369
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, decorators: [{
|
|
1256
1370
|
type: Component,
|
|
1257
1371
|
args: [{ selector: 'jr-element', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet], template: `
|
|
1258
|
-
@if (rawElement() && visible() && component()) {
|
|
1372
|
+
@if (!refusal() && rawElement() && visible() && component()) {
|
|
1259
1373
|
@if (devtoolsKey(); as dk) {
|
|
1260
1374
|
<span [attr.data-jr-key]="dk" style="display: contents">
|
|
1261
1375
|
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
@@ -1267,53 +1381,325 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1267
1381
|
`, styles: [":host{display:contents}\n"] }]
|
|
1268
1382
|
}], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
|
|
1269
1383
|
|
|
1384
|
+
/**
|
|
1385
|
+
* A hostile spec can fail the catalog's schema in as many places as it has
|
|
1386
|
+
* properties. Report enough to act on and stop; the spec is refused either
|
|
1387
|
+
* way, and a console full of them helps nobody.
|
|
1388
|
+
*/
|
|
1389
|
+
const MAX_REPORTED_PROP_ISSUES = 20;
|
|
1390
|
+
/**
|
|
1391
|
+
* Whether this element's `repeat` descends into the item it is already inside,
|
|
1392
|
+
* rather than re-reading a fixed path in state.
|
|
1393
|
+
*
|
|
1394
|
+
* A relative statePath — `{"$item": "children"}` — resolves against the
|
|
1395
|
+
* enclosing item, so each pass reads one level further into the data and the
|
|
1396
|
+
* recursion ends where the data does. A plain string is absolute: every pass
|
|
1397
|
+
* reads the same array, and a spec that re-enters itself through one never
|
|
1398
|
+
* terminates.
|
|
1399
|
+
*/
|
|
1400
|
+
function descendsIntoItems(element) {
|
|
1401
|
+
const repeat = element.repeat;
|
|
1402
|
+
return repeat !== undefined && typeof repeat.statePath !== 'string';
|
|
1403
|
+
}
|
|
1404
|
+
/** Every key an element renders below itself: `children` plus every slot. */
|
|
1405
|
+
function childKeysOf(element) {
|
|
1406
|
+
const slots = element.slots;
|
|
1407
|
+
if (!slots)
|
|
1408
|
+
return element.children ?? [];
|
|
1409
|
+
const fromSlots = Object.values(slots).flat();
|
|
1410
|
+
return element.children ? [...element.children, ...fromSlots] : fromSlots;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Walk the element graph once for depth and cycles.
|
|
1414
|
+
*
|
|
1415
|
+
* Iterative on purpose. A recursive walk would blow the stack on exactly the
|
|
1416
|
+
* specs this exists to catch, which would make the guard a second way to
|
|
1417
|
+
* crash rather than the fix for the first.
|
|
1418
|
+
*
|
|
1419
|
+
* Depth is measured from `spec.root`, because that is what renders. Cycles
|
|
1420
|
+
* are swept for across every element, including the ones no parent reaches:
|
|
1421
|
+
* an unreachable cycle is still a defect, and one edit away from a reachable
|
|
1422
|
+
* one.
|
|
1423
|
+
*
|
|
1424
|
+
* Not every element that reaches itself is a defect, though. Drawing a tree —
|
|
1425
|
+
* a comment thread, a file browser, a nested menu — means an element that
|
|
1426
|
+
* repeats over a relative path and renders itself for each item, and that ends
|
|
1427
|
+
* when the data does. A back edge is only reported when nothing along it
|
|
1428
|
+
* descends into the data, which is the case that cannot end.
|
|
1429
|
+
*
|
|
1430
|
+
* `depthBelow` is memoised per element, so a shared subtree costs one visit
|
|
1431
|
+
* instead of one per path into it — without that, a spec whose elements each
|
|
1432
|
+
* name the next two is exponential to check. The memo makes the reported
|
|
1433
|
+
* depth a lower bound once a cycle has been cut, since which edge closes the
|
|
1434
|
+
* cycle depends on the order the walk reached it. The cycle is reported as an
|
|
1435
|
+
* error of its own, which is the finding worth acting on.
|
|
1436
|
+
*/
|
|
1437
|
+
function analyseSpecGraph(spec) {
|
|
1438
|
+
const elements = spec.elements ?? {};
|
|
1439
|
+
const cycles = new Set();
|
|
1440
|
+
const depthBelow = new Map();
|
|
1441
|
+
const onPath = new Set();
|
|
1442
|
+
const walkFrom = (start) => {
|
|
1443
|
+
const first = elements[start];
|
|
1444
|
+
if (!first || depthBelow.has(start))
|
|
1445
|
+
return;
|
|
1446
|
+
const stack = [
|
|
1447
|
+
{ key: start, kids: childKeysOf(first), next: 0, deepest: 0 },
|
|
1448
|
+
];
|
|
1449
|
+
onPath.add(start);
|
|
1450
|
+
while (stack.length > 0) {
|
|
1451
|
+
const frame = stack[stack.length - 1];
|
|
1452
|
+
if (frame.next < frame.kids.length) {
|
|
1453
|
+
const childKey = frame.kids[frame.next++];
|
|
1454
|
+
const memo = depthBelow.get(childKey);
|
|
1455
|
+
if (memo !== undefined) {
|
|
1456
|
+
frame.deepest = Math.max(frame.deepest, memo);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
// A child that closes the cycle is not followed — that is what keeps
|
|
1460
|
+
// this walk finite — and not counted towards depth either.
|
|
1461
|
+
if (onPath.has(childKey)) {
|
|
1462
|
+
const from = stack.findIndex((frame) => frame.key === childKey);
|
|
1463
|
+
const descends = stack
|
|
1464
|
+
.slice(from)
|
|
1465
|
+
.some((frame) => descendsIntoItems(elements[frame.key]));
|
|
1466
|
+
if (!descends)
|
|
1467
|
+
cycles.add(childKey);
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const child = elements[childKey];
|
|
1471
|
+
// A child the spec never defined is validateSpec's finding, not ours.
|
|
1472
|
+
if (!child)
|
|
1473
|
+
continue;
|
|
1474
|
+
onPath.add(childKey);
|
|
1475
|
+
stack.push({
|
|
1476
|
+
key: childKey,
|
|
1477
|
+
kids: childKeysOf(child),
|
|
1478
|
+
next: 0,
|
|
1479
|
+
deepest: 0,
|
|
1480
|
+
});
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
stack.pop();
|
|
1484
|
+
onPath.delete(frame.key);
|
|
1485
|
+
const depth = frame.deepest + 1;
|
|
1486
|
+
depthBelow.set(frame.key, depth);
|
|
1487
|
+
const parent = stack[stack.length - 1];
|
|
1488
|
+
if (parent)
|
|
1489
|
+
parent.deepest = Math.max(parent.deepest, depth);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
if (spec.root)
|
|
1493
|
+
walkFrom(spec.root);
|
|
1494
|
+
const depth = (spec.root ? depthBelow.get(spec.root) : undefined) ?? 0;
|
|
1495
|
+
for (const key of Object.keys(elements))
|
|
1496
|
+
walkFrom(key);
|
|
1497
|
+
return { depth, cycles: [...cycles] };
|
|
1498
|
+
}
|
|
1499
|
+
/** Report a cycle, naming the element that is its own descendant. */
|
|
1500
|
+
function cycleIssue(key) {
|
|
1501
|
+
return {
|
|
1502
|
+
severity: 'error',
|
|
1503
|
+
code: 'cycle',
|
|
1504
|
+
elementKey: key,
|
|
1505
|
+
message: `Element "${key}" renders itself against the same data, which never terminates. The cycle is broken where it closes — the repeated element does not render a second time — but the spec cannot be drawn as written. A tree that recurses needs a repeat with a relative statePath, like {"$item": "children"}.`,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
/** Report a breached element-count cap. */
|
|
1509
|
+
function elementCountIssue(count, limit) {
|
|
1510
|
+
return {
|
|
1511
|
+
severity: 'error',
|
|
1512
|
+
code: 'too_many_elements',
|
|
1513
|
+
message: `Spec has ${count} elements, over the limit of ${limit}. Nothing is rendered: renderLimits.maxElements is a cap on the whole spec.`,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
/** Report a breached depth cap. */
|
|
1517
|
+
function depthIssue(depth, limit) {
|
|
1518
|
+
return {
|
|
1519
|
+
severity: 'error',
|
|
1520
|
+
code: 'too_deep',
|
|
1521
|
+
message: `Spec nests ${depth} levels deep, over the limit of ${limit}. Elements below level ${limit} do not render.`,
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Check a spec against a catalog: the component types it names, and then the
|
|
1526
|
+
* catalog's own schema.
|
|
1527
|
+
*
|
|
1528
|
+
* The schema pass is skipped while a type is unknown. A catalog schema keys
|
|
1529
|
+
* its element shapes off `type`, so one name it does not recognise turns into
|
|
1530
|
+
* a pile of prop errors describing a component that was never the point.
|
|
1531
|
+
*/
|
|
1532
|
+
function catalogIssues(spec, catalog) {
|
|
1533
|
+
const issues = [];
|
|
1534
|
+
const known = new Set(catalog.componentNames);
|
|
1535
|
+
for (const [key, element] of Object.entries(spec.elements ?? {})) {
|
|
1536
|
+
if (known.has(element.type))
|
|
1537
|
+
continue;
|
|
1538
|
+
issues.push({
|
|
1539
|
+
severity: 'error',
|
|
1540
|
+
code: 'unknown_component',
|
|
1541
|
+
elementKey: key,
|
|
1542
|
+
message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
if (issues.length > 0)
|
|
1546
|
+
return issues;
|
|
1547
|
+
const result = catalog.validate(spec);
|
|
1548
|
+
if (result.success)
|
|
1549
|
+
return issues;
|
|
1550
|
+
return propIssues(result.error);
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Turn whatever the catalog's schema reported into issues.
|
|
1554
|
+
*
|
|
1555
|
+
* Read defensively rather than typed against the schema library: the shape
|
|
1556
|
+
* below (`issues`, each with `path` and `message`) is what every version of
|
|
1557
|
+
* it has produced, and a renderer that threw while explaining a bad spec
|
|
1558
|
+
* would be the worse failure.
|
|
1559
|
+
*/
|
|
1560
|
+
function propIssues(error) {
|
|
1561
|
+
const raw = error?.issues;
|
|
1562
|
+
if (!Array.isArray(raw))
|
|
1563
|
+
return [];
|
|
1564
|
+
return raw.slice(0, MAX_REPORTED_PROP_ISSUES).map((entry) => {
|
|
1565
|
+
const item = entry;
|
|
1566
|
+
const path = Array.isArray(item.path) ? item.path : [];
|
|
1567
|
+
const message = typeof item.message === 'string' ? item.message : 'is not valid';
|
|
1568
|
+
// Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
|
|
1569
|
+
// element in its second segment.
|
|
1570
|
+
const elementKey = path[0] === 'elements' && typeof path[1] === 'string'
|
|
1571
|
+
? path[1]
|
|
1572
|
+
: undefined;
|
|
1573
|
+
const where = path.length > 0 ? path.join('.') : 'spec';
|
|
1574
|
+
return {
|
|
1575
|
+
severity: 'error',
|
|
1576
|
+
code: 'invalid_props',
|
|
1577
|
+
...(elementKey === undefined ? {} : { elementKey }),
|
|
1578
|
+
message: `${where}: ${message}`,
|
|
1579
|
+
};
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1270
1583
|
const CLEAN = {
|
|
1271
1584
|
spec: null,
|
|
1272
1585
|
issues: [],
|
|
1273
1586
|
fixes: [],
|
|
1274
1587
|
hasErrors: false,
|
|
1588
|
+
blocked: false,
|
|
1275
1589
|
};
|
|
1276
1590
|
/**
|
|
1277
|
-
* Check a spec the way `validate`
|
|
1591
|
+
* Check a spec the way `validate` and `renderLimits` ask for.
|
|
1278
1592
|
*
|
|
1279
1593
|
* Only lossless fixes are applied — `visible`, `on` and `repeat` that a model
|
|
1280
1594
|
* put inside `props`, which the renderer would otherwise ignore, moved back
|
|
1281
1595
|
* where they belong. The lossy ones prune content, and core's own guidance is
|
|
1282
1596
|
* to re-prompt rather than accept that; a renderer that silently deleted
|
|
1283
1597
|
* elements would be the worse failure.
|
|
1598
|
+
*
|
|
1599
|
+
* Note what is *not* here: cycles are broken as the tree renders, whatever
|
|
1600
|
+
* this returns, because a spec that never stops rendering is a crash rather
|
|
1601
|
+
* than an opinion about quality. This reports them so an app can see them.
|
|
1284
1602
|
*/
|
|
1285
|
-
function checkSpec(spec, mode) {
|
|
1603
|
+
function checkSpec(spec, mode, options = {}) {
|
|
1286
1604
|
if (!spec)
|
|
1287
1605
|
return CLEAN;
|
|
1288
|
-
|
|
1289
|
-
|
|
1606
|
+
const limits = options.limits ?? null;
|
|
1607
|
+
const wantsStructure = mode !== 'off';
|
|
1608
|
+
// `maxRepeatItems` caps a live state array, which no amount of reading the
|
|
1609
|
+
// spec can predict — it is enforced as the repeat expands, not here.
|
|
1610
|
+
const maxElements = limits?.maxElements;
|
|
1611
|
+
const maxDepth = limits?.maxDepth;
|
|
1612
|
+
if (!wantsStructure && maxElements === undefined && maxDepth === undefined) {
|
|
1613
|
+
return { spec, issues: [], fixes: [], hasErrors: false, blocked: false };
|
|
1614
|
+
}
|
|
1615
|
+
// The order below is the point of this function, not an accident of it.
|
|
1616
|
+
//
|
|
1617
|
+
// Core's `validateSpec` walks the element tree by recursion, so the specs
|
|
1618
|
+
// that most need a limit are the ones that would overflow the stack proving
|
|
1619
|
+
// they exceed it. Each cheap, non-recursive cap therefore runs first and can
|
|
1620
|
+
// stop the expensive check from being attempted at all: count, then depth —
|
|
1621
|
+
// measured by a walk of our own that uses an explicit stack — and only then
|
|
1622
|
+
// the structural pass.
|
|
1623
|
+
//
|
|
1624
|
+
// `autoFixSpec` only relocates fields inside `props` and never touches
|
|
1625
|
+
// `children` or `slots`, so measuring the graph before fixing it measures
|
|
1626
|
+
// the same graph.
|
|
1627
|
+
if (maxElements !== undefined) {
|
|
1628
|
+
const count = Object.keys(spec.elements ?? {}).length;
|
|
1629
|
+
if (count > maxElements) {
|
|
1630
|
+
const issue = elementCountIssue(count, maxElements);
|
|
1631
|
+
return {
|
|
1632
|
+
spec,
|
|
1633
|
+
issues: [issue],
|
|
1634
|
+
fixes: [],
|
|
1635
|
+
hasErrors: true,
|
|
1636
|
+
blocked: true,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const issues = [];
|
|
1641
|
+
let tooDeep = false;
|
|
1642
|
+
if (wantsStructure || maxDepth !== undefined) {
|
|
1643
|
+
const graph = analyseSpecGraph(spec);
|
|
1644
|
+
if (maxDepth !== undefined && graph.depth > maxDepth) {
|
|
1645
|
+
tooDeep = true;
|
|
1646
|
+
issues.push(depthIssue(graph.depth, maxDepth));
|
|
1647
|
+
}
|
|
1648
|
+
if (wantsStructure) {
|
|
1649
|
+
for (const key of graph.cycles)
|
|
1650
|
+
issues.push(cycleIssue(key));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
// A spec past its depth cap is not handed to the recursive checks. The cap
|
|
1654
|
+
// is the finding worth having, and it renders truncated either way.
|
|
1655
|
+
const fixed = wantsStructure && !tooDeep
|
|
1656
|
+
? autoFixSpec(spec, { lossy: false })
|
|
1657
|
+
: { spec, fixes: [] };
|
|
1658
|
+
if (wantsStructure && !tooDeep) {
|
|
1659
|
+
issues.push(...validateSpec(fixed.spec).issues);
|
|
1660
|
+
if (options.catalog) {
|
|
1661
|
+
issues.push(...catalogIssues(fixed.spec, options.catalog));
|
|
1662
|
+
}
|
|
1290
1663
|
}
|
|
1291
|
-
const fixed = autoFixSpec(spec, { lossy: false });
|
|
1292
|
-
const { issues } = validateSpec(fixed.spec);
|
|
1293
1664
|
return {
|
|
1294
1665
|
spec: fixed.spec,
|
|
1295
1666
|
issues,
|
|
1296
1667
|
fixes: fixed.fixes,
|
|
1297
1668
|
hasErrors: issues.some((issue) => issue.severity === 'error'),
|
|
1669
|
+
blocked: false,
|
|
1298
1670
|
};
|
|
1299
1671
|
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Render issues as one indented block, errors and warnings alike.
|
|
1674
|
+
*
|
|
1675
|
+
* Core's `formatSpecIssues` drops warnings and only accepts core's own issue
|
|
1676
|
+
* codes, so it cannot describe what this package reports. Use this instead
|
|
1677
|
+
* when showing `issues()` to a developer.
|
|
1678
|
+
*/
|
|
1679
|
+
function formatSpecCheckIssues(issues) {
|
|
1680
|
+
return issues
|
|
1681
|
+
.map((issue) => ` ${issue.severity === 'error' ? 'error ' : 'warning'} ${issue.elementKey ? `[${issue.elementKey}] ` : ''}${issue.message}`)
|
|
1682
|
+
.join('\n');
|
|
1683
|
+
}
|
|
1300
1684
|
/**
|
|
1301
1685
|
* Report a check to the console: fixes as information, issues at the severity
|
|
1302
|
-
* the
|
|
1303
|
-
*
|
|
1686
|
+
* the outcome gives them. A spec that will not render — refused by a limit,
|
|
1687
|
+
* or by `strict` — is reported as an error, because something the developer
|
|
1688
|
+
* asked for is now missing from the screen; one that renders anyway is a
|
|
1304
1689
|
* warning.
|
|
1305
1690
|
*/
|
|
1306
1691
|
function reportSpecCheck(check, mode) {
|
|
1307
|
-
if (mode === 'off')
|
|
1308
|
-
return;
|
|
1309
1692
|
if (check.fixes.length > 0) {
|
|
1310
1693
|
console.info(`[ngx-json-render] Fixed ${check.fixes.length} spec issue(s) before rendering:\n` +
|
|
1311
1694
|
check.fixes.map((fix) => ` - ${fix}`).join('\n'));
|
|
1312
1695
|
}
|
|
1313
1696
|
if (check.issues.length === 0)
|
|
1314
1697
|
return;
|
|
1315
|
-
const report = `[ngx-json-render] Spec validation:\n${
|
|
1316
|
-
if (
|
|
1698
|
+
const report = `[ngx-json-render] Spec validation:\n${formatSpecCheckIssues(check.issues)}`;
|
|
1699
|
+
if (check.blocked) {
|
|
1700
|
+
console.error(`${report}\nNothing is rendered: the spec is over a renderLimits cap.`);
|
|
1701
|
+
}
|
|
1702
|
+
else if (mode === 'strict' && check.hasErrors) {
|
|
1317
1703
|
console.error(`${report}\nNothing is rendered: validate="strict".`);
|
|
1318
1704
|
}
|
|
1319
1705
|
else {
|
|
@@ -1362,6 +1748,27 @@ class JsonRenderer {
|
|
|
1362
1748
|
* arriving is expected to reference children that have not streamed in yet.
|
|
1363
1749
|
*/
|
|
1364
1750
|
validate = input('off', ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
|
|
1751
|
+
/**
|
|
1752
|
+
* Caps on what the spec may cost the browser: `maxElements`, `maxDepth` and
|
|
1753
|
+
* `maxRepeatItems`. Unset by default, and each field is independently
|
|
1754
|
+
* optional.
|
|
1755
|
+
*
|
|
1756
|
+
* Unlike `validate`, these are enforced in every mode — a number the app
|
|
1757
|
+
* chose is a control, not a report. A spec over `maxElements` renders
|
|
1758
|
+
* nothing; `maxDepth` and `maxRepeatItems` truncate where they are hit.
|
|
1759
|
+
* They apply while `loading` too: a partial spec is a subset of the
|
|
1760
|
+
* finished one, so a cap can only ever fire early, never falsely.
|
|
1761
|
+
*/
|
|
1762
|
+
renderLimits = input(null, ...(ngDevMode ? [{ debugName: "renderLimits" }] : /* istanbul ignore next */ []));
|
|
1763
|
+
/**
|
|
1764
|
+
* The catalog the spec was generated for. Given one, `validate` also checks
|
|
1765
|
+
* what structure alone cannot: that every `type` is a component the catalog
|
|
1766
|
+
* defines, and that the props match its schema.
|
|
1767
|
+
*
|
|
1768
|
+
* Only read when `validate` is on, and — like the rest of the check —
|
|
1769
|
+
* skipped while `loading`.
|
|
1770
|
+
*/
|
|
1771
|
+
catalog = input(null, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
|
|
1365
1772
|
/**
|
|
1366
1773
|
* Initial state model (uncontrolled mode). Defaults to `spec.state`.
|
|
1367
1774
|
* Ignored when `store` is provided.
|
|
@@ -1392,7 +1799,10 @@ class JsonRenderer {
|
|
|
1392
1799
|
* is forced off: the spec is still arriving, and the missing children it
|
|
1393
1800
|
* refers to are the normal state of a stream, not a defect.
|
|
1394
1801
|
*/
|
|
1395
|
-
checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.validate()
|
|
1802
|
+
checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.validate(), {
|
|
1803
|
+
limits: this.renderLimits(),
|
|
1804
|
+
catalog: this.catalog(),
|
|
1805
|
+
}), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
|
|
1396
1806
|
constructor() {
|
|
1397
1807
|
const root = inject(JsonRenderRootContext);
|
|
1398
1808
|
// The fixed spec, not the input: a `visible` the check moved out of
|
|
@@ -1401,6 +1811,7 @@ class JsonRenderer {
|
|
|
1401
1811
|
root.registry = this.registry;
|
|
1402
1812
|
root.loading = this.loading;
|
|
1403
1813
|
root.fallback = this.fallback;
|
|
1814
|
+
root.limits = this.renderLimits;
|
|
1404
1815
|
root.store = this.store;
|
|
1405
1816
|
root.initialState = computed(() => this.state() ?? this.spec()?.state ?? {}, ...(ngDevMode ? [{ debugName: "initialState" }] : /* istanbul ignore next */ []));
|
|
1406
1817
|
root.handlers = this.handlers;
|
|
@@ -1418,11 +1829,23 @@ class JsonRenderer {
|
|
|
1418
1829
|
inject(JsonRenderValidationService);
|
|
1419
1830
|
this.actions = inject(JsonRenderActionsService);
|
|
1420
1831
|
// Reports once per distinct check rather than once per render: `checked`
|
|
1421
|
-
// only recomputes when the spec, the mode or the loading flag
|
|
1422
|
-
|
|
1832
|
+
// only recomputes when the spec, the mode, the limits or the loading flag
|
|
1833
|
+
// change. Silent while loading — a limit can fire on a spec that is still
|
|
1834
|
+
// arriving, and reporting it on every patch would bury the one report
|
|
1835
|
+
// that describes the finished spec.
|
|
1836
|
+
effect(() => {
|
|
1837
|
+
if (this.loading())
|
|
1838
|
+
return;
|
|
1839
|
+
reportSpecCheck(this.checked(), this.validate());
|
|
1840
|
+
});
|
|
1423
1841
|
}
|
|
1424
1842
|
rootKey = computed(() => {
|
|
1425
1843
|
const checked = this.checked();
|
|
1844
|
+
// A spec over an enforced cap does not render in any mode. Whether to
|
|
1845
|
+
// check was the app's decision; once it has made it, honouring the answer
|
|
1846
|
+
// is not also `validate`'s to decide.
|
|
1847
|
+
if (checked.blocked)
|
|
1848
|
+
return null;
|
|
1426
1849
|
// A spec whose errors survived the fixes does not render under `strict`.
|
|
1427
1850
|
// Reporting it and drawing half of it anyway is the behaviour the mode
|
|
1428
1851
|
// exists to refuse.
|
|
@@ -1462,7 +1885,7 @@ class JsonRenderer {
|
|
|
1462
1885
|
});
|
|
1463
1886
|
}, ...(ngDevMode ? [{ debugName: "confirmInjector" }] : /* istanbul ignore next */ []));
|
|
1464
1887
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1465
|
-
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: true, 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 }, 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: [
|
|
1888
|
+
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: true, 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: [
|
|
1466
1889
|
JsonRenderRootContext,
|
|
1467
1890
|
JsonRenderStateService,
|
|
1468
1891
|
JsonRenderValidationService,
|
|
@@ -1511,7 +1934,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1511
1934
|
}
|
|
1512
1935
|
}
|
|
1513
1936
|
`, styles: [":host{display:contents}\n"] }]
|
|
1514
|
-
}], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: true }] }], 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 }] }], 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"] }] } });
|
|
1937
|
+
}], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: true }] }], 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"] }] } });
|
|
1515
1938
|
|
|
1516
1939
|
/**
|
|
1517
1940
|
* Provides the repeat scope (item, index, absolute base path) to the elements
|
|
@@ -1586,13 +2009,43 @@ class JrChildren {
|
|
|
1586
2009
|
}
|
|
1587
2010
|
return resolved;
|
|
1588
2011
|
}, ...(ngDevMode ? [{ debugName: "repeatBasePath" }] : /* istanbul ignore next */ []));
|
|
1589
|
-
|
|
2012
|
+
/** Every item the repeat's state array holds, before any cap. */
|
|
2013
|
+
allRepeatItems = computed(() => {
|
|
1590
2014
|
const basePath = this.repeatBasePath();
|
|
1591
2015
|
if (basePath === undefined)
|
|
1592
2016
|
return [];
|
|
1593
2017
|
return (getByPath(this.state.state(), basePath) ?? []);
|
|
2018
|
+
}, ...(ngDevMode ? [{ debugName: "allRepeatItems" }] : /* istanbul ignore next */ []));
|
|
2019
|
+
/**
|
|
2020
|
+
* The items this repeat actually renders.
|
|
2021
|
+
*
|
|
2022
|
+
* `repeat` iterates a state array the spec may itself have supplied, which
|
|
2023
|
+
* is the one place a spec sizes the render tree out of data rather than out
|
|
2024
|
+
* of its own structure — so the cap has to be applied here, as it expands,
|
|
2025
|
+
* rather than anywhere a spec can be inspected up front.
|
|
2026
|
+
*/
|
|
2027
|
+
repeatItems = computed(() => {
|
|
2028
|
+
const items = this.allRepeatItems();
|
|
2029
|
+
const max = this.root.limits()?.maxRepeatItems;
|
|
2030
|
+
return max !== undefined && items.length > max
|
|
2031
|
+
? items.slice(0, max)
|
|
2032
|
+
: items;
|
|
1594
2033
|
}, ...(ngDevMode ? [{ debugName: "repeatItems" }] : /* istanbul ignore next */ []));
|
|
1595
2034
|
constructor() {
|
|
2035
|
+
// Warn (once) when the cap holds items back. Reading the count only after
|
|
2036
|
+
// the two cheap guards keeps the effect from tracking the state array at
|
|
2037
|
+
// all once there is nothing left to say.
|
|
2038
|
+
let warnedRepeatCap = false;
|
|
2039
|
+
effect(() => {
|
|
2040
|
+
const max = this.root.limits()?.maxRepeatItems;
|
|
2041
|
+
if (max === undefined || warnedRepeatCap)
|
|
2042
|
+
return;
|
|
2043
|
+
const total = this.allRepeatItems().length;
|
|
2044
|
+
if (total <= max)
|
|
2045
|
+
return;
|
|
2046
|
+
warnedRepeatCap = true;
|
|
2047
|
+
console.warn(`[ngx-json-render] renderLimits.maxRepeatItems (${max}) reached: "${untracked(this.repeatBasePath)}" holds ${total} items, so ${total - max} of them do not render.`);
|
|
2048
|
+
});
|
|
1596
2049
|
// Warn (once per key) about children referencing missing elements.
|
|
1597
2050
|
const warned = new Set();
|
|
1598
2051
|
effect(() => {
|
|
@@ -2238,6 +2691,10 @@ function injectUIStream(options) {
|
|
|
2238
2691
|
const rawLines = signal([], ...(ngDevMode ? [{ debugName: "rawLines" }] : /* istanbul ignore next */ []));
|
|
2239
2692
|
const issues = signal([], ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
|
|
2240
2693
|
const validate = options.validate ?? 'off';
|
|
2694
|
+
const checkOptions = {
|
|
2695
|
+
limits: options.renderLimits ?? null,
|
|
2696
|
+
catalog: options.catalog ?? null,
|
|
2697
|
+
};
|
|
2241
2698
|
const session = createStreamSession();
|
|
2242
2699
|
inject(DestroyRef).onDestroy(() => session.cancel());
|
|
2243
2700
|
const stop = () => {
|
|
@@ -2292,7 +2749,7 @@ function injectUIStream(options) {
|
|
|
2292
2749
|
signal: gate.signal,
|
|
2293
2750
|
}, createLineConsumer(handleLine));
|
|
2294
2751
|
gate.commit(() => {
|
|
2295
|
-
const check = checkSpec(currentSpec, validate);
|
|
2752
|
+
const check = checkSpec(currentSpec, validate, checkOptions);
|
|
2296
2753
|
reportSpecCheck(check, validate);
|
|
2297
2754
|
if (check.spec) {
|
|
2298
2755
|
currentSpec = check.spec;
|
|
@@ -2303,8 +2760,11 @@ function injectUIStream(options) {
|
|
|
2303
2760
|
// app's onComplete is where a spec usually gets persisted, and handing
|
|
2304
2761
|
// it one that cannot render is the outcome this mode is asked to
|
|
2305
2762
|
// prevent. The spec stays put so the app can show or discard it.
|
|
2306
|
-
|
|
2307
|
-
|
|
2763
|
+
// A limit the app set fails the generation whatever the mode: the
|
|
2764
|
+
// spec it would hand `onComplete` is one the app already said it did
|
|
2765
|
+
// not want rendered.
|
|
2766
|
+
if (check.blocked || (validate === 'strict' && check.hasErrors)) {
|
|
2767
|
+
const invalid = new Error(`Generated spec failed validation:\n${formatSpecCheckIssues(check.issues)}`);
|
|
2308
2768
|
error.set(invalid);
|
|
2309
2769
|
options.onError?.(invalid);
|
|
2310
2770
|
return;
|
|
@@ -2361,6 +2821,10 @@ function injectChatUI(options) {
|
|
|
2361
2821
|
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
2362
2822
|
const issues = signal([], ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
|
|
2363
2823
|
const validate = options.validate ?? 'off';
|
|
2824
|
+
const checkOptions = {
|
|
2825
|
+
limits: options.renderLimits ?? null,
|
|
2826
|
+
catalog: options.catalog ?? null,
|
|
2827
|
+
};
|
|
2364
2828
|
const session = createStreamSession();
|
|
2365
2829
|
inject(DestroyRef).onDestroy(() => session.cancel());
|
|
2366
2830
|
const stop = () => {
|
|
@@ -2448,7 +2912,7 @@ function injectChatUI(options) {
|
|
|
2448
2912
|
gate.commit(() => {
|
|
2449
2913
|
// A reply that only talked has no spec to check, and reporting
|
|
2450
2914
|
// "missing root" for a sentence would be nonsense.
|
|
2451
|
-
const check = checkSpec(hasSpec ? currentSpec : null, validate);
|
|
2915
|
+
const check = checkSpec(hasSpec ? currentSpec : null, validate, checkOptions);
|
|
2452
2916
|
if (hasSpec) {
|
|
2453
2917
|
reportSpecCheck(check, validate);
|
|
2454
2918
|
if (check.spec)
|
|
@@ -2464,8 +2928,11 @@ function injectChatUI(options) {
|
|
|
2464
2928
|
// The reply keeps the fixed spec, so what renders in the bubble is
|
|
2465
2929
|
// what was checked.
|
|
2466
2930
|
messages.update((prev) => prev.map((m) => (m.id === assistantId ? finalMessage : m)));
|
|
2467
|
-
|
|
2468
|
-
|
|
2931
|
+
// A limit the app set fails the generation whatever the mode: the
|
|
2932
|
+
// spec it would hand `onComplete` is one the app already said it did
|
|
2933
|
+
// not want rendered.
|
|
2934
|
+
if (check.blocked || (validate === 'strict' && check.hasErrors)) {
|
|
2935
|
+
const invalid = new Error(`Generated spec failed validation:\n${formatSpecCheckIssues(check.issues)}`);
|
|
2469
2936
|
error.set(invalid);
|
|
2470
2937
|
options.onError?.(invalid);
|
|
2471
2938
|
return;
|
|
@@ -2611,5 +3078,5 @@ const schema = defineSchema((s) => ({
|
|
|
2611
3078
|
* Generated bundle index. Do not edit.
|
|
2612
3079
|
*/
|
|
2613
3080
|
|
|
2614
|
-
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, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage, schema };
|
|
3081
|
+
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, schema };
|
|
2615
3082
|
//# sourceMappingURL=ngx-json-render.mjs.map
|