workspaces / workspace-xml-4Markdown ↓

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.

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

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

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

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

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

Published platform manual · revision bf113a064595