<a id="conditional-forms"></a>

# Conditional forms

A fieldset controls whether its inputs validate and submit. Its `when` is a bare,
strictly boolean Jinja expression, without interpolation delimiters. Use `draft`
for logical input values, `present` for explicit presence, and `selection` for a
variant's case key. Paths are absolute even inside a case. The curated
`form-predicate/1` profile limits parsing, evaluation and referenced input sizes.

```xml
<workspace id="conditional-address" title="Shipping address" role="Sales" xmlns:t="urn:cococo:dataxml">
  <state name="shippingEnabled" default="true"/>
  <handler name="saveAddress" mode="action">
    <input>{"type":"object","required":["input"],"properties":{"input":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"},"address":{"type":"object","required":["street","city"],"properties":{"street":{"type":"string","minLength":1},"city":{"type":"string","minLength":1},"settings":{"type":"object"}}}}}}}</input>
    <output>{"type":"object"}</output>
  </handler>
  <panel id="main" title="Shipping address">
    <query type="graphql">query { __typename }</query>
    <form id="address">
      <submit type="lua" handler="saveAddress" errors="errors"/>
      <field path="input.enabled" label="Enable shipping" state="shippingEnabled"/>
      <fieldset id="shipping" title="Shipping details" columns="2" when="draft.input.enabled">
        <field path="input.address.street" label="Street"/>
        <field path="input.address.city" label="City"/>
        <field path="input.address.settings" label="Settings" control="json"/>
      </fieldset>
      <form-actions save-label="Save address"/>
    </form>
  </panel>
</workspace>
```

Its companion Lua only acknowledges the input; it does not save anything. Paste it
into the Lua editor to exercise the form; replace the body with an authorized
application action before using it to save business data.

```lua
-- Demonstration response only; application handlers perform authorized persistence.
exports.saveAddress = function(input)
  return { errors = json.array() }
end
```

Hidden drafts are retained, not cleared. Inactive fields do not validate or appear
in the payload. Hiding a persisted address does not delete that address; clearing
requires explicit mutation semantics. Predicates cannot depend on their own hidden
inputs or an unrelated conditional branch. Required inputs may hide only when the
whole optional object they belong to can be omitted safely.

A variant chooses one typed object shape. OneOf emits exactly one member; a
discriminator emits the chosen discriminator plus that case's fields. Case fields
use relative input paths. Shared fields in a discriminator object must be declared
in each applicable case. Ordinary external fields cannot overlap its owned subtree.

The next example uses an illustrative customer mutation, not an API your
instance provides. Adapt the pattern to a supported mutation from your instance
schema.

```xml
<workspace id="conditional-customer" title="Customer" role="Sales" xmlns:t="urn:cococo:dataxml">
  <panel id="main" title="Customer">
    <query type="graphql">query { __typename }</query>
    <form id="customer">
      <submit type="graphql" errors="saveCustomer.errors">mutation($input:CustomerChoice!){saveCustomer(input:$input){errors{path message}}}</submit>
      <variant id="customer-kind" path="input" encoding="one-of" label="Customer type">
        <case field="person" label="Person">
          <field path="firstName" label="First name"/>
          <field path="lastName" label="Last name"/>
          <field path="notes" label="Notes" control="textarea"/>
        </case>
        <case field="organisation" label="Organisation">
          <field path="name" label="Organisation name"/>
          <field path="registration" label="Registration"/>
        </case>
      </variant>
      <form-actions save-label="Save customer"/>
    </form>
  </panel>
</workspace>
```

Create forms have no implicit case unless `default-case` is declared. Switching
away and back restores the case-local draft. Switching the case of an existing
record is not supported yet, because XML cannot express that a mutation removes
the previous branch. Nested cases have the same rules and a
combined nesting budget. Scalar/list OneOf cases and arbitrary overlapping JSON
Schema alternatives are not supported.

The Lua discriminator example has a nested contact choice. Its declared input
schema gives the handler the same case structure the form validates.

```xml
<workspace id="conditional-nested-lua-customer" title="Customer" role="Sales" xmlns:t="urn:cococo:dataxml">
  <handler name="saveCustomer" mode="action"><input>{"type":"object","additionalProperties":false,"required":["input"],"properties":{"input":{"oneOf":[{"type":"object","additionalProperties":false,"required":["kind","firstName","lastName","contact"],"properties":{"kind":{"type":"string","const":"person"},"firstName":{"type":"string","minLength":1},"lastName":{"type":"string","minLength":1},"notes":{"type":"string"},"contact":{"oneOf":[{"type":"object","additionalProperties":false,"required":["method","address"],"properties":{"method":{"type":"string","const":"email"},"address":{"type":"string","minLength":1}}},{"type":"object","additionalProperties":false,"required":["method","city"],"properties":{"method":{"type":"string","const":"postal"},"city":{"type":"string","minLength":1}}}]}}},{"type":"object","additionalProperties":false,"required":["kind","name"],"properties":{"kind":{"type":"string","const":"organisation"},"name":{"type":"string","minLength":1},"registration":{"type":"string"}}}]}}}</input><output>{"type":"object"}</output></handler>
  <panel id="main" title="Customer">
    <query type="graphql">query { __typename }</query>
    <form id="customer">
      <submit type="lua" handler="saveCustomer" errors="errors"/>
      <variant id="customer-kind" path="input" encoding="discriminator" discriminator="kind" label="Customer type">
        <case value="person" label="Person">
          <field path="firstName" label="First name"/>
          <field path="lastName" label="Last name"/>
          <variant id="contact-method" path="contact" encoding="discriminator" discriminator="method" label="Contact method">
            <case value="email" label="Email"><field path="address" label="Email address"/></case>
            <case value="postal" label="Postal"><field path="city" label="City"/></case>
          </variant>
        </case>
        <case value="organisation" label="Organisation">
          <field path="name" label="Organisation name"/>
          <field path="registration" label="Registration"/>
        </case>
      </variant>
      <form-actions save-label="Save customer"/>
    </form>
  </panel>
</workspace>
```

This companion also acknowledges the validated input without persisting it. The
ERP recipe shows an action that actually calls an authorized business mutation.

```lua
-- Demonstration response; persistence belongs in an authorized application handler.
exports.saveCustomer = function(input)
  return { errors = json.array() }
end
```

After saving active fields, inactive edits can remain unsaved. Preserve that
navigation protection. A delayed response may acknowledge the submitted branch
without switching the current branch or overwriting newer input.

## Conditional expression reference

Profile: `form-predicate/1`. Roots: `draft`, `present`, `selection`. Allowed filters: `default`, `length`, `min`, `max`, `lower`, `upper`, `trim`, `join`, `abs`, `round`, `merge`, `pick`, `omit`, `concat`.

Conditions use bare expressions with a boolean result. String and number values are not implicitly accepted as conditions. Parentheses group expressions. Literals include quoted strings, numbers, `true`, `false`, `null`, arrays and objects.

| Operation | Syntax |
| --- | --- |
| Boolean | `not`, `and`, `or` |
| Comparison | `==`, `!=`, `<`, `<=`, `>`, `>=` |
| Arithmetic | `+`, `-`, `*`, `/`, `//`, `%` |
| String concatenation | `~` |
| Membership | `in`, `not in` |
| Conditional value | `value if condition else fallback` |
| Filter | `value` followed by a pipe and a listed filter, with optional arguments |

References must name declared, eligible form inputs. `draft` uses absolute input paths. `present` indexes one complete input path; `selection` indexes a declared variant ID. Literal subscripts are allowed; computed subscripts are rejected. Ambient Workspace state, function calls and arbitrary JavaScript are unavailable. XML attribute values must escape less-than and ampersand characters.

Limits: 2048 UTF-8 source bytes, depth 32, 512 nodes, 2048 tokens and 1000 evaluation operations. Evaluation strings are limited to 4096 characters and collections to 256 items. Input context is limited to 262144 bytes, input strings to 16384 characters and input collections to 1024 items. Exceeding a limit produces an error; it does not silently deactivate the fieldset.