<a id="binding-state"></a>

# Binding and state

A panel owns the result of one query. Components inside it bind to that result:
keep response aliases and field paths aligned. Repeated components declare the
collection with `t:for`, the item name with `t:as`, and the individual value with
the component's binding attribute. These attributes are the supported DataXML
dialect, not arbitrary Jinja templates embedded in XML.

Declare shared selection and filter state with a `state` element. Its default is
JSON, so a string default includes JSON quotes. A query argument uses `state` to
read that state. Only MagicQuery arguments accept `value` for a literal. For
GraphQL and Lua arguments, declare a state default; GraphQL documents can also
contain ordinary GraphQL literals. A record list updates its declared
selection state with the selected record key. Use stable IDs, not row positions.

The directory example connects a selection to a detail query. The query result
is panel-local; the selected ID is workspace state.

```xml
<workspace id="customer-directory" title="Customer directory" role="Customer service" xmlns:t="urn:cococo:dataxml" domains="customer contact">
  <state name="selectedCustomerId" default="null"/>
  <master-detail>
    <panel id="customers" title="Active customers">
      <query type="magicquery" noun="customers">
        <arg name="active" value="true"/>
        <arg name="limit" value="50"/>
      </query>
      <record-list id="customer-list" t:for="listCustomers.edges" t:as="customer"
        primary="customer.node.name" secondary="customer.node.email"
        key="customer.node.id" select="selectedCustomerId"/>
    </panel>
    <column>
      <panel id="customer" title="Customer profile">
        <query type="magicquery" noun="customer" identifier-state="selectedCustomerId"/>
        <record-detail id="profile" t:text="getCustomer.name">
          <field label="Email" t:value="getCustomer.email"/>
          <field label="Phone" t:value="getCustomer.phone"/>
        </record-detail>
      </panel>
      <panel id="contacts" title="Contacts">
        <query type="magicquery" noun="contacts">
          <arg name="customerId" state="selectedCustomerId"/>
          <arg name="limit" value="100"/>
        </query>
        <record-table id="contact-list" t:for="listContacts.edges" t:as="contact">
          <column label="First name" t:value="contact.node.firstName"/>
          <column label="Last name" t:value="contact.node.lastName"/>
          <column label="Email" t:value="contact.node.email"/>
        </record-table>
      </panel>
    </column>
  </master-detail>
</workspace>
```

An edit form owns its draft separately from panel data. A new query result does
not silently overwrite a dirty draft: the session can offer the pending record
and an explicit discard/keep-editing choice. Do not implement a second draft store
in Lua or a query to imitate this lifecycle. [Forms](workspace-manual://workspace-xml-4/forms) describes submission.

## Query readiness

A panel's `wait-for` attribute names the query variables that require a selection
before loading. Use `wait-for="id"` when `id` depends on the selected record but
nullable date or search variables should still reach the query or Lua handler.
Names refer to query arguments, not state keys. Unknown or duplicate names are
rejected. An explicit empty `wait-for=""` allows all variables to be null.

If the attribute is omitted, any null top-level variable suspends the panel,
including optional arguments. Nested null values do not suspend it. Avoid omitting
`wait-for` on panels with nullable optional filters. The explicit setting currently
supports GraphQL and Lua panels.
The server still validates required GraphQL variables and handler input schemas:
readiness does not make a required input optional.