Files
WebForm-mw/lib/conditional.ts
2026-05-09 20:08:15 -07:00

81 lines
2.5 KiB
TypeScript

/**
* Visibility engine for form sections and fields.
*
* Evaluates a VisibilityRule tree against the current form state.
* Form state is a plain object {fieldName: value}. Missing fields are
* treated as undefined.
*
* Rule semantics:
* - SimpleCondition: tests a single field with an operator
* - {all: [...]}: AND
* - {any: [...]}: OR
*
* Operator semantics:
* - equals : field value === rule.value
* - notEquals : field value !== rule.value
* - in : rule.values includes the field value
* - notIn : rule.values does not include the field value
* - isEmpty : field is undefined / null / "" / [] / {}
* - isNotEmpty : negation of isEmpty
*
* If a rule is malformed, evaluate returns false (field stays hidden) rather
* than throwing, so a config typo can never break the page.
*/
import type {
VisibilityRule,
SimpleCondition,
AllCondition,
AnyCondition,
} from "@/types/form";
type FormState = Record<string, unknown>;
function isSimple(r: VisibilityRule): r is SimpleCondition {
return typeof (r as SimpleCondition).field === "string";
}
function isAll(r: VisibilityRule): r is AllCondition {
return Array.isArray((r as AllCondition).all);
}
function isAny(r: VisibilityRule): r is AnyCondition {
return Array.isArray((r as AnyCondition).any);
}
function isEmpty(v: unknown): boolean {
if (v === undefined || v === null) return true;
if (typeof v === "string") return v.length === 0;
if (Array.isArray(v)) return v.length === 0;
if (typeof v === "object") return Object.keys(v as object).length === 0;
return false;
}
function evalSimple(c: SimpleCondition, state: FormState): boolean {
const v = state[c.field];
switch (c.op) {
case "equals":
return v === c.value;
case "notEquals":
return v !== c.value;
case "in":
return Array.isArray(c.values) && c.values.includes(v as string | number | boolean);
case "notIn":
return Array.isArray(c.values) && !c.values.includes(v as string | number | boolean);
case "isEmpty":
return isEmpty(v);
case "isNotEmpty":
return !isEmpty(v);
default:
return false;
}
}
export function evaluate(rule: VisibilityRule | undefined, state: FormState): boolean {
if (!rule) return true;
if (isSimple(rule)) return evalSimple(rule, state);
if (isAll(rule)) return rule.all.every((r) => evaluate(r, state));
if (isAny(rule)) return rule.any.some((r) => evaluate(r, state));
return false;
}