Form Designer — Conditional Sections: Pre/Post Dependencies¶
Feature: FEAT-234
Applies to:parrot-formdesigner>= (version containing FEAT-234)
This document is the authoritative reference for the conditional-logic system in Form Designer. It covers:
- Pre-dependencies (
depends_on) — a field controlling its own visibility/required state based on values of earlier fields. - Operations (
DependencyOperation) — computed/derived values. - Post-dependencies (
post_depends) — a field triggering effects on other fields. - Authoring via
EditToolkitandget_dependency_rule_snippets. - Optional server-side resolution via
RuleEvaluator.
1. Pre-dependencies (FormField.depends_on)¶
A pre-dependency (DependencyRule) decides whether the owning field is
visible, required, or disabled based on the current values of fields that
appear earlier in the form.
from parrot_formdesigner.core import (
DependencyRule, FieldCondition, ConditionOperator, DependencyOperation,
)
# Simple show/hide rule
rule = DependencyRule(
conditions=[
FieldCondition(field_id="has_dependents", operator=ConditionOperator.EQ, value="yes")
],
logic="and",
effect="show", # "show" | "hide" | "require" | "disable"
)
1.1 Logic gates¶
logic |
Semantics |
|---|---|
"and" |
All conditions must be true (default). |
"or" |
At least one condition must be true. |
"xor" |
Exactly one condition must be true. |
"not" |
Negates the AND-combination of conditions (v1 default per spec §8). |
NOT semantics:
"not"evaluatesnot all(conditions). It is not a prefix NOT on a single condition — it negates the whole group.
# XOR: field only shown when exactly one category is selected
DependencyRule(
conditions=[
FieldCondition(field_id="cat_a", operator=ConditionOperator.EQ, value="selected"),
FieldCondition(field_id="cat_b", operator=ConditionOperator.EQ, value="selected"),
],
logic="xor",
effect="show",
)
1.2 Effects¶
effect |
What happens when the rule fires |
|---|---|
"show" |
Field becomes visible. |
"hide" |
Field is hidden. |
"require" |
Field becomes required. |
"disable" |
Field is hidden (v1: surfaced as not-visible). |
1.3 Supported ConditionOperator values¶
eq · neq · gt · lt · gte · lte · in · not_in ·
is_empty · is_not_empty
Numeric operators (gt/lt/gte/lte) require the referenced field to hold
a coercible-to-float value; a non-numeric value evaluates to False
(safe no-op).
2. Operations (DependencyOperation)¶
An operation computes a derived value and writes it to a target field. Operations can appear in two places:
- Inline in a
DependencyRule— applied when the rule fires. - In a
PostDependency— foreffect="set"oreffect="calc".
from parrot_formdesigner.core import DependencyOperation
op = DependencyOperation(
op="multiply", # see table below
operands=["price", "qty"], # field_ids
target="total", # field_id to write the result to
options=None, # optional extra config (e.g. sep, template, unit)
)
2.1 Operation kinds¶
op |
Description | Notable options |
|---|---|---|
"copy" |
Copy first operand value to target. | — |
"add" |
Sum of all operand values. | — |
"subtract" |
First operand minus remainder. | — |
"multiply" |
Product of all operand values. | — |
"divide" |
First operand divided by remainder. Division by zero → no-op. | — |
"percent" |
base * pct / 100 (operands[0] = base, operands[1] = percent). |
— |
"concat" |
String concatenation of all operands. | sep (separator string) |
"format" |
Positional string template. | template ("{0} {1}") |
"date_diff" |
Days (or weeks) between two ISO date operands. | unit ("days" / "weeks") |
"lookup" |
Server-side table lookup (see open questions below). | — |
"aggregate" |
Numeric aggregate over flat operand list. | fn ("sum" / "avg" / "min" / "max" / "count") |
2.2 Inline operation example (arithmetic)¶
from parrot_formdesigner.core import DependencyRule, FieldCondition, ConditionOperator, DependencyOperation
DependencyRule(
conditions=[], # empty = unconditional
logic="and",
effect="show",
operations=[
DependencyOperation(
op="add",
operands=["base_salary", "bonus"],
target="total_compensation",
)
],
)
3. Post-dependencies (FormField.post_depends)¶
A post-dependency (PostDependency) makes the owning field trigger effects
on other (typically later) fields. This is the "forward" direction —
f1 controlling f2, f3, etc.
from parrot_formdesigner.core import PostDependency, DependencyOperation
# When 'country' changes, reload 'city' options and clear 'zip'
PostDependency(
target="city",
effect="reload_options", # see table below
)
PostDependency(
target="zip",
effect="cascade_clear",
)
3.1 Post-dependency effects¶
effect |
Description |
|---|---|
"show" |
Make the target field visible. |
"hide" |
Hide the target field. |
"require" |
Make the target field required. |
"disable" |
Hide the target field (v1). |
"set" |
Write a value to the target field via an operation. operation is required. |
"calc" |
Compute and write a value; always fires (no condition check). operation is required. |
"reload_options" |
Signal that the target field's option list should be reloaded. |
"cascade_clear" |
Clear the target field's answer when this field changes. |
3.2 Conditions on post-dependencies¶
Post-dependencies MAY include their own conditions list (same format as
pre-dependencies). When conditions is omitted, the rule fires whenever the
owning field has a non-empty answer.
3.3 Example: arithmetic calc¶
PostDependency(
target="total",
effect="calc",
operation=DependencyOperation(
op="multiply",
operands=["price", "qty"],
target="total",
),
)
3.4 Constraints¶
post_dependsis only onFormField(notFormSection/FormSubsection) in v1.- A
PostDependencywitheffect="set"oreffect="calc"must supply anoperation— the Pydantic model validator raisesValueErrorotherwise.
4. End-to-end example¶
The following form has:
- An
xorpre-dependency (amount_typeexactly one path active showsfixed_amountorpercentage_amount). - An arithmetic operation computing
tax=price× 0.21. - A
setpost-dependency cascading a computedsubtotal.
{
"form_id": "invoice_line",
"title": "Invoice Line",
"sections": [
{
"section_id": "main",
"fields": [
{ "field_id": "price", "field_type": "number", "label": "Unit Price" },
{ "field_id": "qty", "field_type": "integer", "label": "Quantity" },
{
"field_id": "subtotal",
"field_type": "number",
"label": "Subtotal",
"depends_on": {
"conditions": [],
"logic": "and",
"effect": "show",
"operations": [
{
"op": "multiply",
"operands": ["price", "qty"],
"target": "subtotal"
}
]
}
},
{ "field_id": "amount_type", "field_type": "select", "label": "Amount Type",
"options": [{"value": "fixed", "label": "Fixed"}, {"value": "pct", "label": "Percentage"}] },
{
"field_id": "fixed_amount",
"field_type": "number",
"label": "Fixed Amount",
"depends_on": {
"conditions": [
{ "field_id": "amount_type", "operator": "eq", "value": "fixed" }
],
"logic": "and",
"effect": "show"
}
},
{
"field_id": "percentage_amount",
"field_type": "number",
"label": "Percentage (%)",
"depends_on": {
"conditions": [
{ "field_id": "amount_type", "operator": "eq", "value": "pct" }
],
"logic": "and",
"effect": "show"
}
},
{
"field_id": "tax",
"field_type": "number",
"label": "VAT (21%)",
"post_depends": []
},
{
"field_id": "discount",
"field_type": "number",
"label": "Discount"
}
]
}
]
}
Python construction equivalent:
from parrot_formdesigner.core import (
DependencyOperation, DependencyRule, FieldCondition, ConditionOperator,
FieldType, FormField, FormSchema, FormSection, PostDependency,
)
price = FormField(field_id="price", field_type=FieldType.NUMBER, label="Unit Price")
subtotal = FormField(
field_id="subtotal",
field_type=FieldType.NUMBER,
label="Subtotal",
depends_on=DependencyRule(
conditions=[],
logic="and",
effect="show",
operations=[
DependencyOperation(op="multiply", operands=["price", "qty"], target="subtotal")
],
),
)
qty = FormField(
field_id="qty",
field_type=FieldType.INTEGER,
label="Quantity",
post_depends=[
PostDependency(
target="subtotal",
effect="calc",
operation=DependencyOperation(op="multiply", operands=["price", "qty"], target="subtotal"),
)
],
)
form = FormSchema(
form_id="invoice_line",
title="Invoice Line",
sections=[FormSection(section_id="main", fields=[price, qty, subtotal])],
)
5. Authoring via EditToolkit¶
The EditToolkit exposes five async methods for dependency CRUD:
from parrot_formdesigner.tools import EditToolkit
toolkit = EditToolkit(form)
# Add a pre-dependency
await toolkit.add_dependency("subtotal", {
"conditions": [{"field_id": "price", "operator": "neq", "value": None}],
"logic": "and",
"effect": "show",
})
# Update existing dependency (merge-patch)
await toolkit.update_dependency("subtotal", {"logic": "or"})
# Remove dependency
await toolkit.remove_dependency("subtotal")
# Add a post-dependency
await toolkit.add_post_dependency("qty", {
"target": "subtotal",
"effect": "calc",
"operation": {"op": "multiply", "operands": ["price", "qty"], "target": "subtotal"},
})
# Remove a post-dependency by target
await toolkit.remove_post_dependency("qty", "subtotal")
All methods validate the rule via FormValidator (rule-integrity pass) before
applying. Invalid rules return an error dict; the form is not mutated.
5.1 Rule capability metadata¶
The control registry exposes capability hints to guide authoring:
from parrot_formdesigner.controls import get_controls
for ctrl in get_controls():
print(ctrl.type, ctrl.supported_operators, ctrl.supported_operations)
FieldControlMetadata now carries:
| Field | Type | Description |
|---|---|---|
supported_operators |
list[str] |
Meaningful ConditionOperator values for this control. |
supported_effects |
list[str] |
Applicable dependency effects. |
supported_operations |
list[str] |
DependencyOperation.op values that make semantic sense. |
Empty list means "all values applicable" (extension controls default to []).
5.2 Rule snippets¶
Quick-start skeletons for building rules:
from parrot_formdesigner.tools import get_dependency_rule_snippets
snippets = get_dependency_rule_snippets()
# snippets["depends_on"] — DependencyRule skeleton dict
# snippets["post_depends"] — list of PostDependency skeleton dicts
6. Server-side evaluation via RuleEvaluator¶
The RuleEvaluator is an optional authoritative Python evaluator.
Renderers (HTML5, Adaptive Card, JSON Schema) MAY interpret the rules
client-side; they may also ignore them if the client UI handles visibility
independently.
import asyncio
from parrot_formdesigner.services import RuleEvaluator
evaluator = RuleEvaluator()
resolution = asyncio.run(evaluator.resolve(form, answers={"qty": 3, "price": 10}))
print(resolution.visible) # dict[str, bool] — True = visible
print(resolution.required) # dict[str, bool]
print(resolution.computed) # dict[str, Any] — computed/operation results
print(resolution.cleared) # list[str] — cascade-cleared field ids
RuleResolution fields:
| Field | Type | Default |
|---|---|---|
visible |
dict[str, bool] |
All True |
required |
dict[str, bool] |
From FormField.required |
computed |
dict[str, Any] |
{} |
cleared |
list[str] |
[] |
7. YAML serialization¶
Pre/post-dependencies are fully supported in YAML forms:
form_id: invoice_line
title: Invoice Line
sections:
- section_id: main
fields:
- field_id: price
field_type: number
label: Unit Price
- field_id: qty
field_type: integer
label: Quantity
post_depends:
- target: subtotal
effect: calc
operation:
op: multiply
operands: [price, qty]
target: subtotal
- field_id: subtotal
field_type: number
label: Subtotal
depends_on:
conditions:
- field_id: price
operator: neq
value: null
logic: and
effect: show
operations:
- op: multiply
operands: [price, qty]
target: subtotal
8. JSON Schema rendering (x- extensions)¶
JsonSchemaRenderer emits depends_on and post_depends as vendor extensions
on each property:
{
"properties": {
"subtotal": {
"type": "number",
"x-depends-on": { "conditions": [...], "logic": "and", "effect": "show" },
"x-post-depends": [{ "target": "...", "effect": "..." }]
}
}
}
JsonSchemaExtractor reconstructs DependencyRule / PostDependency models
from these extensions on import (round-trip lossless).
Note: Renderers that do not understand these extensions will silently ignore them. The rendered schema remains a valid JSON Schema.
9. Open questions / Future work¶
The following behaviours are marked as TODO(FEAT-234 open question) in the code and should be revisited in a future spec revision:
reload_optionstiming: The server-side evaluator sets a sentinel value ("__reload__") incomputed[target]to signal that option reload is needed. The timing and mechanism for actually reloading options (e.g., async callback) is not yet specified.- ARRAY-operand aggregation scope: The
aggregateoperation currently aggregates over a flat list of operand values. Aggregating over repeated ARRAY row values (e.g., sum all values of a specific sub-field across rows) is a future extension. - Nested boolean condition trees: v1 uses a flat list of conditions under a
single
logicgate. Nested boolean trees (AND of ORs, etc.) are a Non-Goal in v1 (spec §1 Non-Goals) and may be addressed in a later feature. lookupoperation server-side: Thelookupoperation producesNoneserver-side (conservative no-op); the lookup table format and resolution mechanism are not yet defined.
10. Validation¶
Rule integrity is validated by FormValidator.validate() (or the standalone
FormValidator.validate_rules() / check_schema()):
- Unknown references: conditions, operation operands, and post-dep targets
must reference existing
field_idvalues. - Ordering violations:
depends_onconditions must reference earlier fields;post_dependstargets must reference later fields. - Type compatibility: numeric operators (
gt/lt/gte/lte) must only be used on numeric field types. - Cycle detection: the validator detects and rejects circular dependency graphs.
from parrot_formdesigner.services import FormValidator
validator = FormValidator()
errors = validator.validate_rules(form) # list[str] — empty = clean
11. Legacy parrot.forms re-exports¶
All new public symbols are re-exported from the parrot.forms backward-compat
shim: