@json-render/core 0.9.1 → 0.11.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 +114 -0
- package/dist/{chunk-4ZGEEX7K.mjs → chunk-AFLK3Q4T.mjs} +1 -1
- package/dist/chunk-AFLK3Q4T.mjs.map +1 -0
- package/dist/index.d.mts +25 -4
- package/dist/index.d.ts +25 -4
- package/dist/index.js +210 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +211 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{store-utils-DHnkfKAT.d.mts → store-utils-D98Czbil.d.mts} +6 -0
- package/dist/{store-utils-DHnkfKAT.d.ts → store-utils-D98Czbil.d.ts} +6 -0
- package/dist/store-utils.d.mts +1 -1
- package/dist/store-utils.d.ts +1 -1
- package/dist/store-utils.js.map +1 -1
- package/dist/store-utils.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-4ZGEEX7K.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
removeByPath,
|
|
22
22
|
resolveDynamicValue,
|
|
23
23
|
setByPath
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-AFLK3Q4T.mjs";
|
|
25
25
|
|
|
26
26
|
// src/visibility.ts
|
|
27
27
|
import { z } from "zod";
|
|
@@ -206,6 +206,16 @@ function isBindItemExpression(value) {
|
|
|
206
206
|
function isCondExpression(value) {
|
|
207
207
|
return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
|
|
208
208
|
}
|
|
209
|
+
function isComputedExpression(value) {
|
|
210
|
+
return typeof value === "object" && value !== null && "$computed" in value && typeof value.$computed === "string";
|
|
211
|
+
}
|
|
212
|
+
function isTemplateExpression(value) {
|
|
213
|
+
return typeof value === "object" && value !== null && "$template" in value && typeof value.$template === "string";
|
|
214
|
+
}
|
|
215
|
+
var WARNED_COMPUTED_MAX = 100;
|
|
216
|
+
var warnedComputedFns = /* @__PURE__ */ new Set();
|
|
217
|
+
var WARNED_TEMPLATE_MAX = 100;
|
|
218
|
+
var warnedTemplatePaths = /* @__PURE__ */ new Set();
|
|
209
219
|
function resolveBindItemPath(itemPath, ctx) {
|
|
210
220
|
if (ctx.repeatBasePath == null) {
|
|
211
221
|
console.warn(`$bindItem used outside repeat scope: "${itemPath}"`);
|
|
@@ -240,6 +250,46 @@ function resolvePropValue(value, ctx) {
|
|
|
240
250
|
const result = evaluateVisibility(value.$cond, ctx);
|
|
241
251
|
return resolvePropValue(result ? value.$then : value.$else, ctx);
|
|
242
252
|
}
|
|
253
|
+
if (isComputedExpression(value)) {
|
|
254
|
+
const fn = ctx.functions?.[value.$computed];
|
|
255
|
+
if (!fn) {
|
|
256
|
+
if (!warnedComputedFns.has(value.$computed)) {
|
|
257
|
+
if (warnedComputedFns.size < WARNED_COMPUTED_MAX) {
|
|
258
|
+
warnedComputedFns.add(value.$computed);
|
|
259
|
+
}
|
|
260
|
+
console.warn(`Unknown $computed function: "${value.$computed}"`);
|
|
261
|
+
}
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
const resolvedArgs = {};
|
|
265
|
+
if (value.args) {
|
|
266
|
+
for (const [key, arg] of Object.entries(value.args)) {
|
|
267
|
+
resolvedArgs[key] = resolvePropValue(arg, ctx);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return fn(resolvedArgs);
|
|
271
|
+
}
|
|
272
|
+
if (isTemplateExpression(value)) {
|
|
273
|
+
return value.$template.replace(
|
|
274
|
+
/\$\{([^}]+)\}/g,
|
|
275
|
+
(_match, rawPath) => {
|
|
276
|
+
let path = rawPath;
|
|
277
|
+
if (!path.startsWith("/")) {
|
|
278
|
+
if (!warnedTemplatePaths.has(path)) {
|
|
279
|
+
if (warnedTemplatePaths.size < WARNED_TEMPLATE_MAX) {
|
|
280
|
+
warnedTemplatePaths.add(path);
|
|
281
|
+
}
|
|
282
|
+
console.warn(
|
|
283
|
+
`$template path "${path}" should be a JSON Pointer starting with "/". Automatically resolving as "/${path}".`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
path = "/" + path;
|
|
287
|
+
}
|
|
288
|
+
const resolved = getByPath(ctx.stateModel, path);
|
|
289
|
+
return resolved != null ? String(resolved) : "";
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
}
|
|
243
293
|
if (Array.isArray(value)) {
|
|
244
294
|
return value.map((item) => resolvePropValue(item, ctx));
|
|
245
295
|
}
|
|
@@ -404,6 +454,10 @@ var ValidationConfigSchema = z3.object({
|
|
|
404
454
|
validateOn: z3.enum(["change", "blur", "submit"]).optional(),
|
|
405
455
|
enabled: VisibilityConditionSchema.optional()
|
|
406
456
|
});
|
|
457
|
+
var matchesImpl = (value, args) => {
|
|
458
|
+
const other = args?.other;
|
|
459
|
+
return value === other;
|
|
460
|
+
};
|
|
407
461
|
var builtInValidationFunctions = {
|
|
408
462
|
/**
|
|
409
463
|
* Check if value is not null, undefined, or empty string
|
|
@@ -493,9 +547,60 @@ var builtInValidationFunctions = {
|
|
|
493
547
|
/**
|
|
494
548
|
* Check if value matches another field
|
|
495
549
|
*/
|
|
496
|
-
matches:
|
|
550
|
+
matches: matchesImpl,
|
|
551
|
+
/**
|
|
552
|
+
* Alias for matches with a more descriptive name for cross-field equality
|
|
553
|
+
*/
|
|
554
|
+
equalTo: matchesImpl,
|
|
555
|
+
/**
|
|
556
|
+
* Check if value is less than another field's value.
|
|
557
|
+
* Supports numbers, strings (useful for ISO date comparison), and
|
|
558
|
+
* cross-type numeric coercion (e.g. string "3" vs number 5).
|
|
559
|
+
*/
|
|
560
|
+
lessThan: (value, args) => {
|
|
561
|
+
const other = args?.other;
|
|
562
|
+
if (value == null || other == null || value === "" || other === "")
|
|
563
|
+
return false;
|
|
564
|
+
if (typeof value === "number" && typeof other === "number")
|
|
565
|
+
return value < other;
|
|
566
|
+
if (typeof value === "string" && typeof other === "string")
|
|
567
|
+
return value < other;
|
|
568
|
+
const numVal = Number(value);
|
|
569
|
+
const numOther = Number(other);
|
|
570
|
+
if (!isNaN(numVal) && !isNaN(numOther)) return numVal < numOther;
|
|
571
|
+
return false;
|
|
572
|
+
},
|
|
573
|
+
/**
|
|
574
|
+
* Check if value is greater than another field's value.
|
|
575
|
+
* Supports numbers, strings (useful for ISO date comparison), and
|
|
576
|
+
* cross-type numeric coercion (e.g. string "7" vs number 5).
|
|
577
|
+
*/
|
|
578
|
+
greaterThan: (value, args) => {
|
|
497
579
|
const other = args?.other;
|
|
498
|
-
|
|
580
|
+
if (value == null || other == null || value === "" || other === "")
|
|
581
|
+
return false;
|
|
582
|
+
if (typeof value === "number" && typeof other === "number")
|
|
583
|
+
return value > other;
|
|
584
|
+
if (typeof value === "string" && typeof other === "string")
|
|
585
|
+
return value > other;
|
|
586
|
+
const numVal = Number(value);
|
|
587
|
+
const numOther = Number(other);
|
|
588
|
+
if (!isNaN(numVal) && !isNaN(numOther)) return numVal > numOther;
|
|
589
|
+
return false;
|
|
590
|
+
},
|
|
591
|
+
/**
|
|
592
|
+
* Required only when a condition is met.
|
|
593
|
+
* Uses JS truthiness: 0, false, "", null, and undefined are all
|
|
594
|
+
* treated as "condition not met" (field not required), matching
|
|
595
|
+
* the visibility system's bare-condition semantics.
|
|
596
|
+
*/
|
|
597
|
+
requiredIf: (value, args) => {
|
|
598
|
+
const condition = args?.field;
|
|
599
|
+
if (!condition) return true;
|
|
600
|
+
if (value === null || value === void 0) return false;
|
|
601
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
602
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
603
|
+
return true;
|
|
499
604
|
}
|
|
500
605
|
};
|
|
501
606
|
function runValidationCheck(check2, ctx) {
|
|
@@ -503,7 +608,7 @@ function runValidationCheck(check2, ctx) {
|
|
|
503
608
|
const resolvedArgs = {};
|
|
504
609
|
if (check2.args) {
|
|
505
610
|
for (const [key, argValue] of Object.entries(check2.args)) {
|
|
506
|
-
resolvedArgs[key] =
|
|
611
|
+
resolvedArgs[key] = resolvePropValue(argValue, { stateModel });
|
|
507
612
|
}
|
|
508
613
|
}
|
|
509
614
|
const validationFn = builtInValidationFunctions[check2.type] ?? customFunctions?.[check2.type];
|
|
@@ -587,10 +692,34 @@ var check = {
|
|
|
587
692
|
type: "url",
|
|
588
693
|
message
|
|
589
694
|
}),
|
|
695
|
+
numeric: (message = "Must be a number") => ({
|
|
696
|
+
type: "numeric",
|
|
697
|
+
message
|
|
698
|
+
}),
|
|
590
699
|
matches: (otherPath, message = "Fields must match") => ({
|
|
591
700
|
type: "matches",
|
|
592
701
|
args: { other: { $state: otherPath } },
|
|
593
702
|
message
|
|
703
|
+
}),
|
|
704
|
+
equalTo: (otherPath, message = "Fields must match") => ({
|
|
705
|
+
type: "equalTo",
|
|
706
|
+
args: { other: { $state: otherPath } },
|
|
707
|
+
message
|
|
708
|
+
}),
|
|
709
|
+
lessThan: (otherPath, message) => ({
|
|
710
|
+
type: "lessThan",
|
|
711
|
+
args: { other: { $state: otherPath } },
|
|
712
|
+
message: message ?? "Must be less than the compared field"
|
|
713
|
+
}),
|
|
714
|
+
greaterThan: (otherPath, message) => ({
|
|
715
|
+
type: "greaterThan",
|
|
716
|
+
args: { other: { $state: otherPath } },
|
|
717
|
+
message: message ?? "Must be greater than the compared field"
|
|
718
|
+
}),
|
|
719
|
+
requiredIf: (fieldPath, message = "This field is required") => ({
|
|
720
|
+
type: "requiredIf",
|
|
721
|
+
args: { field: { $state: fieldPath } },
|
|
722
|
+
message
|
|
594
723
|
})
|
|
595
724
|
};
|
|
596
725
|
|
|
@@ -659,6 +788,14 @@ function validateSpec(spec, options = {}) {
|
|
|
659
788
|
code: "repeat_in_props"
|
|
660
789
|
});
|
|
661
790
|
}
|
|
791
|
+
if (props && "watch" in props && props.watch !== void 0) {
|
|
792
|
+
issues.push({
|
|
793
|
+
severity: "error",
|
|
794
|
+
message: `Element "${key}" has "watch" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
795
|
+
elementKey: key,
|
|
796
|
+
code: "watch_in_props"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
662
799
|
}
|
|
663
800
|
if (checkOrphans) {
|
|
664
801
|
const reachable = /* @__PURE__ */ new Set();
|
|
@@ -726,6 +863,16 @@ function autoFixSpec(spec) {
|
|
|
726
863
|
};
|
|
727
864
|
fixes.push(`Moved "repeat" from props to element level on "${key}".`);
|
|
728
865
|
}
|
|
866
|
+
currentProps = fixed.props;
|
|
867
|
+
if (currentProps && "watch" in currentProps && currentProps.watch !== void 0) {
|
|
868
|
+
const { watch, ...restProps } = currentProps;
|
|
869
|
+
fixed = {
|
|
870
|
+
...fixed,
|
|
871
|
+
props: restProps,
|
|
872
|
+
watch
|
|
873
|
+
};
|
|
874
|
+
fixes.push(`Moved "watch" from props to element level on "${key}".`);
|
|
875
|
+
}
|
|
729
876
|
fixedElements[key] = fixed;
|
|
730
877
|
}
|
|
731
878
|
return {
|
|
@@ -1211,6 +1358,29 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1211
1358
|
"Use $bindState for form inputs (text fields, checkboxes, selects, sliders, etc.) and $state for read-only data display. Inside repeat scopes, use $bindItem for form inputs bound to the current item. Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
|
|
1212
1359
|
);
|
|
1213
1360
|
lines.push("");
|
|
1361
|
+
lines.push(
|
|
1362
|
+
'4. Template: `{ "$template": "Hello, ${/name}!" }` - interpolates `${/path}` references in the string with values from the state model.'
|
|
1363
|
+
);
|
|
1364
|
+
lines.push(
|
|
1365
|
+
' Example: `"label": { "$template": "Items: ${/cart/count} | Total: ${/cart/total}" }` renders "Items: 3 | Total: 42.00" when /cart/count is 3 and /cart/total is 42.00.'
|
|
1366
|
+
);
|
|
1367
|
+
lines.push("");
|
|
1368
|
+
const catalogFunctions = catalog.data.functions;
|
|
1369
|
+
if (catalogFunctions && Object.keys(catalogFunctions).length > 0) {
|
|
1370
|
+
lines.push(
|
|
1371
|
+
'5. Computed: `{ "$computed": "<functionName>", "args": { "key": <expression> } }` - calls a registered function with resolved args and returns the result.'
|
|
1372
|
+
);
|
|
1373
|
+
lines.push(
|
|
1374
|
+
' Example: `"value": { "$computed": "fullName", "args": { "first": { "$state": "/form/firstName" }, "last": { "$state": "/form/lastName" } } }`'
|
|
1375
|
+
);
|
|
1376
|
+
lines.push(" Available functions:");
|
|
1377
|
+
for (const name of Object.keys(
|
|
1378
|
+
catalogFunctions
|
|
1379
|
+
)) {
|
|
1380
|
+
lines.push(` - ${name}`);
|
|
1381
|
+
}
|
|
1382
|
+
lines.push("");
|
|
1383
|
+
}
|
|
1214
1384
|
const hasChecksComponents = allComponents ? Object.entries(allComponents).some(([, def]) => {
|
|
1215
1385
|
if (!def.props) return false;
|
|
1216
1386
|
const formatted = formatZodType(def.props);
|
|
@@ -1236,7 +1406,19 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1236
1406
|
lines.push(" - numeric \u2014 value must be a number");
|
|
1237
1407
|
lines.push(" - url \u2014 valid URL format");
|
|
1238
1408
|
lines.push(
|
|
1239
|
-
' - matches \u2014 must equal another field (args: { "other": "
|
|
1409
|
+
' - matches \u2014 must equal another field (args: { "other": { "$state": "/path" } })'
|
|
1410
|
+
);
|
|
1411
|
+
lines.push(
|
|
1412
|
+
' - equalTo \u2014 alias for matches (args: { "other": { "$state": "/path" } })'
|
|
1413
|
+
);
|
|
1414
|
+
lines.push(
|
|
1415
|
+
' - lessThan \u2014 value must be less than another field (args: { "other": { "$state": "/path" } })'
|
|
1416
|
+
);
|
|
1417
|
+
lines.push(
|
|
1418
|
+
' - greaterThan \u2014 value must be greater than another field (args: { "other": { "$state": "/path" } })'
|
|
1419
|
+
);
|
|
1420
|
+
lines.push(
|
|
1421
|
+
' - requiredIf \u2014 required only when another field is truthy (args: { "field": { "$state": "/path" } })'
|
|
1240
1422
|
);
|
|
1241
1423
|
lines.push("");
|
|
1242
1424
|
lines.push("Example:");
|
|
@@ -1252,6 +1434,30 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1252
1434
|
);
|
|
1253
1435
|
lines.push("");
|
|
1254
1436
|
}
|
|
1437
|
+
if (hasCustomActions || hasBuiltInActions) {
|
|
1438
|
+
lines.push("STATE WATCHERS:");
|
|
1439
|
+
lines.push(
|
|
1440
|
+
"Elements can have an optional `watch` field to react to state changes and trigger actions. The `watch` field is a top-level field on the element (sibling of type/props/children), NOT inside props."
|
|
1441
|
+
);
|
|
1442
|
+
lines.push(
|
|
1443
|
+
"Maps state paths (JSON Pointers) to action bindings. When the value at a watched path changes, the bound actions fire automatically."
|
|
1444
|
+
);
|
|
1445
|
+
lines.push("");
|
|
1446
|
+
lines.push(
|
|
1447
|
+
"Example (cascading select \u2014 country changes trigger city loading):"
|
|
1448
|
+
);
|
|
1449
|
+
lines.push(
|
|
1450
|
+
` ${JSON.stringify({ type: "Select", props: { value: { $bindState: "/form/country" }, options: ["US", "Canada", "UK"] }, watch: { "/form/country": { action: "loadCities", params: { country: { $state: "/form/country" } } } }, children: [] })}`
|
|
1451
|
+
);
|
|
1452
|
+
lines.push("");
|
|
1453
|
+
lines.push(
|
|
1454
|
+
"Use `watch` for cascading dependencies where changing one field should trigger side effects (loading data, resetting dependent fields, computing derived values)."
|
|
1455
|
+
);
|
|
1456
|
+
lines.push(
|
|
1457
|
+
"IMPORTANT: `watch` is a top-level field on the element (sibling of type/props/children), NOT inside props. Watchers only fire when the value changes, not on initial render."
|
|
1458
|
+
);
|
|
1459
|
+
lines.push("");
|
|
1460
|
+
}
|
|
1255
1461
|
lines.push("RULES:");
|
|
1256
1462
|
const baseRules = mode === "chat" ? [
|
|
1257
1463
|
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|