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} → ScreenContractResponse 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
| Property | Type | Default | Purpose |
|---|---|---|---|
id | string | required | Section identifier |
label | string | required | Display label (shown in the section border) |
column | integer | 0 | Horizontal position within a row group |
row_group | integer | 0 | Vertical band |
fields | array | required | Fields 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
| Property | Type | Default | Purpose |
|---|---|---|---|
id | string | required | Unique identifier. Used in save changesets. |
label | string | required | Display label (shown with dot-padding) |
type | string | required | Field type: Text, Decimal, Date, Option, etc. |
value | string | "" | Current value. Always a string — your server formats it. |
editable | boolean | true | Whether the field can be edited |
width | integer | null | Character width hint for the input box |
quick_entry | boolean | true | When false, Enter skips this field. Tab still visits all fields. |
focus | boolean | false | When true, cursor starts on this field. One per form. |
placeholder | string | null | Dimmed hint text when value is empty |
validation | object | null | Client-side validation rules |
lookup | object | null | Lookup binding for relational fields |
color | string | null | Named color for the field value text (e.g. "yellow", "green") |
bold | boolean | false | Render the field value bold |
options | array | null | Fixed 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. editabledefaults totrue. Seteditable: falsefor 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
lookupdrills down. AnOptionalways cycles. An editable field withlookupsupports lookup viaCtrl+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.
| Key | Behavior |
|---|---|
| Tab | Next field (all fields, document order) |
| Enter | Next quick_entry field (skips non-essential) |
| Arrow | Spatial 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)