ngx-json-render 0.8.2 → 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 +128 -5
- 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 +716 -514
- 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 +114 -11
|
@@ -23,6 +23,11 @@ class JsonRenderRootContext {
|
|
|
23
23
|
* they apply whatever `validate` is set to.
|
|
24
24
|
*/
|
|
25
25
|
limits = EMPTY;
|
|
26
|
+
/**
|
|
27
|
+
* The catalog the spec was generated for. While loading, an element whose
|
|
28
|
+
* props do not pass its component's schema waits for them.
|
|
29
|
+
*/
|
|
30
|
+
catalog = EMPTY;
|
|
26
31
|
/** External store (controlled mode). */
|
|
27
32
|
store = EMPTY;
|
|
28
33
|
/** Initial state (uncontrolled mode); falls back to `spec.state`. */
|
|
@@ -1106,98 +1111,486 @@ function injectDevtoolsActive() {
|
|
|
1106
1111
|
}
|
|
1107
1112
|
|
|
1108
1113
|
/**
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1114
|
+
* A hostile spec can fail the catalog's schema in as many places as it has
|
|
1115
|
+
* properties. Report enough to act on and stop; the spec is refused either
|
|
1116
|
+
* way, and a console full of them helps nobody.
|
|
1117
|
+
*/
|
|
1118
|
+
const MAX_REPORTED_PROP_ISSUES = 20;
|
|
1119
|
+
/**
|
|
1120
|
+
* Whether this element's `repeat` descends into the item it is already inside,
|
|
1121
|
+
* rather than re-reading a fixed path in state.
|
|
1111
1122
|
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1123
|
+
* A relative statePath — `{"$item": "children"}` — resolves against the
|
|
1124
|
+
* enclosing item, so each pass reads one level further into the data and the
|
|
1125
|
+
* recursion ends where the data does. A plain string is absolute: every pass
|
|
1126
|
+
* reads the same array, and a spec that re-enters itself through one never
|
|
1127
|
+
* terminates.
|
|
1128
|
+
*/
|
|
1129
|
+
function descendsIntoItems(element) {
|
|
1130
|
+
const repeat = element.repeat;
|
|
1131
|
+
return repeat !== undefined && typeof repeat.statePath !== 'string';
|
|
1132
|
+
}
|
|
1133
|
+
/** Every key an element renders below itself: `children` plus every slot. */
|
|
1134
|
+
function childKeysOf(element) {
|
|
1135
|
+
const slots = element.slots;
|
|
1136
|
+
if (!slots)
|
|
1137
|
+
return element.children ?? [];
|
|
1138
|
+
const fromSlots = Object.values(slots).flat();
|
|
1139
|
+
return element.children ? [...element.children, ...fromSlots] : fromSlots;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Walk the element graph once for depth and cycles.
|
|
1117
1143
|
*
|
|
1118
|
-
*
|
|
1119
|
-
* this
|
|
1144
|
+
* Iterative on purpose. A recursive walk would blow the stack on exactly the
|
|
1145
|
+
* specs this exists to catch, which would make the guard a second way to
|
|
1146
|
+
* crash rather than the fix for the first.
|
|
1120
1147
|
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
* - a read of the whole state (`''` or `'/'`), and a condition it can't parse.
|
|
1148
|
+
* Depth is measured from `spec.root`, because that is what renders. Cycles
|
|
1149
|
+
* are swept for across every element, including the ones no parent reaches:
|
|
1150
|
+
* an unreachable cycle is still a defect, and one edit away from a reachable
|
|
1151
|
+
* one.
|
|
1126
1152
|
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1153
|
+
* Not every element that reaches itself is a defect, though. Drawing a tree —
|
|
1154
|
+
* a comment thread, a file browser, a nested menu — means an element that
|
|
1155
|
+
* repeats over a relative path and renders itself for each item, and that ends
|
|
1156
|
+
* when the data does. A back edge is only reported when nothing along it
|
|
1157
|
+
* descends into the data, which is the case that cannot end.
|
|
1158
|
+
*
|
|
1159
|
+
* `depthBelow` is memoised per element, so a shared subtree costs one visit
|
|
1160
|
+
* instead of one per path into it — without that, a spec whose elements each
|
|
1161
|
+
* name the next two is exponential to check. The memo makes the reported
|
|
1162
|
+
* depth a lower bound once a cycle has been cut, since which edge closes the
|
|
1163
|
+
* cycle depends on the order the walk reached it. The cycle is reported as an
|
|
1164
|
+
* error of its own, which is the finding worth acting on.
|
|
1129
1165
|
*/
|
|
1130
|
-
function
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1166
|
+
function analyseSpecGraph(spec) {
|
|
1167
|
+
const elements = spec.elements ?? {};
|
|
1168
|
+
const cycles = new Set();
|
|
1169
|
+
const depthBelow = new Map();
|
|
1170
|
+
const onPath = new Set();
|
|
1171
|
+
const walkFrom = (start) => {
|
|
1172
|
+
const first = elements[start];
|
|
1173
|
+
if (!first || depthBelow.has(start))
|
|
1174
|
+
return;
|
|
1175
|
+
const stack = [
|
|
1176
|
+
{ key: start, kids: childKeysOf(first), next: 0, deepest: 0 },
|
|
1177
|
+
];
|
|
1178
|
+
onPath.add(start);
|
|
1179
|
+
while (stack.length > 0) {
|
|
1180
|
+
const frame = stack[stack.length - 1];
|
|
1181
|
+
if (frame.next < frame.kids.length) {
|
|
1182
|
+
const childKey = frame.kids[frame.next++];
|
|
1183
|
+
const memo = depthBelow.get(childKey);
|
|
1184
|
+
if (memo !== undefined) {
|
|
1185
|
+
frame.deepest = Math.max(frame.deepest, memo);
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
// A child that closes the cycle is not followed — that is what keeps
|
|
1189
|
+
// this walk finite — and not counted towards depth either.
|
|
1190
|
+
if (onPath.has(childKey)) {
|
|
1191
|
+
const from = stack.findIndex((frame) => frame.key === childKey);
|
|
1192
|
+
const descends = stack
|
|
1193
|
+
.slice(from)
|
|
1194
|
+
.some((frame) => descendsIntoItems(elements[frame.key]));
|
|
1195
|
+
if (!descends)
|
|
1196
|
+
cycles.add(childKey);
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
const child = elements[childKey];
|
|
1200
|
+
// A child the spec never defined is validateSpec's finding, not ours.
|
|
1201
|
+
if (!child)
|
|
1202
|
+
continue;
|
|
1203
|
+
onPath.add(childKey);
|
|
1204
|
+
stack.push({
|
|
1205
|
+
key: childKey,
|
|
1206
|
+
kids: childKeysOf(child),
|
|
1207
|
+
next: 0,
|
|
1208
|
+
deepest: 0,
|
|
1209
|
+
});
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
stack.pop();
|
|
1213
|
+
onPath.delete(frame.key);
|
|
1214
|
+
const depth = frame.deepest + 1;
|
|
1215
|
+
depthBelow.set(frame.key, depth);
|
|
1216
|
+
const parent = stack[stack.length - 1];
|
|
1217
|
+
if (parent)
|
|
1218
|
+
parent.deepest = Math.max(parent.deepest, depth);
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
if (spec.root)
|
|
1222
|
+
walkFrom(spec.root);
|
|
1223
|
+
const depth = (spec.root ? depthBelow.get(spec.root) : undefined) ?? 0;
|
|
1224
|
+
for (const key of Object.keys(elements))
|
|
1225
|
+
walkFrom(key);
|
|
1226
|
+
return { depth, cycles: [...cycles] };
|
|
1135
1227
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1228
|
+
/** Report a cycle, naming the element that is its own descendant. */
|
|
1229
|
+
function cycleIssue(key) {
|
|
1230
|
+
return {
|
|
1231
|
+
severity: 'error',
|
|
1232
|
+
code: 'cycle',
|
|
1233
|
+
elementKey: key,
|
|
1234
|
+
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"}.`,
|
|
1235
|
+
};
|
|
1138
1236
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1237
|
+
/** Report a breached element-count cap. */
|
|
1238
|
+
function elementCountIssue(count, limit) {
|
|
1239
|
+
return {
|
|
1240
|
+
severity: 'error',
|
|
1241
|
+
code: 'too_many_elements',
|
|
1242
|
+
message: `Spec has ${count} elements, over the limit of ${limit}. Nothing is rendered: renderLimits.maxElements is a cap on the whole spec.`,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
/** Report a breached depth cap. */
|
|
1246
|
+
function depthIssue(depth, limit) {
|
|
1247
|
+
return {
|
|
1248
|
+
severity: 'error',
|
|
1249
|
+
code: 'too_deep',
|
|
1250
|
+
message: `Spec nests ${depth} levels deep, over the limit of ${limit}. Elements below level ${limit} do not render.`,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Check a spec against a catalog: the component types it names, then the
|
|
1255
|
+
* catalog's spec schema, then each element's props against its own
|
|
1256
|
+
* component's schema.
|
|
1257
|
+
*
|
|
1258
|
+
* The props need that pass of their own. Core builds one schema for the whole
|
|
1259
|
+
* spec, and it gives an element's props a component's schema only when the
|
|
1260
|
+
* catalog has exactly one component; with more, every element's props are an
|
|
1261
|
+
* open record, whatever its `type`. So `catalog.validate` holds an element to
|
|
1262
|
+
* its shape — `children`, a `type` from the catalog — and not to its props.
|
|
1263
|
+
*
|
|
1264
|
+
* Whatever that schema did say about the props of an element checked here is
|
|
1265
|
+
* dropped: with a one-component catalog it would be reported twice, and it
|
|
1266
|
+
* judges expressions this pass knows to leave alone.
|
|
1267
|
+
*
|
|
1268
|
+
* The schema passes are skipped while a type is unknown. That is an error on
|
|
1269
|
+
* its own, and the spec schema would only restate it as a `type` outside the
|
|
1270
|
+
* ones it accepts.
|
|
1271
|
+
*/
|
|
1272
|
+
function catalogIssues(spec, catalog) {
|
|
1273
|
+
const elements = Object.entries(spec.elements ?? {});
|
|
1274
|
+
const known = new Set(catalog.componentNames);
|
|
1275
|
+
const unknown = [];
|
|
1276
|
+
for (const [key, element] of elements) {
|
|
1277
|
+
if (known.has(element.type))
|
|
1278
|
+
continue;
|
|
1279
|
+
unknown.push({
|
|
1280
|
+
severity: 'error',
|
|
1281
|
+
code: 'unknown_component',
|
|
1282
|
+
elementKey: key,
|
|
1283
|
+
message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
|
|
1284
|
+
});
|
|
1148
1285
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
return this.item(value['$item']);
|
|
1157
|
-
if ('$index' in value && value['$index'] === true)
|
|
1158
|
-
return true;
|
|
1159
|
-
// A two-way binding reads the path it writes back to.
|
|
1160
|
-
if (typeof value['$bindState'] === 'string') {
|
|
1161
|
-
return this.state(value['$bindState']);
|
|
1162
|
-
}
|
|
1163
|
-
if (typeof value['$bindItem'] === 'string') {
|
|
1164
|
-
return this.boundItem(value['$bindItem']);
|
|
1165
|
-
}
|
|
1166
|
-
if ('$cond' in value && '$then' in value && '$else' in value) {
|
|
1167
|
-
return (this.condition(value['$cond']) &&
|
|
1168
|
-
this.value(value['$then']) &&
|
|
1169
|
-
this.value(value['$else']));
|
|
1170
|
-
}
|
|
1171
|
-
if (typeof value['$computed'] === 'string') {
|
|
1172
|
-
// The function is handed its resolved arguments and nothing else.
|
|
1173
|
-
const args = value['args'];
|
|
1174
|
-
return (!args || Object.values(Object(args)).every((arg) => this.value(arg)));
|
|
1175
|
-
}
|
|
1176
|
-
if (typeof value['$template'] === 'string') {
|
|
1177
|
-
return this.template(value['$template']);
|
|
1178
|
-
}
|
|
1179
|
-
if (Array.isArray(value))
|
|
1180
|
-
return value.every((item) => this.value(item));
|
|
1181
|
-
if (this.directives) {
|
|
1182
|
-
for (const name of this.directives.keys()) {
|
|
1183
|
-
if (name in value)
|
|
1184
|
-
return false;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1188
|
-
if (key.startsWith('$') || !this.value(entry))
|
|
1189
|
-
return false;
|
|
1190
|
-
}
|
|
1191
|
-
return true;
|
|
1286
|
+
if (unknown.length > 0)
|
|
1287
|
+
return unknown;
|
|
1288
|
+
const schemas = new Map();
|
|
1289
|
+
for (const [key, element] of elements) {
|
|
1290
|
+
const schema = propsSchemaOf(catalog.data, element.type);
|
|
1291
|
+
if (schema)
|
|
1292
|
+
schemas.set(key, schema);
|
|
1192
1293
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1294
|
+
const found = specSchemaIssues(spec, catalog).filter(({ path }) => !(path[0] === 'elements' &&
|
|
1295
|
+
typeof path[1] === 'string' &&
|
|
1296
|
+
schemas.has(path[1]) &&
|
|
1297
|
+
path[2] === 'props'));
|
|
1298
|
+
for (const [key, element] of elements) {
|
|
1299
|
+
const room = MAX_REPORTED_PROP_ISSUES - found.length;
|
|
1300
|
+
if (room <= 0)
|
|
1301
|
+
break;
|
|
1302
|
+
const schema = schemas.get(key);
|
|
1303
|
+
if (schema)
|
|
1304
|
+
found.push(...propsIssues(key, element, schema, room));
|
|
1305
|
+
}
|
|
1306
|
+
return found.slice(0, MAX_REPORTED_PROP_ISSUES).map(toSpecCheckIssue);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Whether an element's props have arrived: they pass its component's schema,
|
|
1310
|
+
* leaving out what the schema says about expressions, the same way
|
|
1311
|
+
* {@link catalogIssues} judges them. The renderer holds an element back on
|
|
1312
|
+
* this while the spec streams.
|
|
1313
|
+
*
|
|
1314
|
+
* The props are taken as written in the spec, not as resolved. What a stream
|
|
1315
|
+
* leaves out is spec, and an expression that is there has arrived, whatever
|
|
1316
|
+
* it resolves to; checking the spec also keeps state writes from re-running
|
|
1317
|
+
* the schema.
|
|
1318
|
+
*
|
|
1319
|
+
* True when the catalog declares no schema that can be called for the type:
|
|
1320
|
+
* there is nothing to wait for. False when the schema throws, or fails
|
|
1321
|
+
* without saying where — the element mounts once loading ends either way.
|
|
1322
|
+
*/
|
|
1323
|
+
function propsArrived(element, catalog) {
|
|
1324
|
+
const schema = propsSchemaOf(catalog.data, element.type);
|
|
1325
|
+
if (!schema)
|
|
1326
|
+
return true;
|
|
1327
|
+
const props = element.props ?? {};
|
|
1328
|
+
let result;
|
|
1329
|
+
try {
|
|
1330
|
+
result = schema.safeParse(props);
|
|
1331
|
+
}
|
|
1332
|
+
catch {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
if (result.success)
|
|
1336
|
+
return true;
|
|
1337
|
+
const issues = readIssues(result.error);
|
|
1338
|
+
return (issues.length > 0 &&
|
|
1339
|
+
issues.every(({ path }) => reachesExpression(props, path)));
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* The props schema a catalog declares for a component type, if it declares
|
|
1343
|
+
* one that can be called.
|
|
1344
|
+
*/
|
|
1345
|
+
function propsSchemaOf(data, type) {
|
|
1346
|
+
const components = data
|
|
1347
|
+
?.components;
|
|
1348
|
+
if (typeof components !== 'object' ||
|
|
1349
|
+
components === null ||
|
|
1350
|
+
!Object.hasOwn(components, type)) {
|
|
1351
|
+
return undefined;
|
|
1352
|
+
}
|
|
1353
|
+
const props = components[type]?.props;
|
|
1354
|
+
return typeof props
|
|
1355
|
+
?.safeParse === 'function'
|
|
1356
|
+
? props
|
|
1357
|
+
: undefined;
|
|
1358
|
+
}
|
|
1359
|
+
/** What the catalog's spec schema reported, with paths rooted at the spec. */
|
|
1360
|
+
function specSchemaIssues(spec, catalog) {
|
|
1361
|
+
try {
|
|
1362
|
+
const result = catalog.validate(spec);
|
|
1363
|
+
return result.success ? [] : readIssues(result.error);
|
|
1364
|
+
}
|
|
1365
|
+
catch (error) {
|
|
1366
|
+
return [{ path: [], message: threwMessage('the catalog schema', error) }];
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* What an element's component schema says about its props, leaving out what
|
|
1371
|
+
* it says about expressions, with paths rooted at the spec.
|
|
1372
|
+
*
|
|
1373
|
+
* A schema that throws counts as a rejection, not a crash. Real ones do:
|
|
1374
|
+
* `ValidationConfigSchema`, which every Material input's `validation` prop
|
|
1375
|
+
* uses, recurses through `enabled` and overflows the stack a thousand
|
|
1376
|
+
* `$and`s deep.
|
|
1377
|
+
*/
|
|
1378
|
+
function propsIssues(key, element, schema, room) {
|
|
1379
|
+
const at = ['elements', key, 'props'];
|
|
1380
|
+
let result;
|
|
1381
|
+
try {
|
|
1382
|
+
result = schema.safeParse(element.props);
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
return [
|
|
1386
|
+
{
|
|
1387
|
+
path: at,
|
|
1388
|
+
message: threwMessage(`the "${element.type}" schema`, error),
|
|
1389
|
+
},
|
|
1390
|
+
];
|
|
1391
|
+
}
|
|
1392
|
+
if (result.success)
|
|
1393
|
+
return [];
|
|
1394
|
+
const issues = [];
|
|
1395
|
+
for (const { path, message } of readIssues(result.error)) {
|
|
1396
|
+
if (issues.length >= room)
|
|
1397
|
+
break;
|
|
1398
|
+
if (reachesExpression(element.props, path))
|
|
1399
|
+
continue;
|
|
1400
|
+
issues.push({ path: [...at, ...path], message });
|
|
1401
|
+
}
|
|
1402
|
+
return issues;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Whether a props issue is about an expression: it sits on one, inside one,
|
|
1406
|
+
* or on a value holding one.
|
|
1407
|
+
*
|
|
1408
|
+
* `{"$state": "/title"}` where the schema wants a string is not a defect.
|
|
1409
|
+
* Core resolves every such value before the component sees it, so the schema
|
|
1410
|
+
* would be judging something that never renders. Any `$`-prefixed key counts
|
|
1411
|
+
* — directive names must start with `$`, and so would an expression added to
|
|
1412
|
+
* core later. A value merely holding one is left alone too, since a union or
|
|
1413
|
+
* a refinement over it ran on the unresolved value.
|
|
1414
|
+
*
|
|
1415
|
+
* The props object itself is not an expression: core resolves the values
|
|
1416
|
+
* inside it, never the object.
|
|
1417
|
+
*/
|
|
1418
|
+
function reachesExpression(props, path) {
|
|
1419
|
+
let value = props;
|
|
1420
|
+
for (const segment of path) {
|
|
1421
|
+
if (typeof value !== 'object' || value === null)
|
|
1422
|
+
return false;
|
|
1423
|
+
value = value[segment];
|
|
1424
|
+
if (isExpression(value))
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
return holdsExpression(value);
|
|
1428
|
+
}
|
|
1429
|
+
/** A value core resolves at render time — or will, once it knows the key. */
|
|
1430
|
+
function isExpression(value) {
|
|
1431
|
+
return (typeof value === 'object' &&
|
|
1432
|
+
value !== null &&
|
|
1433
|
+
!Array.isArray(value) &&
|
|
1434
|
+
Object.keys(value).some((key) => key.startsWith('$')));
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Whether anything nested inside `value` is an expression.
|
|
1438
|
+
*
|
|
1439
|
+
* Iterative, and it remembers what it has seen, for the same reason as
|
|
1440
|
+
* {@link analyseSpecGraph}: props are as attacker-shaped as the rest of the
|
|
1441
|
+
* spec, and a check that overflowed or looped on them would be a second way
|
|
1442
|
+
* to crash.
|
|
1443
|
+
*/
|
|
1444
|
+
function holdsExpression(value) {
|
|
1445
|
+
const pending = [value];
|
|
1446
|
+
const seen = new Set();
|
|
1447
|
+
while (pending.length > 0) {
|
|
1448
|
+
const current = pending.pop();
|
|
1449
|
+
if (typeof current !== 'object' || current === null)
|
|
1450
|
+
continue;
|
|
1451
|
+
if (seen.has(current))
|
|
1452
|
+
continue;
|
|
1453
|
+
seen.add(current);
|
|
1454
|
+
for (const child of Object.values(current)) {
|
|
1455
|
+
if (isExpression(child))
|
|
1456
|
+
return true;
|
|
1457
|
+
pending.push(child);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Read what a schema reported.
|
|
1464
|
+
*
|
|
1465
|
+
* Read defensively rather than typed against the schema library: the shape
|
|
1466
|
+
* below (`issues`, each with `path` and `message`) is what every version of
|
|
1467
|
+
* it has produced, and a renderer that threw while explaining a bad spec
|
|
1468
|
+
* would be the worse failure.
|
|
1469
|
+
*/
|
|
1470
|
+
function readIssues(error) {
|
|
1471
|
+
const raw = error?.issues;
|
|
1472
|
+
if (!Array.isArray(raw))
|
|
1473
|
+
return [];
|
|
1474
|
+
return raw.map((entry) => {
|
|
1475
|
+
const item = entry;
|
|
1476
|
+
const path = item?.path;
|
|
1477
|
+
const message = item?.message;
|
|
1478
|
+
return {
|
|
1479
|
+
path: Array.isArray(path) ? path : [],
|
|
1480
|
+
message: typeof message === 'string' ? message : 'is not valid',
|
|
1481
|
+
};
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
function threwMessage(schema, error) {
|
|
1485
|
+
const reason = error instanceof Error ? ` (${error.name}: ${error.message})` : '';
|
|
1486
|
+
return `${schema} threw instead of answering${reason}, so this counts as rejected`;
|
|
1487
|
+
}
|
|
1488
|
+
function toSpecCheckIssue({ path, message }) {
|
|
1489
|
+
// Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
|
|
1490
|
+
// element in its second segment.
|
|
1491
|
+
const elementKey = path[0] === 'elements' && typeof path[1] === 'string' ? path[1] : undefined;
|
|
1492
|
+
const where = path.length > 0 ? path.map(String).join('.') : 'spec';
|
|
1493
|
+
return {
|
|
1494
|
+
severity: 'error',
|
|
1495
|
+
code: 'invalid_props',
|
|
1496
|
+
...(elementKey === undefined ? {} : { elementKey }),
|
|
1497
|
+
message: `${where}: ${message}`,
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* The state paths an element's expressions can read, or `null` when that
|
|
1503
|
+
* can't be known from the spec.
|
|
1504
|
+
*
|
|
1505
|
+
* Worked out from the spec alone, before anything resolves, so that an element
|
|
1506
|
+
* can depend on those paths rather than on the whole state. The answer may
|
|
1507
|
+
* name more paths than a given resolution reads — both branches of a `$cond`,
|
|
1508
|
+
* say — but never fewer: a path left out would be a write the element misses,
|
|
1509
|
+
* and it would keep showing the old value with nothing in the console.
|
|
1510
|
+
*
|
|
1511
|
+
* `null` is the safe answer, and it is given whenever the spec holds something
|
|
1512
|
+
* this reader does not model:
|
|
1513
|
+
*
|
|
1514
|
+
* - a registered directive, whose `resolve` receives the whole context and may
|
|
1515
|
+
* read any path at all;
|
|
1516
|
+
* - a `$`-prefixed key it doesn't recognise, which is what an expression added
|
|
1517
|
+
* to core after this was written would look like;
|
|
1518
|
+
* - a read of the whole state (`''` or `'/'`), and a condition it can't parse.
|
|
1519
|
+
*
|
|
1520
|
+
* The walk mirrors core's `resolvePropValue` and `evaluateVisibility`, in the
|
|
1521
|
+
* same order, because the order decides which expression an object is.
|
|
1522
|
+
*/
|
|
1523
|
+
function collectStateReads(element, directives, repeatBasePath) {
|
|
1524
|
+
const reader = new StateReader(directives, repeatBasePath);
|
|
1525
|
+
const understood = reader.condition(element.visible) &&
|
|
1526
|
+
Object.values(element.props ?? {}).every((value) => reader.value(value));
|
|
1527
|
+
return understood ? reader.paths : null;
|
|
1528
|
+
}
|
|
1529
|
+
function isObject(value) {
|
|
1530
|
+
return typeof value === 'object' && value !== null;
|
|
1531
|
+
}
|
|
1532
|
+
/** Comparison operators a single condition may carry, as core reads them. */
|
|
1533
|
+
const COMPARISONS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'];
|
|
1534
|
+
class StateReader {
|
|
1535
|
+
directives;
|
|
1536
|
+
repeatBasePath;
|
|
1537
|
+
paths = new Set();
|
|
1538
|
+
constructor(directives, repeatBasePath) {
|
|
1539
|
+
this.directives = directives;
|
|
1540
|
+
this.repeatBasePath = repeatBasePath;
|
|
1541
|
+
}
|
|
1542
|
+
/** A prop value. Returns false when it can read state this can't name. */
|
|
1543
|
+
value(value) {
|
|
1544
|
+
if (!isObject(value))
|
|
1545
|
+
return true;
|
|
1546
|
+
if (typeof value['$state'] === 'string')
|
|
1547
|
+
return this.state(value['$state']);
|
|
1548
|
+
if (typeof value['$item'] === 'string')
|
|
1549
|
+
return this.item(value['$item']);
|
|
1550
|
+
if ('$index' in value && value['$index'] === true)
|
|
1551
|
+
return true;
|
|
1552
|
+
// A two-way binding reads the path it writes back to.
|
|
1553
|
+
if (typeof value['$bindState'] === 'string') {
|
|
1554
|
+
return this.state(value['$bindState']);
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof value['$bindItem'] === 'string') {
|
|
1557
|
+
return this.boundItem(value['$bindItem']);
|
|
1558
|
+
}
|
|
1559
|
+
if ('$cond' in value && '$then' in value && '$else' in value) {
|
|
1560
|
+
return (this.condition(value['$cond']) &&
|
|
1561
|
+
this.value(value['$then']) &&
|
|
1562
|
+
this.value(value['$else']));
|
|
1563
|
+
}
|
|
1564
|
+
if (typeof value['$computed'] === 'string') {
|
|
1565
|
+
// The function is handed its resolved arguments and nothing else.
|
|
1566
|
+
const args = value['args'];
|
|
1567
|
+
return (!args || Object.values(Object(args)).every((arg) => this.value(arg)));
|
|
1568
|
+
}
|
|
1569
|
+
if (typeof value['$template'] === 'string') {
|
|
1570
|
+
return this.template(value['$template']);
|
|
1571
|
+
}
|
|
1572
|
+
if (Array.isArray(value))
|
|
1573
|
+
return value.every((item) => this.value(item));
|
|
1574
|
+
if (this.directives) {
|
|
1575
|
+
for (const name of this.directives.keys()) {
|
|
1576
|
+
if (name in value)
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1581
|
+
if (key.startsWith('$') || !this.value(entry))
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
/** A `visible` condition, or the condition of a `$cond`. */
|
|
1587
|
+
condition(condition) {
|
|
1588
|
+
if (condition === undefined || typeof condition === 'boolean')
|
|
1589
|
+
return true;
|
|
1590
|
+
if (Array.isArray(condition)) {
|
|
1591
|
+
return condition.every((single) => this.singleCondition(single));
|
|
1592
|
+
}
|
|
1593
|
+
if (!isObject(condition))
|
|
1201
1594
|
return false;
|
|
1202
1595
|
for (const group of ['$and', '$or']) {
|
|
1203
1596
|
if (group in condition) {
|
|
@@ -1668,7 +2061,34 @@ class JrElement {
|
|
|
1668
2061
|
const el = this.rawElement();
|
|
1669
2062
|
return el ? this.root.resolveEntry(el.type) : undefined;
|
|
1670
2063
|
}, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
|
|
1671
|
-
|
|
2064
|
+
/** Set once the real component has been shown; see {@link held}. */
|
|
2065
|
+
mounted = false;
|
|
2066
|
+
/**
|
|
2067
|
+
* Whether the component waits for its props: the spec is still loading, a
|
|
2068
|
+
* catalog was given, and the props written so far do not pass the
|
|
2069
|
+
* component's schema (see {@link propsArrived}).
|
|
2070
|
+
*
|
|
2071
|
+
* One-way. Once the component has been shown it is never swapped back for
|
|
2072
|
+
* the fallback, even if a later patch takes a prop away again: a component
|
|
2073
|
+
* flickering out mid-stream is worse than one showing a gap. Once latched
|
|
2074
|
+
* this reads no signals, so a mounted element pays nothing for the gate on
|
|
2075
|
+
* later patches — and neither does any element when nothing is loading.
|
|
2076
|
+
*/
|
|
2077
|
+
held = computed(() => {
|
|
2078
|
+
if (this.mounted)
|
|
2079
|
+
return false;
|
|
2080
|
+
const el = this.rawElement();
|
|
2081
|
+
const catalog = this.root.catalog();
|
|
2082
|
+
const held = !!el && !!catalog && this.root.loading() && !propsArrived(el, catalog);
|
|
2083
|
+
if (!held && this.entry() && this.visible() && !this.refusal()) {
|
|
2084
|
+
this.mounted = true;
|
|
2085
|
+
}
|
|
2086
|
+
return held;
|
|
2087
|
+
}, ...(ngDevMode ? [{ debugName: "held" }] : /* istanbul ignore next */ []));
|
|
2088
|
+
/** What the outlet shows: the component, or its fallback while held. */
|
|
2089
|
+
component = computed(() => this.held()
|
|
2090
|
+
? (this.entry()?.fallback ?? null)
|
|
2091
|
+
: (this.entry()?.component ?? null), ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
|
|
1672
2092
|
devtoolsKey = computed(() => this.devtoolsActive() ? this.elementKey() : null, ...(ngDevMode ? [{ debugName: "devtoolsKey" }] : /* istanbul ignore next */ []));
|
|
1673
2093
|
/**
|
|
1674
2094
|
* Where this element sits in the tree. Passed down to everything it renders
|
|
@@ -1884,432 +2304,163 @@ class JrElement {
|
|
|
1884
2304
|
stateModel: this.state.getSnapshot(),
|
|
1885
2305
|
};
|
|
1886
2306
|
}
|
|
1887
|
-
/**
|
|
1888
|
-
* Fire-and-forget entry point for event emission: dispatching is async, but
|
|
1889
|
-
* a template listener has nowhere to await it, so the failure must be
|
|
1890
|
-
* handled here rather than escaping as an unhandled rejection.
|
|
1891
|
-
*/
|
|
1892
|
-
fireEvent(eventName) {
|
|
1893
|
-
this.emitEvent(eventName).catch(reportActionError);
|
|
1894
|
-
}
|
|
1895
|
-
async emitEvent(eventName) {
|
|
1896
|
-
const binding = untracked(this.rawElement)?.on?.[eventName];
|
|
1897
|
-
if (binding)
|
|
1898
|
-
await this.dispatch(binding);
|
|
1899
|
-
}
|
|
1900
|
-
/**
|
|
1901
|
-
* Run an `on` or `watch` binding's actions in order. Params are resolved
|
|
1902
|
-
* per action, just before it runs, so a `$state` read in a later action of
|
|
1903
|
-
* the chain sees what an earlier one wrote.
|
|
1904
|
-
*/
|
|
1905
|
-
async dispatch(binding) {
|
|
1906
|
-
for (const b of Array.isArray(binding) ? binding : [binding]) {
|
|
1907
|
-
if (!b.params) {
|
|
1908
|
-
await this.actions.execute(b);
|
|
1909
|
-
continue;
|
|
1910
|
-
}
|
|
1911
|
-
const liveCtx = this.liveResolutionCtx();
|
|
1912
|
-
const resolved = {};
|
|
1913
|
-
for (const [key, val] of Object.entries(b.params)) {
|
|
1914
|
-
resolved[key] = resolveActionParam(val, liveCtx);
|
|
1915
|
-
}
|
|
1916
|
-
await this.actions.execute({ ...b, params: resolved });
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
eventHandle(eventName) {
|
|
1920
|
-
const el = this.rawElement();
|
|
1921
|
-
const binding = el?.on?.[eventName];
|
|
1922
|
-
if (!binding) {
|
|
1923
|
-
return { emit: () => { }, shouldPreventDefault: false, bound: false };
|
|
1924
|
-
}
|
|
1925
|
-
const actionBindings = Array.isArray(binding) ? binding : [binding];
|
|
1926
|
-
return {
|
|
1927
|
-
emit: () => this.fireEvent(eventName),
|
|
1928
|
-
shouldPreventDefault: actionBindings.some((b) => b.preventDefault),
|
|
1929
|
-
bound: true,
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1933
|
-
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: `
|
|
1934
|
-
@if (!refusal() && rawElement() && visible() && component()) {
|
|
1935
|
-
@if (devtoolsKey(); as dk) {
|
|
1936
|
-
<span [attr.data-jr-key]="dk" style="display: contents">
|
|
1937
|
-
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
1938
|
-
</span>
|
|
1939
|
-
} @else {
|
|
1940
|
-
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
`, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1944
|
-
}
|
|
1945
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, decorators: [{
|
|
1946
|
-
type: Component,
|
|
1947
|
-
args: [{ selector: 'jr-element', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet], template: `
|
|
1948
|
-
@if (!refusal() && rawElement() && visible() && component()) {
|
|
1949
|
-
@if (devtoolsKey(); as dk) {
|
|
1950
|
-
<span [attr.data-jr-key]="dk" style="display: contents">
|
|
1951
|
-
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
1952
|
-
</span>
|
|
1953
|
-
} @else {
|
|
1954
|
-
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
`, styles: [":host{display:contents}\n"] }]
|
|
1958
|
-
}], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
|
|
1959
|
-
|
|
1960
|
-
/**
|
|
1961
|
-
* A hostile spec can fail the catalog's schema in as many places as it has
|
|
1962
|
-
* properties. Report enough to act on and stop; the spec is refused either
|
|
1963
|
-
* way, and a console full of them helps nobody.
|
|
1964
|
-
*/
|
|
1965
|
-
const MAX_REPORTED_PROP_ISSUES = 20;
|
|
1966
|
-
/**
|
|
1967
|
-
* Whether this element's `repeat` descends into the item it is already inside,
|
|
1968
|
-
* rather than re-reading a fixed path in state.
|
|
1969
|
-
*
|
|
1970
|
-
* A relative statePath — `{"$item": "children"}` — resolves against the
|
|
1971
|
-
* enclosing item, so each pass reads one level further into the data and the
|
|
1972
|
-
* recursion ends where the data does. A plain string is absolute: every pass
|
|
1973
|
-
* reads the same array, and a spec that re-enters itself through one never
|
|
1974
|
-
* terminates.
|
|
1975
|
-
*/
|
|
1976
|
-
function descendsIntoItems(element) {
|
|
1977
|
-
const repeat = element.repeat;
|
|
1978
|
-
return repeat !== undefined && typeof repeat.statePath !== 'string';
|
|
1979
|
-
}
|
|
1980
|
-
/** Every key an element renders below itself: `children` plus every slot. */
|
|
1981
|
-
function childKeysOf(element) {
|
|
1982
|
-
const slots = element.slots;
|
|
1983
|
-
if (!slots)
|
|
1984
|
-
return element.children ?? [];
|
|
1985
|
-
const fromSlots = Object.values(slots).flat();
|
|
1986
|
-
return element.children ? [...element.children, ...fromSlots] : fromSlots;
|
|
1987
|
-
}
|
|
1988
|
-
/**
|
|
1989
|
-
* Walk the element graph once for depth and cycles.
|
|
1990
|
-
*
|
|
1991
|
-
* Iterative on purpose. A recursive walk would blow the stack on exactly the
|
|
1992
|
-
* specs this exists to catch, which would make the guard a second way to
|
|
1993
|
-
* crash rather than the fix for the first.
|
|
1994
|
-
*
|
|
1995
|
-
* Depth is measured from `spec.root`, because that is what renders. Cycles
|
|
1996
|
-
* are swept for across every element, including the ones no parent reaches:
|
|
1997
|
-
* an unreachable cycle is still a defect, and one edit away from a reachable
|
|
1998
|
-
* one.
|
|
1999
|
-
*
|
|
2000
|
-
* Not every element that reaches itself is a defect, though. Drawing a tree —
|
|
2001
|
-
* a comment thread, a file browser, a nested menu — means an element that
|
|
2002
|
-
* repeats over a relative path and renders itself for each item, and that ends
|
|
2003
|
-
* when the data does. A back edge is only reported when nothing along it
|
|
2004
|
-
* descends into the data, which is the case that cannot end.
|
|
2005
|
-
*
|
|
2006
|
-
* `depthBelow` is memoised per element, so a shared subtree costs one visit
|
|
2007
|
-
* instead of one per path into it — without that, a spec whose elements each
|
|
2008
|
-
* name the next two is exponential to check. The memo makes the reported
|
|
2009
|
-
* depth a lower bound once a cycle has been cut, since which edge closes the
|
|
2010
|
-
* cycle depends on the order the walk reached it. The cycle is reported as an
|
|
2011
|
-
* error of its own, which is the finding worth acting on.
|
|
2012
|
-
*/
|
|
2013
|
-
function analyseSpecGraph(spec) {
|
|
2014
|
-
const elements = spec.elements ?? {};
|
|
2015
|
-
const cycles = new Set();
|
|
2016
|
-
const depthBelow = new Map();
|
|
2017
|
-
const onPath = new Set();
|
|
2018
|
-
const walkFrom = (start) => {
|
|
2019
|
-
const first = elements[start];
|
|
2020
|
-
if (!first || depthBelow.has(start))
|
|
2021
|
-
return;
|
|
2022
|
-
const stack = [
|
|
2023
|
-
{ key: start, kids: childKeysOf(first), next: 0, deepest: 0 },
|
|
2024
|
-
];
|
|
2025
|
-
onPath.add(start);
|
|
2026
|
-
while (stack.length > 0) {
|
|
2027
|
-
const frame = stack[stack.length - 1];
|
|
2028
|
-
if (frame.next < frame.kids.length) {
|
|
2029
|
-
const childKey = frame.kids[frame.next++];
|
|
2030
|
-
const memo = depthBelow.get(childKey);
|
|
2031
|
-
if (memo !== undefined) {
|
|
2032
|
-
frame.deepest = Math.max(frame.deepest, memo);
|
|
2033
|
-
continue;
|
|
2034
|
-
}
|
|
2035
|
-
// A child that closes the cycle is not followed — that is what keeps
|
|
2036
|
-
// this walk finite — and not counted towards depth either.
|
|
2037
|
-
if (onPath.has(childKey)) {
|
|
2038
|
-
const from = stack.findIndex((frame) => frame.key === childKey);
|
|
2039
|
-
const descends = stack
|
|
2040
|
-
.slice(from)
|
|
2041
|
-
.some((frame) => descendsIntoItems(elements[frame.key]));
|
|
2042
|
-
if (!descends)
|
|
2043
|
-
cycles.add(childKey);
|
|
2044
|
-
continue;
|
|
2045
|
-
}
|
|
2046
|
-
const child = elements[childKey];
|
|
2047
|
-
// A child the spec never defined is validateSpec's finding, not ours.
|
|
2048
|
-
if (!child)
|
|
2049
|
-
continue;
|
|
2050
|
-
onPath.add(childKey);
|
|
2051
|
-
stack.push({
|
|
2052
|
-
key: childKey,
|
|
2053
|
-
kids: childKeysOf(child),
|
|
2054
|
-
next: 0,
|
|
2055
|
-
deepest: 0,
|
|
2056
|
-
});
|
|
2057
|
-
continue;
|
|
2058
|
-
}
|
|
2059
|
-
stack.pop();
|
|
2060
|
-
onPath.delete(frame.key);
|
|
2061
|
-
const depth = frame.deepest + 1;
|
|
2062
|
-
depthBelow.set(frame.key, depth);
|
|
2063
|
-
const parent = stack[stack.length - 1];
|
|
2064
|
-
if (parent)
|
|
2065
|
-
parent.deepest = Math.max(parent.deepest, depth);
|
|
2066
|
-
}
|
|
2067
|
-
};
|
|
2068
|
-
if (spec.root)
|
|
2069
|
-
walkFrom(spec.root);
|
|
2070
|
-
const depth = (spec.root ? depthBelow.get(spec.root) : undefined) ?? 0;
|
|
2071
|
-
for (const key of Object.keys(elements))
|
|
2072
|
-
walkFrom(key);
|
|
2073
|
-
return { depth, cycles: [...cycles] };
|
|
2074
|
-
}
|
|
2075
|
-
/** Report a cycle, naming the element that is its own descendant. */
|
|
2076
|
-
function cycleIssue(key) {
|
|
2077
|
-
return {
|
|
2078
|
-
severity: 'error',
|
|
2079
|
-
code: 'cycle',
|
|
2080
|
-
elementKey: key,
|
|
2081
|
-
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"}.`,
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
/** Report a breached element-count cap. */
|
|
2085
|
-
function elementCountIssue(count, limit) {
|
|
2086
|
-
return {
|
|
2087
|
-
severity: 'error',
|
|
2088
|
-
code: 'too_many_elements',
|
|
2089
|
-
message: `Spec has ${count} elements, over the limit of ${limit}. Nothing is rendered: renderLimits.maxElements is a cap on the whole spec.`,
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
/** Report a breached depth cap. */
|
|
2093
|
-
function depthIssue(depth, limit) {
|
|
2094
|
-
return {
|
|
2095
|
-
severity: 'error',
|
|
2096
|
-
code: 'too_deep',
|
|
2097
|
-
message: `Spec nests ${depth} levels deep, over the limit of ${limit}. Elements below level ${limit} do not render.`,
|
|
2098
|
-
};
|
|
2099
|
-
}
|
|
2100
|
-
/**
|
|
2101
|
-
* Check a spec against a catalog: the component types it names, then the
|
|
2102
|
-
* catalog's spec schema, then each element's props against its own
|
|
2103
|
-
* component's schema.
|
|
2104
|
-
*
|
|
2105
|
-
* The props need that pass of their own. Core builds one schema for the whole
|
|
2106
|
-
* spec, and it gives an element's props a component's schema only when the
|
|
2107
|
-
* catalog has exactly one component; with more, every element's props are an
|
|
2108
|
-
* open record, whatever its `type`. So `catalog.validate` holds an element to
|
|
2109
|
-
* its shape — `children`, a `type` from the catalog — and not to its props.
|
|
2110
|
-
*
|
|
2111
|
-
* Whatever that schema did say about the props of an element checked here is
|
|
2112
|
-
* dropped: with a one-component catalog it would be reported twice, and it
|
|
2113
|
-
* judges expressions this pass knows to leave alone.
|
|
2114
|
-
*
|
|
2115
|
-
* The schema passes are skipped while a type is unknown. That is an error on
|
|
2116
|
-
* its own, and the spec schema would only restate it as a `type` outside the
|
|
2117
|
-
* ones it accepts.
|
|
2118
|
-
*/
|
|
2119
|
-
function catalogIssues(spec, catalog) {
|
|
2120
|
-
const elements = Object.entries(spec.elements ?? {});
|
|
2121
|
-
const known = new Set(catalog.componentNames);
|
|
2122
|
-
const unknown = [];
|
|
2123
|
-
for (const [key, element] of elements) {
|
|
2124
|
-
if (known.has(element.type))
|
|
2125
|
-
continue;
|
|
2126
|
-
unknown.push({
|
|
2127
|
-
severity: 'error',
|
|
2128
|
-
code: 'unknown_component',
|
|
2129
|
-
elementKey: key,
|
|
2130
|
-
message: `Element "${key}" has type "${element.type}", which this catalog does not define. Nothing renders for it.`,
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
if (unknown.length > 0)
|
|
2134
|
-
return unknown;
|
|
2135
|
-
const schemas = new Map();
|
|
2136
|
-
for (const [key, element] of elements) {
|
|
2137
|
-
const schema = propsSchemaOf(catalog.data, element.type);
|
|
2138
|
-
if (schema)
|
|
2139
|
-
schemas.set(key, schema);
|
|
2307
|
+
/**
|
|
2308
|
+
* Fire-and-forget entry point for event emission: dispatching is async, but
|
|
2309
|
+
* a template listener has nowhere to await it, so the failure must be
|
|
2310
|
+
* handled here rather than escaping as an unhandled rejection.
|
|
2311
|
+
*/
|
|
2312
|
+
fireEvent(eventName) {
|
|
2313
|
+
this.emitEvent(eventName).catch(reportActionError);
|
|
2140
2314
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
for (const [key, element] of elements) {
|
|
2146
|
-
const room = MAX_REPORTED_PROP_ISSUES - found.length;
|
|
2147
|
-
if (room <= 0)
|
|
2148
|
-
break;
|
|
2149
|
-
const schema = schemas.get(key);
|
|
2150
|
-
if (schema)
|
|
2151
|
-
found.push(...propsIssues(key, element, schema, room));
|
|
2315
|
+
async emitEvent(eventName) {
|
|
2316
|
+
const binding = untracked(this.rawElement)?.on?.[eventName];
|
|
2317
|
+
if (binding)
|
|
2318
|
+
await this.dispatch(binding);
|
|
2152
2319
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2320
|
+
/**
|
|
2321
|
+
* Run an `on` or `watch` binding's actions in order. Params are resolved
|
|
2322
|
+
* per action, just before it runs, so a `$state` read in a later action of
|
|
2323
|
+
* the chain sees what an earlier one wrote.
|
|
2324
|
+
*/
|
|
2325
|
+
async dispatch(binding) {
|
|
2326
|
+
for (const b of Array.isArray(binding) ? binding : [binding]) {
|
|
2327
|
+
if (!b.params) {
|
|
2328
|
+
await this.actions.execute(b);
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
const liveCtx = this.liveResolutionCtx();
|
|
2332
|
+
const resolved = {};
|
|
2333
|
+
for (const [key, val] of Object.entries(b.params)) {
|
|
2334
|
+
resolved[key] = resolveActionParam(val, liveCtx);
|
|
2335
|
+
}
|
|
2336
|
+
await this.actions.execute({ ...b, params: resolved });
|
|
2337
|
+
}
|
|
2166
2338
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2339
|
+
eventHandle(eventName) {
|
|
2340
|
+
const el = this.rawElement();
|
|
2341
|
+
const binding = el?.on?.[eventName];
|
|
2342
|
+
if (!binding) {
|
|
2343
|
+
return { emit: () => { }, shouldPreventDefault: false, bound: false };
|
|
2344
|
+
}
|
|
2345
|
+
const actionBindings = Array.isArray(binding) ? binding : [binding];
|
|
2346
|
+
return {
|
|
2347
|
+
emit: () => this.fireEvent(eventName),
|
|
2348
|
+
shouldPreventDefault: actionBindings.some((b) => b.preventDefault),
|
|
2349
|
+
bound: true,
|
|
2350
|
+
};
|
|
2178
2351
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2352
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2353
|
+
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: `
|
|
2354
|
+
@if (!refusal() && rawElement() && visible() && component()) {
|
|
2355
|
+
@if (devtoolsKey(); as dk) {
|
|
2356
|
+
<span [attr.data-jr-key]="dk" style="display: contents">
|
|
2357
|
+
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
2358
|
+
</span>
|
|
2359
|
+
} @else {
|
|
2360
|
+
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
2361
|
+
}
|
|
2181
2362
|
}
|
|
2363
|
+
`, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2182
2364
|
}
|
|
2365
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrElement, decorators: [{
|
|
2366
|
+
type: Component,
|
|
2367
|
+
args: [{ selector: 'jr-element', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgComponentOutlet], template: `
|
|
2368
|
+
@if (!refusal() && rawElement() && visible() && component()) {
|
|
2369
|
+
@if (devtoolsKey(); as dk) {
|
|
2370
|
+
<span [attr.data-jr-key]="dk" style="display: contents">
|
|
2371
|
+
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
2372
|
+
</span>
|
|
2373
|
+
} @else {
|
|
2374
|
+
<ng-container *ngComponentOutlet="component(); injector: outletInjector" />
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
`, styles: [":host{display:contents}\n"] }]
|
|
2378
|
+
}], ctorParameters: () => [], propDecorators: { elementKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementKey", required: true }] }] } });
|
|
2379
|
+
|
|
2183
2380
|
/**
|
|
2184
|
-
*
|
|
2185
|
-
*
|
|
2186
|
-
*
|
|
2187
|
-
* A schema that throws counts as a rejection, not a crash. Real ones do:
|
|
2188
|
-
* `ValidationConfigSchema`, which every Material input's `validation` prop
|
|
2189
|
-
* uses, recurses through `enabled` and overflows the stack a thousand
|
|
2190
|
-
* `$and`s deep.
|
|
2381
|
+
* The defaults the nearest {@link provideJsonRender} supplies, already merged
|
|
2382
|
+
* with any provider above it.
|
|
2191
2383
|
*/
|
|
2192
|
-
|
|
2193
|
-
const at = ['elements', key, 'props'];
|
|
2194
|
-
let result;
|
|
2195
|
-
try {
|
|
2196
|
-
result = schema.safeParse(element.props);
|
|
2197
|
-
}
|
|
2198
|
-
catch (error) {
|
|
2199
|
-
return [
|
|
2200
|
-
{
|
|
2201
|
-
path: at,
|
|
2202
|
-
message: threwMessage(`the "${element.type}" schema`, error),
|
|
2203
|
-
},
|
|
2204
|
-
];
|
|
2205
|
-
}
|
|
2206
|
-
if (result.success)
|
|
2207
|
-
return [];
|
|
2208
|
-
const issues = [];
|
|
2209
|
-
for (const { path, message } of readIssues(result.error)) {
|
|
2210
|
-
if (issues.length >= room)
|
|
2211
|
-
break;
|
|
2212
|
-
if (reachesExpression(element.props, path))
|
|
2213
|
-
continue;
|
|
2214
|
-
issues.push({ path: [...at, ...path], message });
|
|
2215
|
-
}
|
|
2216
|
-
return issues;
|
|
2217
|
-
}
|
|
2384
|
+
const JSON_RENDER_CONFIG = new InjectionToken('JSON_RENDER_CONFIG');
|
|
2218
2385
|
/**
|
|
2219
|
-
*
|
|
2220
|
-
* or on a value holding one.
|
|
2386
|
+
* Set renderer defaults once instead of on every `<json-render>`:
|
|
2221
2387
|
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
* — directive names must start with `$`, and so would an expression added to
|
|
2226
|
-
* core later. A value merely holding one is left alone too, since a union or
|
|
2227
|
-
* a refinement over it ran on the unresolved value.
|
|
2388
|
+
* ```ts
|
|
2389
|
+
* bootstrapApplication(App, { providers: [provideJsonRender({ registry })] });
|
|
2390
|
+
* ```
|
|
2228
2391
|
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
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
|
+
];
|
|
2242
2424
|
}
|
|
2243
|
-
/**
|
|
2244
|
-
function
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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;
|
|
2249
2438
|
}
|
|
2250
2439
|
/**
|
|
2251
|
-
*
|
|
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.
|
|
2252
2442
|
*
|
|
2253
|
-
*
|
|
2254
|
-
* {@link analyseSpecGraph}: props are as attacker-shaped as the rest of the
|
|
2255
|
-
* spec, and a check that overflowed or looped on them would be a second way
|
|
2256
|
-
* to crash.
|
|
2443
|
+
* @internal
|
|
2257
2444
|
*/
|
|
2258
|
-
function
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
continue;
|
|
2265
|
-
if (seen.has(current))
|
|
2266
|
-
continue;
|
|
2267
|
-
seen.add(current);
|
|
2268
|
-
for (const child of Object.values(current)) {
|
|
2269
|
-
if (isExpression(child))
|
|
2270
|
-
return true;
|
|
2271
|
-
pending.push(child);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
return false;
|
|
2445
|
+
function mergeByName(outer, inner) {
|
|
2446
|
+
if (!outer)
|
|
2447
|
+
return inner ?? undefined;
|
|
2448
|
+
if (!inner)
|
|
2449
|
+
return outer;
|
|
2450
|
+
return { ...outer, ...inner };
|
|
2275
2451
|
}
|
|
2276
2452
|
/**
|
|
2277
|
-
*
|
|
2453
|
+
* Two directive lists as one, `inner` winning a name both define — the
|
|
2454
|
+
* directive registry keeps the last definition of a name.
|
|
2278
2455
|
*
|
|
2279
|
-
*
|
|
2280
|
-
* below (`issues`, each with `path` and `message`) is what every version of
|
|
2281
|
-
* it has produced, and a renderer that threw while explaining a bad spec
|
|
2282
|
-
* would be the worse failure.
|
|
2456
|
+
* @internal
|
|
2283
2457
|
*/
|
|
2284
|
-
function
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
const path = item?.path;
|
|
2291
|
-
const message = item?.message;
|
|
2292
|
-
return {
|
|
2293
|
-
path: Array.isArray(path) ? path : [],
|
|
2294
|
-
message: typeof message === 'string' ? message : 'is not valid',
|
|
2295
|
-
};
|
|
2296
|
-
});
|
|
2297
|
-
}
|
|
2298
|
-
function threwMessage(schema, error) {
|
|
2299
|
-
const reason = error instanceof Error ? ` (${error.name}: ${error.message})` : '';
|
|
2300
|
-
return `${schema} threw instead of answering${reason}, so this counts as rejected`;
|
|
2301
|
-
}
|
|
2302
|
-
function toSpecCheckIssue({ path, message }) {
|
|
2303
|
-
// Paths are rooted at the spec, so `elements.<key>.props.<prop>` names the
|
|
2304
|
-
// element in its second segment.
|
|
2305
|
-
const elementKey = path[0] === 'elements' && typeof path[1] === 'string' ? path[1] : undefined;
|
|
2306
|
-
const where = path.length > 0 ? path.map(String).join('.') : 'spec';
|
|
2307
|
-
return {
|
|
2308
|
-
severity: 'error',
|
|
2309
|
-
code: 'invalid_props',
|
|
2310
|
-
...(elementKey === undefined ? {} : { elementKey }),
|
|
2311
|
-
message: `${where}: ${message}`,
|
|
2312
|
-
};
|
|
2458
|
+
function mergeDirectives(outer, inner) {
|
|
2459
|
+
if (!outer)
|
|
2460
|
+
return inner ?? undefined;
|
|
2461
|
+
if (!inner)
|
|
2462
|
+
return outer;
|
|
2463
|
+
return [...outer, ...inner];
|
|
2313
2464
|
}
|
|
2314
2465
|
|
|
2315
2466
|
const CLEAN = {
|
|
@@ -2461,8 +2612,11 @@ function reportSpecCheck(check, mode) {
|
|
|
2461
2612
|
class JsonRenderer {
|
|
2462
2613
|
/** The UI spec to render (may be partial while streaming). */
|
|
2463
2614
|
spec = input.required(...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
|
|
2464
|
-
/**
|
|
2465
|
-
|
|
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 */ []));
|
|
2466
2620
|
/** Whether the spec is currently loading/streaming. */
|
|
2467
2621
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2468
2622
|
/** Fallback component for unknown types. */
|
|
@@ -2479,7 +2633,7 @@ class JsonRenderer {
|
|
|
2479
2633
|
* The check is skipped while `loading` is true: a spec that is still
|
|
2480
2634
|
* arriving is expected to reference children that have not streamed in yet.
|
|
2481
2635
|
*/
|
|
2482
|
-
validate = input(
|
|
2636
|
+
validate = input(undefined, ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
|
|
2483
2637
|
/**
|
|
2484
2638
|
* Caps on what the spec may cost the browser: `maxElements`, `maxDepth` and
|
|
2485
2639
|
* `maxRepeatItems`. Unset by default, and each field is independently
|
|
@@ -2501,8 +2655,14 @@ class JsonRenderer {
|
|
|
2501
2655
|
* `$bindState`, `$template`, a directive — has no value until render time,
|
|
2502
2656
|
* so it is not checked.
|
|
2503
2657
|
*
|
|
2504
|
-
*
|
|
2505
|
-
* skipped while `loading`.
|
|
2658
|
+
* The check is read only when `validate` is on, and — like the rest of
|
|
2659
|
+
* it — skipped while `loading`.
|
|
2660
|
+
*
|
|
2661
|
+
* While `loading`, the catalog also gates mounting, whatever `validate` is:
|
|
2662
|
+
* an element whose props do not yet pass its component's schema shows its
|
|
2663
|
+
* registry entry's `fallback`, or nothing, instead of a component with half
|
|
2664
|
+
* its props. Once an element has mounted it stays mounted, and when
|
|
2665
|
+
* `loading` ends every element mounts.
|
|
2506
2666
|
*/
|
|
2507
2667
|
catalog = input(null, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
|
|
2508
2668
|
/**
|
|
@@ -2530,33 +2690,52 @@ class JsonRenderer {
|
|
|
2530
2690
|
stateStore;
|
|
2531
2691
|
/** The action dispatcher of this renderer. */
|
|
2532
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 */ []));
|
|
2533
2702
|
/**
|
|
2534
2703
|
* The spec as checked, and what the check found. While `loading`, the mode
|
|
2535
2704
|
* is forced off: the spec is still arriving, and the missing children it
|
|
2536
2705
|
* refers to are the normal state of a stream, not a defect.
|
|
2537
2706
|
*/
|
|
2538
|
-
checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.
|
|
2539
|
-
limits: this.
|
|
2540
|
-
catalog: this.
|
|
2707
|
+
checked = computed(() => checkSpec(this.spec(), this.loading() ? 'off' : this.mode(), {
|
|
2708
|
+
limits: this.limits(),
|
|
2709
|
+
catalog: this.specCatalog(),
|
|
2541
2710
|
}), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
|
|
2542
2711
|
constructor() {
|
|
2543
2712
|
const root = inject(JsonRenderRootContext);
|
|
2544
2713
|
// The fixed spec, not the input: a `visible` the check moved out of
|
|
2545
2714
|
// `props` has to be the one the tree renders, or the fix is cosmetic.
|
|
2546
2715
|
root.spec = computed(() => this.checked().spec, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
|
|
2547
|
-
|
|
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 */ []));
|
|
2548
2726
|
root.loading = this.loading;
|
|
2549
|
-
root.fallback = this.fallback;
|
|
2550
|
-
root.limits = 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;
|
|
2551
2730
|
root.store = this.store;
|
|
2552
2731
|
root.initialState = computed(() => this.state() ?? this.spec()?.state ?? {}, ...(ngDevMode ? [{ debugName: "initialState" }] : /* istanbul ignore next */ []));
|
|
2553
|
-
root.handlers = this.handlers;
|
|
2554
|
-
root.onAction = this.onAction;
|
|
2555
|
-
root.navigate = this.navigate;
|
|
2556
|
-
root.validationFunctions = this.validationFunctions;
|
|
2557
|
-
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 */ []));
|
|
2558
2737
|
root.directiveRegistry = computed(() => {
|
|
2559
|
-
const definitions = this.directives();
|
|
2738
|
+
const definitions = mergeDirectives(config.directives, this.directives());
|
|
2560
2739
|
return definitions ? createDirectiveRegistry(definitions) : undefined;
|
|
2561
2740
|
}, ...(ngDevMode ? [{ debugName: "directiveRegistry" }] : /* istanbul ignore next */ []));
|
|
2562
2741
|
root.emitStateChange = (changes) => this.stateChange.emit(changes);
|
|
@@ -2572,7 +2751,25 @@ class JsonRenderer {
|
|
|
2572
2751
|
effect(() => {
|
|
2573
2752
|
if (this.loading())
|
|
2574
2753
|
return;
|
|
2575
|
-
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.`);
|
|
2576
2773
|
});
|
|
2577
2774
|
}
|
|
2578
2775
|
rootKey = computed(() => {
|
|
@@ -2585,7 +2782,7 @@ class JsonRenderer {
|
|
|
2585
2782
|
// A spec whose errors survived the fixes does not render under `strict`.
|
|
2586
2783
|
// Reporting it and drawing half of it anyway is the behaviour the mode
|
|
2587
2784
|
// exists to refuse.
|
|
2588
|
-
if (this.
|
|
2785
|
+
if (this.mode() === 'strict' && checked.hasErrors)
|
|
2589
2786
|
return null;
|
|
2590
2787
|
const spec = checked.spec;
|
|
2591
2788
|
return spec?.root && spec.elements?.[spec.root] ? spec.root : null;
|
|
@@ -2624,7 +2821,7 @@ class JsonRenderer {
|
|
|
2624
2821
|
});
|
|
2625
2822
|
}, ...(ngDevMode ? [{ debugName: "confirmInjector" }] : /* istanbul ignore next */ []));
|
|
2626
2823
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2627
|
-
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: [
|
|
2628
2825
|
JsonRenderRootContext,
|
|
2629
2826
|
JsonRenderStateService,
|
|
2630
2827
|
JsonRenderValidationService,
|
|
@@ -2673,7 +2870,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2673
2870
|
}
|
|
2674
2871
|
}
|
|
2675
2872
|
`, styles: [":host{display:contents}\n"] }]
|
|
2676
|
-
}], 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"] }] } });
|
|
2677
2874
|
|
|
2678
2875
|
/**
|
|
2679
2876
|
* Provides the repeat scope (item, index, absolute base path) to the elements
|
|
@@ -2878,8 +3075,13 @@ function defineRegistry(catalog, options) {
|
|
|
2878
3075
|
// Build the component registry, attaching slot metadata from the catalog.
|
|
2879
3076
|
const registry = {};
|
|
2880
3077
|
if (options.components) {
|
|
2881
|
-
for (const [name,
|
|
2882
|
-
const entry =
|
|
3078
|
+
for (const [name, value] of Object.entries(options.components)) {
|
|
3079
|
+
const entry = typeof value === 'function'
|
|
3080
|
+
? { component: value }
|
|
3081
|
+
: {
|
|
3082
|
+
component: value.component,
|
|
3083
|
+
...(value.fallback ? { fallback: value.fallback } : {}),
|
|
3084
|
+
};
|
|
2883
3085
|
const slots = catalogComponents?.[name]?.slots;
|
|
2884
3086
|
if (slots)
|
|
2885
3087
|
entry.slots = slots;
|
|
@@ -3809,5 +4011,5 @@ function injectChatUI(options) {
|
|
|
3809
4011
|
* Generated bundle index. Do not edit.
|
|
3810
4012
|
*/
|
|
3811
4013
|
|
|
3812
|
-
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 };
|
|
3813
4015
|
//# sourceMappingURL=ngx-json-render.mjs.map
|