Hi! Thanks for json-render, directives have been really useful for us. Here's a small feature request around conditions.
Context
Directives (defineDirective) are resolved in props and action params (via resolvePropValue), but not in conditions. In @json-render/core 0.20:
resolveComparisonValue(the right side ofeq,neq,gt,lt…) only resolves{ "$state" }; any other object is compared as is.resolveConditionValue(the left side) only accepts$state,$itemor$index.
Use case
Comparing against a value computed at render time, with the standard directives or a custom one:
{ "visible": { "$count": { "$state": "/cart/items" }, "gt": 0 } }{
"visible": {
"$state": "/product/stock",
"lt": { "$math": "multiply", "a": { "$state": "/product/minStock" }, "b": 2 }
}
}{ "visible": { "$item": "dueDate", "eq": { "$today": true } } }($today would be a custom directive that returns today's date.)
Current behavior
- On the right side, the directive is compared as a literal object: the condition never matches, with no warning.
- On the left side, the condition has no
$state,$itemor$index: its value isundefined, andvalidateSpecrejects it (VisibilityConditionStrictSchema).
The workaround today is to save the value to state first (a setState on some event) and compare against { "$state" }. It's more verbose for whoever writes the spec (or the model generating it), and the value goes stale until that event runs again.
Proposal
Resolve a registered directive on both sides of a comparison. The ctx already carries directives both in an element's visible (the renderer builds the full context) and in $cond (resolvePropValue):
function resolveComparisonValue(value, ctx) {
if (typeof value === "object" && value !== null) {
if ("$state" in value && typeof value.$state === "string") {
return getByPath(ctx.stateModel, value.$state);
}
const directive = findDirective(value, ctx.directives);
if (directive) return directive.resolve(value, ctx);
}
return value;
}
function resolveConditionValue(cond, ctx) {
// … $index and $item as today
const directive = findDirective(cond, ctx.directives);
if (directive) return directive.resolve(cond, ctx);
return getByPath(ctx.stateModel, cond.$state);
}On the left side, the directive gets the object with its operator included (gt, eq…), which its resolve ignores.
To be consistent, this would also need:
- Types:
gt/gte/lt/lteaccepting a directive, and aSingleConditionvariant whose value is a directive. - Schemas:
VisibilityConditionSchemaandVisibilityConditionStrictSchemaaccepting those forms, sovalidateSpecdoesn't flag them as invalid. - Prompt: a line in the conditions section of
catalog.prompt({ directives }), so the model knows it can use a directive on either side.
We tested it with a local patch of @json-render/core 0.20, covering runtime and types. Happy to open a PR if this sounds good, thanks!
Activity
2@harold18m thanks for taking this on and for opening the PR! It covers the right-hand side nicely. I'd just treat the left-hand side as part of this issue rather than an optional follow-up.
The reason: with only the right side, the value being checked still has to come from $state, $item or $index. So something like "show this if the cart count is greater than 0" still wouldn't work without copying the count into state first, which is exactly the workaround I was hoping to get rid of.
Could we keep the issue open until the left side lands too? Also curious how you'd tell a directive apart from $state/$item/$index there. Happy to share my local patch if it helps.
Hi! I'd like to take this one.
Plan:
resolveComparisonValuethrough the same registry lookupresolvePropValueuses (findDirective), sogt: { "$count": ... }works.{ "$count": ..., "gt": 0 }), extendingSingleConditionand its schema.I'd ship (1) first in a small PR, then (2) separately if that direction works for you. Does that sound right?