Skip to content

Cards

Summary

A Card is a single-record form — the most common screen type. Customer Card, Item Card, Sales Order header. This page explains how to build Card ScreenContracts from your server.

What the client does with a Card

The client renders sections as bordered boxes with dot-padded field labels. The user navigates between fields with Tab/arrows and edits with F2/typing. Ctrl+S sends changed fields back to your server.

┌─ General ─────────────────────┐  ┌─ Communication ────────────────┐
│ No............. [10000       ]│  │ Phone........ [+45 70 10 20 30]│
│ Name........... [Cannon Group]│  │ E-Mail....... [ap@cannon.test ]│
│ Balance (LCY).. [45,000.00   ]│  │ Web.......... [www.cannon.test]│
└───────────────────────────────┘  └────────────────────────────────┘

Building a Card

Route

GET /screen/customer_card/{id} → ScreenContract

Response structure

json
{
  "layout": "Card",
  "title": "Customer Card - 10000",
  "sections": [
    {
      "id": "general",
      "label": "General",
      "column": 0,
      "row_group": 0,
      "fields": [
        {
          "id": "no",
          "label": "No.",
          "type": "Text",
          "value": "10000",
          "editable": false
        },
        {
          "id": "name",
          "label": "Name",
          "type": "Text",
          "value": "The Cannon Group",
          "editable": true
        },
        {
          "id": "balance_lcy",
          "label": "Balance (LCY)",
          "type": "Decimal",
          "value": "45,000.00",
          "editable": false,
          "color": "yellow",
          "bold": true,
          "lookup": {
            "endpoint": "/drilldown/balance_lcy/10000"
          }
        }
      ]
    },
    {
      "id": "communication",
      "label": "Communication",
      "column": 1,
      "row_group": 0,
      "fields": [
        {
          "id": "phone",
          "label": "Phone",
          "type": "Phone",
          "value": "+45 70 10 20 30"
        },
        {
          "id": "email",
          "label": "E-Mail",
          "type": "Email",
          "value": "ap@cannongroup.example"
        },
        {
          "id": "home_page",
          "label": "Web",
          "type": "URL",
          "value": "www.cannongroup.example"
        }
      ]
    }
  ],
  "actions": {
    "save": "/screen/customer_card/10000/save",
    "delete": "/screen/customer_card/10000/delete",
    "create": "/screen/customer_card/new"
  }
}

Sections

Sections group related fields. They are positioned on a 2D grid using column and row_group.

Section properties

PropertyTypeDefaultPurpose
idstringrequiredSection identifier
labelstringrequiredDisplay label (shown in the section border)
columninteger0Horizontal position within a row group
row_groupinteger0Vertical band
fieldsarrayrequiredFields in this section

Layout grid

Sections with the same row_group share a horizontal band. Within a band, column determines left-to-right placement:

row_group 0:  [column 0: General]     [column 1: Communication]
row_group 1:  [column 0: Invoicing]   [column 1: Payments]

A single-column card uses column: 0 for all sections. A two-column card uses column: 0 and column: 1.

Example: three-section layout

json
{
  "sections": [
    {
      "id": "general",
      "label": "General",
      "column": 0,
      "row_group": 0,
      "fields": []
    },
    {
      "id": "contact",
      "label": "Contact",
      "column": 1,
      "row_group": 0,
      "fields": []
    },
    {
      "id": "invoicing",
      "label": "Invoicing",
      "column": 0,
      "row_group": 1,
      "fields": []
    }
  ]
}

Result:

┌─ General ──────────┐  ┌─ Contact ──────────┐
│ ...                │  │ ...                │
└────────────────────┘  └────────────────────┘
┌─ Invoicing ────────────────────────────────┐
│ ...                                        │
└────────────────────────────────────────────┘

Fields

Each field in a section describes one data point. See Field types for the complete reference.

Field properties

PropertyTypeDefaultPurpose
idstringrequiredUnique identifier. Used in save changesets.
labelstringrequiredDisplay label (shown with dot-padding)
typestringrequiredField type: Text, Decimal, Date, Option, etc.
valuestring""Current value. Always a string — your server formats it.
editablebooleantrueWhether the field can be edited
widthintegernullCharacter width hint for the input box
quick_entrybooleantrueWhen false, Enter skips this field. Tab still visits all fields.
focusbooleanfalseWhen true, cursor starts on this field. One per form.
placeholderstringnullDimmed hint text when value is empty
validationobjectnullClient-side validation rules
lookupobjectnullLookup binding for relational fields
colorstringnullNamed color for the field value text (e.g. "yellow", "green")
boldbooleanfalseRender the field value bold
optionsarraynullFixed choices for Option fields

Important notes

  • All values are strings. Your server formats numbers ("45,000.00"), dates ("10-03-2026"), booleans ("true"/"false"). The client displays exactly what you send.
  • editable defaults to true. Set editable: false for read-only fields (e.g., calculated values, primary keys).
  • The client derives behavior from field properties. You do not need to declare per-field behavior. A non-editable field with lookup drills down. An Option always cycles. An editable field with lookup supports lookup via Ctrl+Enter.

Example: common field patterns

json
[
  {
    "id": "no",
    "label": "No.",
    "type": "Text",
    "value": "10000",
    "editable": false
  },
  {
    "id": "name",
    "label": "Name",
    "type": "Text",
    "value": "Acme Corp",
    "validation": {
      "required": true,
      "max_length": 100
    }
  },
  {
    "id": "balance",
    "label": "Balance (LCY)",
    "type": "Decimal",
    "value": "45,000.00",
    "editable": false,
    "color": "yellow",
    "bold": true,
    "lookup": {
      "endpoint": "/drilldown/balance_lcy/10000"
    }
  },
  {
    "id": "post_code",
    "label": "Post Code",
    "type": "Text",
    "value": "100",
    "lookup": {
      "endpoint": "/lookup/post_code",
      "display_field": "code",
      "validate": "/validate/post_code",
      "display": "modal"
    }
  },
  {
    "id": "city",
    "label": "City",
    "type": "Text",
    "value": "Tórshavn",
    "editable": false
  },
  {
    "id": "blocked",
    "label": "Blocked",
    "type": "Option",
    "value": "",
    "options": [
      {
        "value": "",
        "label": " "
      },
      {
        "value": "ship",
        "label": "Ship"
      },
      {
        "value": "invoice",
        "label": "Invoice"
      },
      {
        "value": "all",
        "label": "All"
      }
    ]
  },
  {
    "id": "posting_date",
    "label": "Posting Date",
    "type": "Date",
    "value": "10-03-2026"
  },
  {
    "id": "email",
    "label": "E-Mail",
    "type": "Email",
    "value": "ap@acme.example",
    "validation": {
      "pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"
    }
  }
]

Dot-padded labels

The client automatically pads labels with dots so all input boxes align. Just provide the label string.

Quick Entry

Tab and arrow keys visit all fields in document order. Enter is the fast data entry path — it skips fields where quick_entry is false.

Set quick_entry: false on fields that are typically autofilled or rarely edited:

json
{
  "id": "sell_to_name",
  "label": "Name",
  "type": "Text",
  "value": "Cannon Group",
  "quick_entry": false
}

On a sales order, the Enter path might be: Customer No. → Order Date → Posting Date → Your Reference → Payment Terms. Fields like Name, Address, City, Phone (autofilled from the customer lookup) are skipped.

KeyBehavior
TabNext field (all fields, document order)
EnterNext quick_entry field (skips non-essential)
ArrowSpatial movement within/between sections

All fields default to quick_entry: true. Only set it to false where you want Enter to skip.

Quick Entry on grid columns

The same quick_entry property works on grid columns (HeaderLines and Grid screens). On a sales order line grid, Enter skips autofilled columns:

Type → No. → skip Description, UoM, Unit Price (autofilled) → Quantity → next row.

json
{
  "id": "description",
  "label": "Description",
  "type": "Text",
  "editable": true,
  "quick_entry": false
}

Tab still visits every column. Enter is the fast path.

Initial focus

By default, the cursor starts on the first field where quick_entry is true and editable is true.

To override this, set focus: true on one field:

json
{
  "id": "sell_to_no",
  "label": "Customer No.",
  "type": "Text",
  "value": "10000",
  "focus": true
}

This is independent of quick_entry — a field with focus: true still participates in the Enter path normally. Use it when the field you want focused isn't the first one in document order (e.g., it's in the second column).

Only one field per form should have focus: true. If none do, the default applies.

New record (Ctrl+N)

When the user presses Ctrl+N, the client fetches the create action URL:

GET /screen/customer_card/new → ScreenContract (Card with empty fields)

Return a Card with empty or default values. The actions.save URL should point to a create endpoint.

When the user saves the new card:

POST /screen/customer_card/new → ScreenContract (Card with the new record)