#367

[Feature request] Resolve directives in conditions (visible, $cond)

Openeserdeiroopened 1mo ago

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 of eq, neq, gt, lt…) only resolves { "$state" }; any other object is compared as is.
  • resolveConditionValue (the left side) only accepts $state, $item or $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, $item or $index: its value is undefined, and validateSpec rejects 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/lte accepting a directive, and a SingleCondition variant whose value is a directive.
  • Schemas: VisibilityConditionSchema and VisibilityConditionStrictSchema accepting those forms, so validateSpec doesn'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
harold18m5d ago

Hi! I'd like to take this one.

Plan:

  1. Right-hand side: resolve custom directives in resolveComparisonValue through the same registry lookup resolvePropValue uses (findDirective), so gt: { "$count": ... } works.
  2. Left-hand side: let a condition use a directive as its source ({ "$count": ..., "gt": 0 }), extending SingleCondition and its schema.

I'd ship (1) first in a small PR, then (2) separately if that direction works for you. Does that sound right?

eserdeiro4d ago

@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.