workspaces / workspace-xml-4Markdown ↓

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.

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

Published platform manual · revision 6abecd22b42d