@json-render/core 0.9.1 → 0.10.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 +1 -1
- package/dist/chunk-4ZGEEX7K.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -889,6 +889,16 @@ function isBindItemExpression(value) {
|
|
|
889
889
|
function isCondExpression(value) {
|
|
890
890
|
return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
|
|
891
891
|
}
|
|
892
|
+
function isComputedExpression(value) {
|
|
893
|
+
return typeof value === "object" && value !== null && "$computed" in value && typeof value.$computed === "string";
|
|
894
|
+
}
|
|
895
|
+
function isTemplateExpression(value) {
|
|
896
|
+
return typeof value === "object" && value !== null && "$template" in value && typeof value.$template === "string";
|
|
897
|
+
}
|
|
898
|
+
var WARNED_COMPUTED_MAX = 100;
|
|
899
|
+
var warnedComputedFns = /* @__PURE__ */ new Set();
|
|
900
|
+
var WARNED_TEMPLATE_MAX = 100;
|
|
901
|
+
var warnedTemplatePaths = /* @__PURE__ */ new Set();
|
|
892
902
|
function resolveBindItemPath(itemPath, ctx) {
|
|
893
903
|
if (ctx.repeatBasePath == null) {
|
|
894
904
|
console.warn(`$bindItem used outside repeat scope: "${itemPath}"`);
|
|
@@ -923,6 +933,46 @@ function resolvePropValue(value, ctx) {
|
|
|
923
933
|
const result = evaluateVisibility(value.$cond, ctx);
|
|
924
934
|
return resolvePropValue(result ? value.$then : value.$else, ctx);
|
|
925
935
|
}
|
|
936
|
+
if (isComputedExpression(value)) {
|
|
937
|
+
const fn = ctx.functions?.[value.$computed];
|
|
938
|
+
if (!fn) {
|
|
939
|
+
if (!warnedComputedFns.has(value.$computed)) {
|
|
940
|
+
if (warnedComputedFns.size < WARNED_COMPUTED_MAX) {
|
|
941
|
+
warnedComputedFns.add(value.$computed);
|
|
942
|
+
}
|
|
943
|
+
console.warn(`Unknown $computed function: "${value.$computed}"`);
|
|
944
|
+
}
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
const resolvedArgs = {};
|
|
948
|
+
if (value.args) {
|
|
949
|
+
for (const [key, arg] of Object.entries(value.args)) {
|
|
950
|
+
resolvedArgs[key] = resolvePropValue(arg, ctx);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return fn(resolvedArgs);
|
|
954
|
+
}
|
|
955
|
+
if (isTemplateExpression(value)) {
|
|
956
|
+
return value.$template.replace(
|
|
957
|
+
/\$\{([^}]+)\}/g,
|
|
958
|
+
(_match, rawPath) => {
|
|
959
|
+
let path = rawPath;
|
|
960
|
+
if (!path.startsWith("/")) {
|
|
961
|
+
if (!warnedTemplatePaths.has(path)) {
|
|
962
|
+
if (warnedTemplatePaths.size < WARNED_TEMPLATE_MAX) {
|
|
963
|
+
warnedTemplatePaths.add(path);
|
|
964
|
+
}
|
|
965
|
+
console.warn(
|
|
966
|
+
`$template path "${path}" should be a JSON Pointer starting with "/". Automatically resolving as "/${path}".`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
path = "/" + path;
|
|
970
|
+
}
|
|
971
|
+
const resolved = getByPath(ctx.stateModel, path);
|
|
972
|
+
return resolved != null ? String(resolved) : "";
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
}
|
|
926
976
|
if (Array.isArray(value)) {
|
|
927
977
|
return value.map((item) => resolvePropValue(item, ctx));
|
|
928
978
|
}
|
|
@@ -1087,6 +1137,10 @@ var ValidationConfigSchema = import_zod4.z.object({
|
|
|
1087
1137
|
validateOn: import_zod4.z.enum(["change", "blur", "submit"]).optional(),
|
|
1088
1138
|
enabled: VisibilityConditionSchema.optional()
|
|
1089
1139
|
});
|
|
1140
|
+
var matchesImpl = (value, args) => {
|
|
1141
|
+
const other = args?.other;
|
|
1142
|
+
return value === other;
|
|
1143
|
+
};
|
|
1090
1144
|
var builtInValidationFunctions = {
|
|
1091
1145
|
/**
|
|
1092
1146
|
* Check if value is not null, undefined, or empty string
|
|
@@ -1176,9 +1230,60 @@ var builtInValidationFunctions = {
|
|
|
1176
1230
|
/**
|
|
1177
1231
|
* Check if value matches another field
|
|
1178
1232
|
*/
|
|
1179
|
-
matches:
|
|
1233
|
+
matches: matchesImpl,
|
|
1234
|
+
/**
|
|
1235
|
+
* Alias for matches with a more descriptive name for cross-field equality
|
|
1236
|
+
*/
|
|
1237
|
+
equalTo: matchesImpl,
|
|
1238
|
+
/**
|
|
1239
|
+
* Check if value is less than another field's value.
|
|
1240
|
+
* Supports numbers, strings (useful for ISO date comparison), and
|
|
1241
|
+
* cross-type numeric coercion (e.g. string "3" vs number 5).
|
|
1242
|
+
*/
|
|
1243
|
+
lessThan: (value, args) => {
|
|
1180
1244
|
const other = args?.other;
|
|
1181
|
-
|
|
1245
|
+
if (value == null || other == null || value === "" || other === "")
|
|
1246
|
+
return false;
|
|
1247
|
+
if (typeof value === "number" && typeof other === "number")
|
|
1248
|
+
return value < other;
|
|
1249
|
+
if (typeof value === "string" && typeof other === "string")
|
|
1250
|
+
return value < other;
|
|
1251
|
+
const numVal = Number(value);
|
|
1252
|
+
const numOther = Number(other);
|
|
1253
|
+
if (!isNaN(numVal) && !isNaN(numOther)) return numVal < numOther;
|
|
1254
|
+
return false;
|
|
1255
|
+
},
|
|
1256
|
+
/**
|
|
1257
|
+
* Check if value is greater than another field's value.
|
|
1258
|
+
* Supports numbers, strings (useful for ISO date comparison), and
|
|
1259
|
+
* cross-type numeric coercion (e.g. string "7" vs number 5).
|
|
1260
|
+
*/
|
|
1261
|
+
greaterThan: (value, args) => {
|
|
1262
|
+
const other = args?.other;
|
|
1263
|
+
if (value == null || other == null || value === "" || other === "")
|
|
1264
|
+
return false;
|
|
1265
|
+
if (typeof value === "number" && typeof other === "number")
|
|
1266
|
+
return value > other;
|
|
1267
|
+
if (typeof value === "string" && typeof other === "string")
|
|
1268
|
+
return value > other;
|
|
1269
|
+
const numVal = Number(value);
|
|
1270
|
+
const numOther = Number(other);
|
|
1271
|
+
if (!isNaN(numVal) && !isNaN(numOther)) return numVal > numOther;
|
|
1272
|
+
return false;
|
|
1273
|
+
},
|
|
1274
|
+
/**
|
|
1275
|
+
* Required only when a condition is met.
|
|
1276
|
+
* Uses JS truthiness: 0, false, "", null, and undefined are all
|
|
1277
|
+
* treated as "condition not met" (field not required), matching
|
|
1278
|
+
* the visibility system's bare-condition semantics.
|
|
1279
|
+
*/
|
|
1280
|
+
requiredIf: (value, args) => {
|
|
1281
|
+
const condition = args?.field;
|
|
1282
|
+
if (!condition) return true;
|
|
1283
|
+
if (value === null || value === void 0) return false;
|
|
1284
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
1285
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
1286
|
+
return true;
|
|
1182
1287
|
}
|
|
1183
1288
|
};
|
|
1184
1289
|
function runValidationCheck(check2, ctx) {
|
|
@@ -1186,7 +1291,7 @@ function runValidationCheck(check2, ctx) {
|
|
|
1186
1291
|
const resolvedArgs = {};
|
|
1187
1292
|
if (check2.args) {
|
|
1188
1293
|
for (const [key, argValue] of Object.entries(check2.args)) {
|
|
1189
|
-
resolvedArgs[key] =
|
|
1294
|
+
resolvedArgs[key] = resolvePropValue(argValue, { stateModel });
|
|
1190
1295
|
}
|
|
1191
1296
|
}
|
|
1192
1297
|
const validationFn = builtInValidationFunctions[check2.type] ?? customFunctions?.[check2.type];
|
|
@@ -1270,10 +1375,34 @@ var check = {
|
|
|
1270
1375
|
type: "url",
|
|
1271
1376
|
message
|
|
1272
1377
|
}),
|
|
1378
|
+
numeric: (message = "Must be a number") => ({
|
|
1379
|
+
type: "numeric",
|
|
1380
|
+
message
|
|
1381
|
+
}),
|
|
1273
1382
|
matches: (otherPath, message = "Fields must match") => ({
|
|
1274
1383
|
type: "matches",
|
|
1275
1384
|
args: { other: { $state: otherPath } },
|
|
1276
1385
|
message
|
|
1386
|
+
}),
|
|
1387
|
+
equalTo: (otherPath, message = "Fields must match") => ({
|
|
1388
|
+
type: "equalTo",
|
|
1389
|
+
args: { other: { $state: otherPath } },
|
|
1390
|
+
message
|
|
1391
|
+
}),
|
|
1392
|
+
lessThan: (otherPath, message) => ({
|
|
1393
|
+
type: "lessThan",
|
|
1394
|
+
args: { other: { $state: otherPath } },
|
|
1395
|
+
message: message ?? "Must be less than the compared field"
|
|
1396
|
+
}),
|
|
1397
|
+
greaterThan: (otherPath, message) => ({
|
|
1398
|
+
type: "greaterThan",
|
|
1399
|
+
args: { other: { $state: otherPath } },
|
|
1400
|
+
message: message ?? "Must be greater than the compared field"
|
|
1401
|
+
}),
|
|
1402
|
+
requiredIf: (fieldPath, message = "This field is required") => ({
|
|
1403
|
+
type: "requiredIf",
|
|
1404
|
+
args: { field: { $state: fieldPath } },
|
|
1405
|
+
message
|
|
1277
1406
|
})
|
|
1278
1407
|
};
|
|
1279
1408
|
|
|
@@ -1342,6 +1471,14 @@ function validateSpec(spec, options = {}) {
|
|
|
1342
1471
|
code: "repeat_in_props"
|
|
1343
1472
|
});
|
|
1344
1473
|
}
|
|
1474
|
+
if (props && "watch" in props && props.watch !== void 0) {
|
|
1475
|
+
issues.push({
|
|
1476
|
+
severity: "error",
|
|
1477
|
+
message: `Element "${key}" has "watch" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
1478
|
+
elementKey: key,
|
|
1479
|
+
code: "watch_in_props"
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1345
1482
|
}
|
|
1346
1483
|
if (checkOrphans) {
|
|
1347
1484
|
const reachable = /* @__PURE__ */ new Set();
|
|
@@ -1409,6 +1546,16 @@ function autoFixSpec(spec) {
|
|
|
1409
1546
|
};
|
|
1410
1547
|
fixes.push(`Moved "repeat" from props to element level on "${key}".`);
|
|
1411
1548
|
}
|
|
1549
|
+
currentProps = fixed.props;
|
|
1550
|
+
if (currentProps && "watch" in currentProps && currentProps.watch !== void 0) {
|
|
1551
|
+
const { watch, ...restProps } = currentProps;
|
|
1552
|
+
fixed = {
|
|
1553
|
+
...fixed,
|
|
1554
|
+
props: restProps,
|
|
1555
|
+
watch
|
|
1556
|
+
};
|
|
1557
|
+
fixes.push(`Moved "watch" from props to element level on "${key}".`);
|
|
1558
|
+
}
|
|
1412
1559
|
fixedElements[key] = fixed;
|
|
1413
1560
|
}
|
|
1414
1561
|
return {
|
|
@@ -1894,6 +2041,29 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1894
2041
|
"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."
|
|
1895
2042
|
);
|
|
1896
2043
|
lines.push("");
|
|
2044
|
+
lines.push(
|
|
2045
|
+
'4. Template: `{ "$template": "Hello, ${/name}!" }` - interpolates `${/path}` references in the string with values from the state model.'
|
|
2046
|
+
);
|
|
2047
|
+
lines.push(
|
|
2048
|
+
' 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.'
|
|
2049
|
+
);
|
|
2050
|
+
lines.push("");
|
|
2051
|
+
const catalogFunctions = catalog.data.functions;
|
|
2052
|
+
if (catalogFunctions && Object.keys(catalogFunctions).length > 0) {
|
|
2053
|
+
lines.push(
|
|
2054
|
+
'5. Computed: `{ "$computed": "<functionName>", "args": { "key": <expression> } }` - calls a registered function with resolved args and returns the result.'
|
|
2055
|
+
);
|
|
2056
|
+
lines.push(
|
|
2057
|
+
' Example: `"value": { "$computed": "fullName", "args": { "first": { "$state": "/form/firstName" }, "last": { "$state": "/form/lastName" } } }`'
|
|
2058
|
+
);
|
|
2059
|
+
lines.push(" Available functions:");
|
|
2060
|
+
for (const name of Object.keys(
|
|
2061
|
+
catalogFunctions
|
|
2062
|
+
)) {
|
|
2063
|
+
lines.push(` - ${name}`);
|
|
2064
|
+
}
|
|
2065
|
+
lines.push("");
|
|
2066
|
+
}
|
|
1897
2067
|
const hasChecksComponents = allComponents ? Object.entries(allComponents).some(([, def]) => {
|
|
1898
2068
|
if (!def.props) return false;
|
|
1899
2069
|
const formatted = formatZodType(def.props);
|
|
@@ -1919,7 +2089,19 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1919
2089
|
lines.push(" - numeric \u2014 value must be a number");
|
|
1920
2090
|
lines.push(" - url \u2014 valid URL format");
|
|
1921
2091
|
lines.push(
|
|
1922
|
-
' - matches \u2014 must equal another field (args: { "other": "
|
|
2092
|
+
' - matches \u2014 must equal another field (args: { "other": { "$state": "/path" } })'
|
|
2093
|
+
);
|
|
2094
|
+
lines.push(
|
|
2095
|
+
' - equalTo \u2014 alias for matches (args: { "other": { "$state": "/path" } })'
|
|
2096
|
+
);
|
|
2097
|
+
lines.push(
|
|
2098
|
+
' - lessThan \u2014 value must be less than another field (args: { "other": { "$state": "/path" } })'
|
|
2099
|
+
);
|
|
2100
|
+
lines.push(
|
|
2101
|
+
' - greaterThan \u2014 value must be greater than another field (args: { "other": { "$state": "/path" } })'
|
|
2102
|
+
);
|
|
2103
|
+
lines.push(
|
|
2104
|
+
' - requiredIf \u2014 required only when another field is truthy (args: { "field": { "$state": "/path" } })'
|
|
1923
2105
|
);
|
|
1924
2106
|
lines.push("");
|
|
1925
2107
|
lines.push("Example:");
|
|
@@ -1935,6 +2117,30 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1935
2117
|
);
|
|
1936
2118
|
lines.push("");
|
|
1937
2119
|
}
|
|
2120
|
+
if (hasCustomActions || hasBuiltInActions) {
|
|
2121
|
+
lines.push("STATE WATCHERS:");
|
|
2122
|
+
lines.push(
|
|
2123
|
+
"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."
|
|
2124
|
+
);
|
|
2125
|
+
lines.push(
|
|
2126
|
+
"Maps state paths (JSON Pointers) to action bindings. When the value at a watched path changes, the bound actions fire automatically."
|
|
2127
|
+
);
|
|
2128
|
+
lines.push("");
|
|
2129
|
+
lines.push(
|
|
2130
|
+
"Example (cascading select \u2014 country changes trigger city loading):"
|
|
2131
|
+
);
|
|
2132
|
+
lines.push(
|
|
2133
|
+
` ${JSON.stringify({ type: "Select", props: { value: { $bindState: "/form/country" }, options: ["US", "Canada", "UK"] }, watch: { "/form/country": { action: "loadCities", params: { country: { $state: "/form/country" } } } }, children: [] })}`
|
|
2134
|
+
);
|
|
2135
|
+
lines.push("");
|
|
2136
|
+
lines.push(
|
|
2137
|
+
"Use `watch` for cascading dependencies where changing one field should trigger side effects (loading data, resetting dependent fields, computing derived values)."
|
|
2138
|
+
);
|
|
2139
|
+
lines.push(
|
|
2140
|
+
"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."
|
|
2141
|
+
);
|
|
2142
|
+
lines.push("");
|
|
2143
|
+
}
|
|
1938
2144
|
lines.push("RULES:");
|
|
1939
2145
|
const baseRules = mode === "chat" ? [
|
|
1940
2146
|
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|