AI: Verified example structures
Every JSON block on this page is checked against the library source: element type strings against the element registry, property names against the builder property datasets, sub-object fields against the TypeScript interfaces. Copy the shapes from here; invent nothing that is not shown or listed in the properties reference.
Names are deliberately generic (el1, column1, ds1) so the structure is what stands out, not the naming.
How to read these examples
- Layout is always
pages→rows→columns→elementRef. Configuration is always the flatelementsmap. See Layout model first. - Anything a container holds is attached to the column that references it, never inside the element.
- A property that does not appear on this page or in the properties reference does not exist. An invented property is dropped silently, so the view ships subtly broken with no error.
1. Skeleton and layout grammar
One page, four bands: full width, even halves, uneven thirds, and a mobile-stacking trio.
{
"schemaVersion": 1,
"settings": {
"language": "en",
"locale": "en-US",
"width": "900px",
"widthUnit": "px",
"renderMode": "page",
"elementSpacing": "12px"
},
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }] },
{ "columns": [{ "elementRef": "el2" }, { "elementRef": "el3" }] },
{ "columns": [{ "elementRef": "el4" }, { "elementRef": "el5" }] },
{
"columns": [
{ "elementRef": "el6" },
{ "elementRef": "el7" },
{ "elementRef": "el8" }
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Page 1", "type": "page", "hideHeader": true },
"el1": { "name": "el1", "label": "Full width", "type": "text" },
"el2": { "name": "el2", "label": "Half", "type": "text" },
"el3": { "name": "el3", "label": "Half", "type": "text" },
"el4": { "name": "el4", "label": "Narrow", "type": "text", "width": "160px" },
"el5": { "name": "el5", "label": "Takes the rest", "type": "text" },
"el6": { "name": "el6", "label": "Third", "type": "text", "mobileWidth": "100%" },
"el7": { "name": "el7", "label": "Third", "type": "text", "mobileWidth": "100%" },
"el8": { "name": "el8", "label": "Third", "type": "text", "mobileWidth": "100%" }
},
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}What to take from it:
el2/el3split 50/50 with no width property at all. Columns with no width getflex: 1 1 0.el4is fixed at160px, soel5absorbs the remainder. Width is on the element, never on the column.mobileWidth: "100%"is what makes a row stack on phones.
2. Containers
2.1 Panels as titled sections
{
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "panel1",
"rows": [
{ "columns": [{ "elementRef": "el1" }, { "elementRef": "el2" }] },
{ "columns": [{ "elementRef": "el3" }] }
]
}
]
},
{
"columns": [
{
"elementRef": "panel2",
"rows": [{ "columns": [{ "elementRef": "el4" }, { "elementRef": "el5" }] }]
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Page 1", "type": "page" },
"panel1": {
"name": "panel1",
"label": "Section one",
"type": "panel",
"showBorder": false,
"panelPadding": "0px",
"titleUnderline": true
},
"panel2": {
"name": "panel2",
"label": "Section two",
"type": "panel",
"showBorder": true,
"panelBorderWidth": "1px",
"panelRadius": "8px",
"panelPadding": "16px",
"contentGap": "12px"
},
"el1": { "name": "el1", "label": "Field 1", "type": "text" },
"el2": { "name": "el2", "label": "Field 2", "type": "text" },
"el3": { "name": "el3", "label": "Field 3", "type": "textarea", "rows": 3 },
"el4": { "name": "el4", "label": "Field 4", "type": "text" },
"el5": { "name": "el5", "label": "Field 5", "type": "text" }
}
}2.2 Tabs (tabRows, keyed by tab value)
{
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "el1",
"tabRows": {
"tab1": [{ "columns": [{ "elementRef": "el2" }, { "elementRef": "el3" }] }],
"tab2": [{ "columns": [{ "elementRef": "el4" }] }]
}
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Page 1", "type": "page" },
"el1": {
"name": "el1",
"label": "Details",
"type": "tabs",
"tabsPosition": "top",
"tabsVariant": "underline",
"fullWidthTabs": false,
"tabs": [
{ "value": "tab1", "label": "General" },
{ "value": "tab2", "label": "Address" }
]
},
"el2": { "name": "el2", "label": "First name", "type": "text" },
"el3": { "name": "el3", "label": "Last name", "type": "text" },
"el4": { "name": "el4", "label": "Street", "type": "text" }
}
}tabsPro is the same element with the same properties; its list property is items instead of tabs.
2.3 Accordion and splitter
Both key their tabRows by the value of each entry in their own list property (items for accordion, panels for splitter).
{
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "el1",
"tabRows": {
"item1": [{ "columns": [{ "elementRef": "el3" }] }],
"item2": [{ "columns": [{ "elementRef": "el4" }] }]
}
}
]
},
{
"columns": [
{
"elementRef": "el2",
"tabRows": {
"left": [{ "columns": [{ "elementRef": "el5" }] }],
"right": [{ "columns": [{ "elementRef": "el6" }] }]
}
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Page 1", "type": "page" },
"el1": {
"name": "el1",
"label": "Sections",
"type": "accordion",
"allowMultiple": false,
"openFirst": true,
"items": [
{ "value": "item1", "label": "Contact" },
{ "value": "item2", "label": "Billing" }
]
},
"el2": {
"name": "el2",
"label": "Workspace",
"type": "splitter",
"orientation": "horizontal",
"allowResize": true,
"gutterSize": "6px",
"minPanelSize": "160px",
"showPanelHeaders": true,
"panels": [
{ "value": "left", "label": "Filters" },
{ "value": "right", "label": "Results" }
]
},
"el3": { "name": "el3", "label": "Email", "type": "text" },
"el4": { "name": "el4", "label": "IBAN", "type": "text" },
"el5": { "name": "el5", "label": "Search", "type": "text" },
"el6": { "name": "el6", "label": "Notes", "type": "textarea" }
}
}2.4 emptyBlock as a CSS grid
emptyBlock is the unstyled layout box. Use it when a panel would add unwanted chrome, or when you need real grid/flex control.
{
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "el1",
"rows": [
{
"columns": [
{ "elementRef": "el2" },
{ "elementRef": "el3" },
{ "elementRef": "el4" }
]
}
]
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Page 1", "type": "page" },
"el1": {
"name": "el1",
"type": "emptyBlock",
"contentDisplay": "grid",
"gridTemplateColumns": "repeat(3, minmax(0, 1fr))",
"contentGap": "16px",
"panelPadding": "16px",
"panelBackgroundColor": "#f6f8fa",
"panelRadius": "10px"
},
"el2": { "name": "el2", "label": "Open", "type": "statsCard", "valueText": "128", "variant": "info" },
"el3": { "name": "el3", "label": "Closed", "type": "statsCard", "valueText": "47", "variant": "success" },
"el4": { "name": "el4", "label": "Overdue", "type": "statsCard", "valueText": "9", "variant": "risk" }
}
}2.5 In-view dialog element
The dialog element is a modal that lives inside a page, opened by an action or openIf. It is not the same as settings.renderMode: "dialog", which renders the whole view as a modal.
{
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }] },
{
"columns": [
{
"elementRef": "el2",
"rows": [
{ "columns": [{ "elementRef": "el3" }] },
{ "columns": [{ "elementRef": "el4" }] }
]
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Page 1", "type": "page" },
"el1": {
"name": "el1",
"label": "",
"type": "button",
"text": "Edit record",
"variant": "solid",
"tone": "primary",
"fitContent": true,
"events": [
{ "trigger": "click", "type": "dialog", "dialogName": "el2", "dialogOperation": "open" }
]
},
"el2": {
"name": "el2",
"label": "Edit record",
"type": "dialog",
"showTriggerButton": false,
"showCloseButton": true,
"closeOnBackdrop": true,
"dialogWidth": "560px"
},
"el3": { "name": "el3", "label": "Title", "type": "text", "required": true },
"el4": {
"name": "el4",
"label": "",
"type": "button",
"text": "Save",
"variant": "solid",
"tone": "primary",
"fitContent": true,
"events": [
{
"trigger": "click",
"type": "dataSource",
"dataSourceName": "ds1",
"validateForm": true,
"showToastAfter": true,
"toastTitle": "Saved",
"toastVariant": "success"
},
{ "trigger": "click", "type": "dialog", "dialogName": "el2", "dialogOperation": "close" }
]
}
}
}3. Input elements
Every property below is real. Notice what each element does not have: phoneInput has no mask, singleCheckbox has no checkedValue, datepicker formats use uppercase tokens.
{
"elements": {
"el1": {
"name": "el1",
"label": "Text",
"type": "text",
"placeholder": "Type here",
"minlength": 2,
"maxlength": 60,
"showMaxLengthCounter": true,
"inputMode": "text",
"spellcheck": false,
"autocomplete": "off",
"required": true,
"requiredMessage": "This field is required"
},
"el2": {
"name": "el2",
"label": "Masked text",
"type": "text",
"maskType": "custom",
"maskPattern": "AA-0000",
"placeholder": "AB-1234"
},
"el3": {
"name": "el3",
"label": "Textarea",
"type": "textarea",
"rows": 4,
"maxlength": 500,
"showMaxLengthCounter": true
},
"el4": {
"name": "el4",
"label": "Number",
"type": "number",
"min": 0,
"max": 1000000,
"step": 0.01,
"inputMode": "decimal",
"valueStorageType": "number",
"visualFormatEnabled": true,
"visualFormatLocale": "lt-LT",
"visualFormatUseGrouping": true,
"visualFormatMinFractionDigits": 2,
"visualFormatMaxFractionDigits": 2
},
"el5": {
"name": "el5",
"label": "Slider",
"type": "slider",
"min": 0,
"max": 100,
"step": 5,
"showValueLabel": true,
"defaultValue": 20
},
"el6": {
"name": "el6",
"label": "Stepper",
"type": "numberStepper",
"min": 1,
"max": 20,
"step": 1,
"defaultValue": 1
},
"el7": {
"name": "el7",
"label": "Phone",
"type": "phoneInput",
"defaultCountryCode": "LT",
"allowedCountryCodes": ["LT", "LV", "EE"],
"placeholder": "600 00000"
},
"el8": {
"name": "el8",
"label": "Date",
"type": "datepicker",
"placeholder": "Pick a date",
"pickerMode": "date",
"format": "YYYY-MM-DD",
"minValue": "today",
"validators": [{ "type": "maxDate", "value": "today", "message": "Cannot be in the future" }]
},
"el9": {
"name": "el9",
"label": "Date and time",
"type": "datepicker",
"pickerMode": "datetime",
"includeSeconds": false,
"format": "YYYY-MM-DD HH:mm"
},
"el10": {
"name": "el10",
"label": "Date range",
"type": "dateRange",
"format": "YYYY-MM-DD",
"placeholder": "From - to"
},
"el11": { "name": "el11", "label": "Time", "type": "timePicker", "minuteStep": 15 },
"el12": {
"name": "el12",
"label": "Attachments",
"type": "fileUpload",
"accept": ".pdf,image/*",
"multiple": true,
"maxFiles": 5,
"maxFileSizeMb": 10,
"showPreview": true,
"dropzoneText": "Drop files here",
"uploadDataSourceName": "ds1",
"downloadDataSourceName": "ds2",
"deleteDataSourceName": "ds3",
"uploadFormFieldName": "file",
"fileKeyField": "key",
"fileNameField": "name",
"fileTypeField": "contentType",
"fileSizeField": "size"
},
"el13": {
"name": "el13",
"label": "Signature",
"type": "signaturePad",
"canvasHeight": "180px",
"strokeColor": "#111827",
"strokeWidth": 2,
"backgroundColor": "#ffffff",
"exportFormat": "png",
"clearLabel": "Clear"
},
"el14": {
"name": "el14",
"label": "Rich text",
"type": "richText",
"toolbarTools": ["bold", "italic", "underline", "bulletList", "link"]
}
}
}Value shapes: text / textarea / richText store a string, number / slider / numberStepper a number, dateRange an object { dateFrom, dateTo }, fileUpload the upload response object (or an array of them), timePicker a time string.
4. Choice elements
{
"elements": {
"el1": {
"name": "el1",
"label": "Select",
"type": "select",
"showSearch": true,
"strictOptions": true,
"defaultValue": "a",
"options": [
{ "value": "a", "label": "Option A" },
{ "value": "b", "label": "Option B" },
{ "value": "c", "label": "Option C" }
]
},
"el2": {
"name": "el2",
"label": "Multi select",
"type": "multiSelect",
"showSearch": true,
"strictOptions": true,
"options": [
{ "value": "a", "label": "Option A" },
{ "value": "b", "label": "Option B" }
]
},
"el3": {
"name": "el3",
"label": "Radio",
"type": "radio",
"showInline": true,
"defaultValue": "y",
"options": [
{ "value": "y", "label": "Yes" },
{ "value": "n", "label": "No" }
]
},
"el4": {
"name": "el4",
"label": "Checkbox group",
"type": "checkbox",
"showInline": false,
"options": [
{ "value": "a", "label": "Option A" },
{ "value": "b", "label": "Option B" }
]
},
"el5": {
"name": "el5",
"label": "",
"type": "singleCheckbox",
"checkboxLabel": "I accept the terms",
"defaultValue": false,
"required": true,
"requiredMessage": "You must accept the terms"
},
"el6": {
"name": "el6",
"label": "Toggle",
"type": "toggleSwitch",
"trueLabel": "On",
"falseLabel": "Off",
"trueValue": true,
"falseValue": false,
"defaultValue": false
},
"el7": {
"name": "el7",
"label": "Segmented",
"type": "selectButton",
"orientation": "horizontal",
"multiple": false,
"allowEmpty": false,
"variant": "outline",
"size": "normal",
"showSelectedIcon": true,
"options": [
{ "value": "day", "label": "Day" },
{ "value": "week", "label": "Week" },
{ "value": "month", "label": "Month" }
]
},
"el8": {
"name": "el8",
"label": "List box",
"type": "listBox",
"selectionMode": "multiple",
"maxHeight": "220px",
"options": [
{ "value": "a", "label": "Option A" },
{ "value": "b", "label": "Option B" },
{ "value": "c", "label": "Option C" }
]
},
"el9": {
"name": "el9",
"label": "Autocomplete",
"type": "autocomplete",
"placeholder": "Start typing",
"minSearchLength": 2,
"debounceMs": 300,
"maxSuggestions": 20,
"forceSelection": true,
"lazyLoad": true,
"queryContextKey": "query",
"dataSource": {
"name": "ds1",
"useFor": "option",
"optionValue": "id",
"optionLabel": "name"
}
}
}
}Value shapes: select / radio / selectButton / autocomplete store a single value; multiSelect / checkbox store an array; listBox stores a single value or an array depending on selectionMode; singleCheckbox / toggleSwitch store a boolean.
5. Content and feedback elements
{
"elements": {
"el1": {
"name": "el1",
"label": "",
"type": "pageTitle",
"title": "Customer records",
"subtitle": "Everything the support desk needs",
"level": 2,
"align": "left",
"showDivider": true
},
"el2": {
"name": "el2",
"label": "",
"type": "messageCard",
"title": "Heads up",
"descriptionText": "Changes are applied immediately.",
"variant": "info",
"showIcon": true,
"dismissible": true,
"initiallyVisible": true
},
"el3": {
"name": "el3",
"label": "",
"type": "statsCard",
"title": "Open tickets",
"valueText": "128",
"variant": "info",
"icon": "inbox",
"trendDirection": "up",
"trendText": "+12 this week"
},
"el4": {
"name": "el4",
"label": "",
"type": "badge",
"text": "Beta",
"variant": "warning",
"pill": true,
"size": "small"
},
"el5": {
"name": "el5",
"label": "",
"type": "progressBar",
"min": 0,
"max": 100,
"displayType": "bar",
"variant": "success",
"showValueLabel": true,
"valueLabelMode": "percent"
},
"el6": { "name": "el6", "label": "", "type": "divider", "orientation": "horizontal", "thickness": "1px", "spacing": "16px" },
"el7": { "name": "el7", "label": "", "type": "spacer", "axis": "vertical", "size": "24px" },
"el8": {
"name": "el8",
"label": "",
"type": "image",
"src": "/assets/cover.png",
"alt": "Cover",
"fit": "cover",
"aspectRatio": "16/9",
"radius": "8px",
"showPreview": true
},
"el9": {
"name": "el9",
"label": "",
"type": "customHtml",
"htmlTemplate": "<p class=\"note\">Reference: <b>{el1}</b></p>"
},
"el10": {
"name": "el10",
"label": "",
"type": "richTextViewer",
"htmlSourcePath": "el11"
},
"el11": {
"name": "el11",
"label": "",
"type": "chart",
"chartType": "bar",
"chartHeight": "280px",
"showLegend": true,
"legendLayout": "horizontal",
"labelKey": "label",
"valueKey": "value",
"chartDataPath": "stats.byMonth",
"dataSource": { "name": "ds1", "useFor": "value" }
}
}
}6. table: client-side data grid
This is the shape to reach for when the endpoint returns every row in one response. The table sorts, searches, filters, paginates and exports entirely in the browser.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" },
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }] },
{ "columns": [{ "elementRef": "el3" }] },
{ "columns": [{ "elementRef": "el2" }] }
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Customers", "type": "page", "hideHeader": true },
"el1": { "name": "el1", "label": "", "type": "pageTitle", "title": "Customers", "level": 2 },
"el2": {
"name": "el2",
"label": "All customers",
"type": "table",
"dataSource": {
"name": "ds1",
"useFor": "value",
"refreshOnChange": true,
"refreshPaths": ["el3"]
},
"tableItemsPath": "data.items",
"lazyLoad": false,
"pageSize": 25,
"pageSizeOptions": "10,25,50,100",
"paginator": true,
"paginatorStyle": "default",
"paginatorVariant": "full",
"paginatorAlign": "end",
"paginatorMaxButtons": 7,
"showPageSizeSelector": true,
"tableStyle": "striped",
"stickyHeader": true,
"rowHover": true,
"responsiveMode": "auto",
"flatHeaderSurface": false,
"flatFooterSurface": false,
"emptyMessage": "No customers found",
"loadingLabel": "Loading...",
"showHeaderControls": true,
"showQuickSearch": true,
"quickSearchPlaceholder": "Search customers",
"quickSearchCondition": "%-%",
"quickSearchCaseMode": "caseInsensitiveLatin",
"searchDebounceMs": 300,
"showDetailedSearch": true,
"detailedSearchCaseMode": "caseInsensitiveLatin",
"showExport": true,
"exportFileName": "customers",
"exportUseColumnPicker": true,
"showColumnSettings": true,
"columnSettingsMode": "localStorage",
"columnSettingsStorageKey": "customersTableColumns",
"columnSettingsDialogTitle": "Visible columns",
"orderClause": "column2 ASC",
"orderDirection": "asc",
"columnsConfig": [
{
"key": "column1",
"label": "ID",
"type": "number",
"sortable": true,
"width": "90px",
"align": "right",
"numberUseGrouping": false,
"showInTable": true,
"showInDetails": true
},
{
"key": "column2",
"label": "Name",
"type": "text",
"sortable": true,
"showInTable": true,
"filterControlType": "text"
},
{
"key": "column3",
"label": "Email",
"type": "text",
"sortable": true,
"showInTable": true,
"mobileLabel": "Mail"
},
{
"key": "column4",
"label": "Balance",
"type": "number",
"sortable": true,
"align": "right",
"formatLocale": "lt-LT",
"numberMinFractionDigits": 2,
"numberMaxFractionDigits": 2,
"numberUseGrouping": true,
"showInTable": true,
"filterControlType": "number"
},
{
"key": "column5",
"label": "Created",
"type": "dateTime",
"sortable": true,
"dateFormatPattern": "yyyy-MM-dd HH:mm",
"dateIncludeSeconds": false,
"showInTable": true,
"filterControlType": "date"
},
{
"key": "column6",
"label": "Country",
"type": "text",
"showInTable": true,
"filterControlType": "select",
"filterOptionsSourceType": "static",
"filterOptions": [
{ "value": "LT", "label": "Lithuania" },
{ "value": "LV", "label": "Latvia" }
],
"filterOptionValueKey": "value",
"filterOptionLabelKey": "label"
},
{
"key": "column7",
"label": "Internal note",
"type": "text",
"showInTable": false,
"showInDetails": true,
"detailLabel": "Note (internal)"
}
]
},
"el3": { "name": "el3", "label": "Segment", "type": "select", "options": [] }
},
"dataSources": [
{
"name": "ds1",
"title": "Customers",
"type": "rest",
"params": { "url": "/api/customers", "method": "GET" }
}
],
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}Points that decide whether this works:
columnsConfig[*].key, nevername. A column with onlynamedoes not render.typefor a column is one oftextdatedateTimenumberelement. The renderer still understands the legacybooleanhtmlstatustoggleSwitchsingleCheckboxso old views keep working, but author new columns withelementinstead.tableItemsPathis only needed when the rows are not the response root and not underitems/data/results/rows.refreshPathson the elementdataSourcemakes the table reload when another field changes.showInTable: false+showInDetails: truekeeps a field out of the grid but available in the details panel.
6.1 Row, header and selection actions
{
"el2": {
"name": "el2",
"label": "All customers",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"columnsConfig": [
{ "key": "column1", "label": "ID", "type": "number", "showInTable": true },
{ "key": "column2", "label": "Name", "type": "text", "showInTable": true }
],
"enableSelection": true,
"selectionKey": "column1",
"selectLabel": "Select",
"selectAllLabel": "Select all",
"selectionActionsLabel": "With selected",
"autoSelectCondition": "row.column3 == 'active'",
"selectionActions": [
{
"label": "Archive selected",
"type": "dataSource",
"dataSourceName": "ds2",
"buttonTone": "neutral",
"confirmEnabled": true,
"confirmTitle": "Archive",
"confirmMessage": "Archive every selected record?",
"reloadCurrentElementAfterSuccess": true
}
],
"headerActionsPosition": "end",
"headerActionsDisplayMode": "buttons",
"headerActions": [
{
"label": "New customer",
"icon": "add",
"type": "navigate",
"navigateTo": "/customers/new",
"buttonVariant": "filled",
"buttonTone": "primary"
},
{
"label": "Reload",
"icon": "refresh",
"type": "reloadElements",
"reloadElementNames": ["el2"],
"buttonVariant": "text"
}
],
"showActionsHeaderLabel": true,
"actionsLabel": "Actions",
"rowActionsDisplayMode": "dropdown",
"rowActionsDropdownButtonStyle": "borderless",
"rowActionVisibilityPath": "column4",
"rowActions": [
{
"label": "Open",
"icon": "open_in_new",
"type": "navigate",
"navigateTo": "/customers/{row.column1}"
},
{
"label": "Deactivate",
"icon": "block",
"type": "dataSource",
"dataSourceName": "ds3",
"condition": "row.column3 == 'active'",
"reloadCurrentElementAfterSuccess": true,
"showToastAfter": true,
"toastTitle": "Customer deactivated",
"toastVariant": "success",
"toastPosition": "bottom-right",
"toastAutoHide": true,
"toastAutoHideMs": 4000
},
{
"label": "Delete",
"icon": "delete",
"type": "dataSource",
"dataSourceName": "ds4",
"buttonTone": "risk",
"confirmEnabled": true,
"confirmTitle": "Delete customer",
"confirmMessage": "This cannot be undone. Continue?",
"confirmConfirmLabel": "Delete",
"confirmCancelLabel": "Cancel",
"reloadCurrentElementAfterSuccess": true
}
],
"rowClickActions": [
{ "type": "navigate", "navigateTo": "/customers/{row.column1}" }
]
},
"dataSources": [
{ "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers", "method": "GET" } },
{ "name": "ds2", "title": "Archive", "type": "rest", "params": { "url": "/api/customers/archive", "method": "POST", "body": { "ids": "{selectedKeys}" } } },
{ "name": "ds3", "title": "Deactivate", "type": "rest", "params": { "url": "/api/customers/{row.column1}", "method": "PUT", "body": { "active": false } } },
{ "name": "ds4", "title": "Delete", "type": "rest", "params": { "url": "/api/customers/{row.column1}", "method": "DELETE" } }
]
}Context available to actions:
| Action kind | Sees |
|---|---|
rowActions[*], rowClickActions[*] | {row.*} for the clicked row |
selectionActions[*] | {selectedRows}, {selectedKeys}, {rows}, {items} |
headerActions[*] | the surrounding form context, no row |
| inline edit save | {row.*} plus {changedValues} (only the columns that changed) |
rowActions[*].condition is evaluated per row against row.*. rowActionVisibilityPath names a row field that hides the whole action column for rows where it is falsy.
6.2 Element columns, status badges and templates
A column of type: "element" renders a real builder element in every cell. element holds that element's own configuration, with the same property names it would have as a page element.
{
"el2": {
"name": "el2",
"label": "Orders",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"columnsConfig": [
{
"key": "column1",
"label": "Order",
"type": "text",
"showInTable": true,
"template": "<b>#{row.column1}</b><br><small>{row.column2}</small>"
},
{
"key": "column3",
"label": "State",
"type": "status",
"statusShowIcon": true,
"showInTable": true,
"statusRules": [
{ "condition": "value == 'new'", "label": "New", "tone": "info", "icon": "fiber_new", "rounded": true },
{ "condition": "value == 'paid'", "label": "Paid", "tone": "success", "icon": "check_circle", "rounded": true },
{ "condition": "value == 'late'", "label": "Late", "tone": "risk", "icon": "error", "rounded": true }
]
},
{
"key": "column4",
"label": "Assignee",
"type": "element",
"elementType": "select",
"showInTable": true,
"controlEnabledIf": "row.column3 != 'paid'",
"element": {
"placeholder": "Unassigned",
"strictOptions": true,
"options": [
{ "value": "u1", "label": "Anna" },
{ "value": "u2", "label": "Ben" }
],
"events": [
{
"trigger": "change",
"type": "dataSource",
"dataSourceName": "ds2",
"params": [
{ "name": "id", "value": "{row.column1}" },
{ "name": "assignee", "value": "{value}" }
]
}
]
}
},
{
"key": "column5",
"label": "Progress",
"type": "element",
"elementType": "progressBar",
"showInTable": true,
"element": { "min": 0, "max": 100, "showValueLabel": true, "displayType": "bar" }
},
{
"key": "column6",
"label": "Description",
"type": "element",
"elementType": "richTextViewer",
"showInTable": false,
"showInDetails": true,
"element": {}
},
{
"key": "column7",
"label": "",
"type": "text",
"showInTable": true,
"cellActionsEnabled": true,
"cellActionsDisplayMode": "iconButtons",
"cellActions": [
{ "label": "Copy", "icon": "content_copy", "type": "setValue", "setValueTargetPath": "el3", "setValueMode": "template", "setValueValue": "{row.column2}" }
]
}
]
}
}Rules for these:
statusRules[*].conditionreads the cell value asvalue, not as the column key.status == 'paid'never matches;value == 'paid'does.row.otherColumnalso works.statusRules[*].toneis one ofneutralsuccesswarningriskprimarydangerinfo.- Inside
templateand inside a hostedelement, the row is reachable as{row.*}, a bare sibling key,{index}and{value}. visibleIf,controlEnabledIfandcontrolActiveIfon a column are evaluated per row.- Action
paramsare always[{ "name": ..., "value": ... }]. An array of pairs is dropped silently.
6.3 Server-side paging: lazyLoad and TABLE-POST
With lazyLoad: true and the data source method set to the literal string TABLE-POST, the table merges paging, sorting and filter state into the request body for you.
{
"elements": {
"el2": {
"name": "el2",
"label": "Customers",
"type": "table",
"lazyLoad": true,
"dataSource": { "name": "ds1", "useFor": "value" },
"tableItemsPath": "data.items",
"tableTotalPath": "data.total",
"params": [
{ "paramName": "tenantId", "paramValue": "{__variables.tenantId}" },
{ "paramName": "segment", "paramValue": "{el3}" }
],
"pageSize": 50,
"pageSizeOptions": "25,50,100",
"orderClause": "column2 ASC",
"showQuickSearch": true,
"quickSearchParamName": "quickSearch",
"quickSearchCondition": "%-%",
"showDetailedSearch": true,
"showExport": true,
"exportAllPageSize": 10000,
"virtualScroll": true,
"virtualScrollRowHeight": 44,
"virtualScrollViewportHeight": "600px",
"virtualScrollBuffer": 10,
"columnsConfig": [
{ "key": "column1", "label": "ID", "type": "number", "sortable": true, "showInTable": true },
{ "key": "column2", "label": "Name", "type": "text", "sortable": true, "showInTable": true, "filterControlType": "text" },
{ "key": "column3", "label": "Active", "type": "element", "elementType": "badge", "showInTable": true, "filterControlType": "boolean", "filterToggleTrueValue": "true", "filterToggleFalseValue": "false", "element": {} }
]
}
},
"dataSources": [
{
"name": "ds1",
"title": "Customers (paged)",
"type": "rest",
"params": { "url": "/api/customers/search", "method": "TABLE-POST" }
}
]
}The body your endpoint receives:
{
"pagingParams": {
"cnt": null,
"orderClause": "column2 ASC",
"pageSize": 50,
"skipRows": 100,
"totalCountUsed": false
},
"params": [["tenantId", "42"], ["segment", "vip"]],
"extendedParams": [
{ "paramName": "quickSearch", "paramValue": { "condition": "%-%", "value": "acme", "upperLower": "caseInsensitiveLatin" } },
{ "paramName": "column2", "paramValue": { "condition": "%-", "value": "Ac" } },
{ "paramName": "column3", "paramValue": { "condition": "=", "value": true } }
]
}- On the wire
paramsbecomes[name, value]tuples, but you author it as[{ "paramName": ..., "paramValue": ... }]on the element. Up to five rows. conditionvocabulary:%-%contains,!%-%does not contain,%-starts with,-%ends with,=,!=,>,>=,<,<=.- Response: rows come from
tableItemsPath, oritems/data/results/rows. Total comes fromtableTotalPath, ortotal/totalCount/totalRecords/count/cnt/paging.*. Without a recognizable total the pager looks stuck. - Export "all" re-issues the same request with
pagingParams.pageSizeset toexportAllPageSize, then builds the file in the browser.
6.4 Inline edit, expandable rows, details panel, saved filters
{
"elements": {
"el2": {
"name": "el2",
"label": "Orders",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"enableInlineEdit": true,
"inlineEditStartMode": "doubleClick",
"inlineEditSaveDataSourceName": "ds2",
"inlineEditCancelOnReload": true,
"expandableRows": true,
"expandedDisplayMode": "inline",
"showExpandChevron": true,
"alwaysShowExpandChevron": false,
"expandLabel": "Show items",
"collapseLabel": "Hide items",
"expandedEmptyMessage": "This order has no lines",
"expandedDataSourceName": "ds3",
"expandedDataSourceItemsPath": "data.lines",
"expandedColumnsConfig": [
{ "key": "column10", "label": "Product", "type": "text", "showInTable": true },
{ "key": "column11", "label": "Qty", "type": "number", "align": "right", "showInTable": true },
{ "key": "column12", "label": "Price", "type": "number", "align": "right", "numberMinFractionDigits": 2, "showInTable": true }
],
"expandedRowActions": [
{ "label": "Remove line", "icon": "delete", "type": "dataSource", "dataSourceName": "ds4", "buttonTone": "risk" }
],
"rowClickOpensDetails": true,
"detailsPanelTitle": "Order details",
"enableSavedFilters": true,
"savedFiltersMode": "dataSource",
"savedFiltersDataSourceName": "ds5",
"savedFiltersItemsPath": "data.items",
"saveFilterDataSourceName": "ds6",
"deleteFilterDataSourceName": "ds7",
"savedFilterIdKey": "id",
"savedFilterNameKey": "name",
"savedFilterCodeKey": "code",
"savedFilterDescriptionKey": "description",
"savedFilterPayloadKey": "payload",
"columnsConfig": [
{ "key": "column1", "label": "Order", "type": "text", "showInTable": true },
{
"key": "column2",
"label": "Status",
"type": "text",
"showInTable": true,
"editable": true,
"editorType": "select",
"editorOptions": [
{ "value": "new", "label": "New" },
{ "value": "paid", "label": "Paid" }
],
"editorOptionValueKey": "value",
"editorOptionLabelKey": "label"
},
{
"key": "column3",
"label": "Note",
"type": "text",
"showInTable": true,
"editable": true,
"editorType": "textarea",
"editorPlaceholder": "Internal note"
}
]
}
}
}The inline-edit save data source receives {row.*} (the whole edited row) and {changedValues}, a flat object holding only the columns that actually changed, which is what a PATCH endpoint wants.
6.5 The table's live state: __table.*
Every table continuously publishes its own state into the form data, so any other element can read it without knowing how the table is fed. This is the mechanism behind selection-driven toolbars, counters, master/detail screens and conditional buttons.
Two roots exist. __table.<tableName>.* is that specific table. Bare __table.* is the last table the user touched, useful when there is only one.
| Path | Holds |
|---|---|
__table.<name>.rows | rows currently visible (the active page, after filtering) |
__table.<name>.allRows | every loaded row |
__table.<name>.rowCount | number of visible rows |
__table.<name>.totalRecords | total according to the server (lazy) or the loaded set |
__table.<name>.page | current page, 1-based |
__table.<name>.size | current page size |
__table.<name>.sortField | sorted column key, or "" |
__table.<name>.sortDirection | asc or desc |
__table.<name>.quickSearch | current quick-search term |
__table.<name>.detailedFilters | active per-column filters (lazy) |
__table.<name>.request | the exact request payload last sent (lazy) |
__table.<name>.selectedRows | array of checkbox-selected row objects |
__table.<name>.selectedKeys | array of their selectionKey values |
__table.<name>.selectedItems | the same selection rendered through selectionItemTemplate |
__table.<name>.selectedCount | how many are selected |
__table.<name>.selectedRow | the single row the user is working on: first selected, else expanded/active, else last clicked |
__table.<name>.selectedRowKey | its key, or null |
__table.<name>.selectedRowIndex | its index, or -1 |
__table.<name>.activeRow / .activeRowKey | the expanded / detail row |
These are read in braces like any other path: {__table.el2.selectedCount}.
6.6 Selection-driven toolbar
A counter, a bulk button that only enables with a selection, and a panel that mirrors the row being worked on. Nothing here is wired by hand; everything reads __table.*.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" },
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }, { "elementRef": "el2" }, { "elementRef": "el3" }] },
{ "columns": [{ "elementRef": "el4" }] },
{
"columns": [
{
"elementRef": "el5",
"rows": [
{ "columns": [{ "elementRef": "el6" }, { "elementRef": "el7" }] }
]
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Orders", "type": "page", "hideHeader": true },
"el1": {
"name": "el1",
"label": "",
"type": "badge",
"text": "Selected: {__table.el4.selectedCount}",
"variant": "info",
"pill": true,
"visibleIf": "{__table.el4.selectedCount} > 0"
},
"el2": {
"name": "el2",
"label": "",
"type": "button",
"text": "Approve selected",
"variant": "solid",
"tone": "primary",
"fitContent": true,
"disableIf": "{__table.el4.selectedCount} == 0",
"events": [
{
"trigger": "click",
"type": "dataSource",
"dataSourceName": "ds2",
"confirmEnabled": true,
"confirmTitle": "Approve",
"confirmMessage": "Approve every selected order?",
"reloadElementNames": ["el4"],
"showToastAfter": true,
"toastTitle": "Approved",
"toastVariant": "success"
}
]
},
"el3": {
"name": "el3",
"label": "",
"type": "button",
"text": "Clear search",
"variant": "text",
"tone": "neutral",
"fitContent": true,
"visibleIf": "notEmpty({__table.el4.quickSearch})",
"events": [{ "trigger": "click", "type": "reloadElements", "reloadElementNames": ["el4"] }]
},
"el4": {
"name": "el4",
"label": "Orders",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"enableSelection": true,
"selectionKey": "column1",
"selectionItemTemplate": "#{row.column1} ({row.column2})",
"showQuickSearch": true,
"pageSize": 25,
"columnsConfig": [
{ "key": "column1", "label": "Order", "type": "text", "showInTable": true, "sortable": true },
{ "key": "column2", "label": "Customer", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Total", "type": "number", "align": "right", "numberMinFractionDigits": 2, "showInTable": true, "sortable": true }
]
},
"el5": {
"name": "el5",
"label": "Current order",
"type": "panel",
"titleUnderline": true,
"panelPadding": "0px",
"showBorder": false,
"visibleIf": "notEmpty({__table.el4.selectedRowKey})"
},
"el6": {
"name": "el6",
"label": "Order",
"type": "text",
"readOnly": true,
"expression": "{__table.el4.selectedRow}.column1",
"logicExecutionMode": "onChange"
},
"el7": {
"name": "el7",
"label": "Customer",
"type": "text",
"readOnly": true,
"expression": "{__table.el4.selectedRow}.column2",
"logicExecutionMode": "onChange"
}
},
"dataSources": [
{ "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } },
{
"name": "ds2",
"title": "Approve selected",
"type": "rest",
"params": {
"url": "/api/orders/approve",
"method": "POST",
"body": { "ids": "{__table.el4.selectedKeys}" }
}
}
],
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}selectionKey names the row field used as the identity of a selection; it defaults to id. selectionItemTemplate shapes each entry of selectedItems, which is what you send when the endpoint wants labels rather than raw rows.
6.7 Master and detail: one table drives another
The detail table reloads whenever the master's selected row changes, because its data source declares that dependency in refreshPaths.
{
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }] },
{ "columns": [{ "elementRef": "el2" }] }
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Orders", "type": "page" },
"el1": {
"name": "el1",
"label": "Orders",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"rowHover": true,
"columnsConfig": [
{ "key": "column1", "label": "Order", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Customer", "type": "text", "showInTable": true }
]
},
"el2": {
"name": "el2",
"label": "Lines of the selected order",
"type": "table",
"emptyMessage": "Pick an order above",
"visibleIf": "notEmpty({__table.el1.selectedRowKey})",
"dataSource": {
"name": "ds2",
"useFor": "value",
"params": [{ "name": "orderId", "value": "{__table.el1.selectedRowKey}" }],
"refreshOnChange": true,
"refreshPaths": ["__table.el1.selectedRowKey"]
},
"columnsConfig": [
{ "key": "column10", "label": "Product", "type": "text", "showInTable": true },
{ "key": "column11", "label": "Qty", "type": "number", "align": "right", "showInTable": true }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } },
{ "name": "ds2", "title": "Order lines", "type": "rest", "params": { "url": "/api/orders/{orderId}/lines", "method": "GET" } }
]
}Clicking a row is enough: selectedRow falls back to the last clicked row when there is no selection column.
6.8 A filter toolbar above the table
Real fields drive the request. params decides what is sent, and each field change re-issues it.
{
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "el1",
"rows": [
{
"columns": [
{ "elementRef": "el2" },
{ "elementRef": "el3" },
{ "elementRef": "el4" }
]
}
]
}
]
},
{ "columns": [{ "elementRef": "el5" }] }
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Reports", "type": "page" },
"el1": {
"name": "el1",
"label": "Filters",
"type": "panel",
"showBorder": true,
"panelRadius": "8px",
"panelPadding": "12px",
"contentGap": "12px"
},
"el2": {
"name": "el2",
"label": "Status",
"type": "select",
"strictOptions": true,
"defaultValue": "",
"options": [
{ "value": "", "label": "All" },
{ "value": "open", "label": "Open" },
{ "value": "closed", "label": "Closed" }
]
},
"el3": { "name": "el3", "label": "Period", "type": "dateRange", "format": "YYYY-MM-DD" },
"el4": { "name": "el4", "label": "Owner", "type": "text" },
"el5": {
"name": "el5",
"label": "Results",
"type": "table",
"lazyLoad": true,
"dataSource": { "name": "ds1", "useFor": "value" },
"tableItemsPath": "data.items",
"tableTotalPath": "data.total",
"params": [
{ "paramName": "status", "paramValue": "{el2}" },
{ "paramName": "from", "paramValue": "{el3.dateFrom}" },
{ "paramName": "to", "paramValue": "{el3.dateTo}" },
{ "paramName": "owner", "paramValue": "{el4}" }
],
"showQuickSearch": true,
"pageSize": 50,
"columnsConfig": [
{ "key": "column1", "label": "Ref", "type": "text", "showInTable": true, "sortable": true },
{ "key": "column2", "label": "Opened", "type": "date", "showInTable": true, "sortable": true },
{ "key": "column3", "label": "Owner", "type": "text", "showInTable": true }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Report search", "type": "rest", "params": { "url": "/api/reports/search", "method": "TABLE-POST" } }
]
}A dateRange value is an object, so its parts are {el3.dateFrom} and {el3.dateTo}.
6.9 Column filter options loaded from a data source
Per-column filters do not have to be hardcoded. A column can pull its own option list.
{
"elements": {
"el1": {
"name": "el1",
"label": "Tickets",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"showDetailedSearch": true,
"columnsConfig": [
{ "key": "column1", "label": "Ref", "type": "text", "showInTable": true },
{
"key": "column2",
"label": "Assignee",
"type": "text",
"showInTable": true,
"filterControlType": "multiSelect",
"filterOptionsSourceType": "datasource",
"filterOptionsDataSourceName": "ds2",
"filterOptionsDataSourceItemsPath": "data.items",
"filterOptionValueKey": "id",
"filterOptionLabelKey": "name",
"filterMultiValueDelimiter": ","
},
{
"key": "column3",
"label": "Archived",
"type": "text",
"showInTable": true,
"filterControlType": "toggle",
"filterToggleTrueValue": "true",
"filterToggleFalseValue": "false"
},
{
"key": "column4",
"label": "Priority",
"type": "text",
"showInTable": true,
"filterControlType": "select",
"filterOptionsSourceType": "static",
"filterOptionValueKey": "value",
"filterOptionLabelKey": "label",
"filterOptions": [
{ "value": "low", "label": "Low" },
{ "value": "high", "label": "High" }
]
}
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } },
{ "name": "ds2", "title": "Users", "type": "rest", "params": { "url": "/api/users", "method": "GET" } }
]
}6.10 Header dropdown
Beside the header buttons a table can host one dropdown, either with static options or fed by a data source, whose selection runs actions.
{
"elements": {
"el1": {
"name": "el1",
"label": "Tickets",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"showHeaderControls": true,
"headerActionsPosition": "end",
"headerMenuEnabled": true,
"headerMenuPlaceholder": "Switch queue",
"headerMenuDataSourceName": "ds2",
"headerMenuItemsPath": "data.items",
"headerMenuLabelKey": "name",
"headerMenuValueKey": "id",
"headerMenuActions": [
{ "trigger": "change", "type": "reloadElements", "reloadElementNames": ["el1"] }
],
"columnsConfig": [
{ "key": "column1", "label": "Ref", "type": "text", "showInTable": true }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } },
{ "name": "ds2", "title": "Queues", "type": "rest", "params": { "url": "/api/queues", "method": "GET" } }
]
}For static options instead of a source, drop headerMenuDataSourceName / headerMenuItemsPath / headerMenuLabelKey / headerMenuValueKey and set headerMenuOptions: [{ "value": "a", "label": "A" }].
6.11 Templates and mobile behaviour
headerTemplate fills the centre of the header, rowTemplate replaces the whole row, expandedTemplate renders the expanded area. Row-level templates see {row.*}; all of them can read {__table.<name>.*}.
{
"elements": {
"el1": {
"name": "el1",
"label": "Tickets",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"headerTemplate": "<span class=\"muted\">Showing {__table.el1.rowCount} of {__table.el1.totalRecords}</span>",
"headerCenterElementsPosition": "center",
"responsiveMode": "stack",
"stickyHeader": true,
"tableStyle": "grid",
"expandableRows": true,
"expandedDisplayMode": "inline",
"expandedTemplate": "<div class=\"detail\"><b>{row.column2}</b><p>{row.column3}</p></div>",
"columnsConfig": [
{
"key": "column1",
"label": "Ticket",
"type": "text",
"showInTable": true,
"mobileLabel": "Ref",
"template": "<b>#{row.column1}</b>"
},
{ "key": "column2", "label": "Subject", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Body", "type": "text", "showInTable": false, "showInDetails": true }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } }
]
}responsiveMode: "stack" turns each row into a stacked card on narrow screens, where mobileLabel becomes the visible field label.
6.12 Column settings stored on the server
columnSettingsMode: "localStorage" needs only columnSettingsStorageKey. Per-user layouts kept server-side use the data-source mode instead.
{
"elements": {
"el1": {
"name": "el1",
"label": "Tickets",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"showColumnSettings": true,
"columnSettingsDialogTitle": "Columns",
"columnSettingsMode": "dataSource",
"columnSettingsDataSourceName": "ds2",
"columnSettingsDataSourcePath": "data.layout",
"saveColumnSettingsDataSourceName": "ds3",
"resetColumnSettingsDataSourceName": "ds4",
"columnsConfig": [
{ "key": "column1", "label": "Ref", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Subject", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Owner", "type": "text", "showInTable": false }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } },
{ "name": "ds2", "title": "Load layout", "type": "rest", "params": { "url": "/api/me/table-layout", "method": "GET" } },
{ "name": "ds3", "title": "Save layout", "type": "rest", "params": { "url": "/api/me/table-layout", "method": "PUT" } },
{ "name": "ds4", "title": "Reset layout", "type": "rest", "params": { "url": "/api/me/table-layout", "method": "DELETE" } }
]
}6.13 Full CRUD loop: table, dialog editor, reload
The pattern most admin screens need, end to end.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" },
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }] },
{ "columns": [{ "elementRef": "el2" }] },
{
"columns": [
{
"elementRef": "el3",
"rows": [
{ "columns": [{ "elementRef": "el4" }, { "elementRef": "el5" }] },
{ "columns": [{ "elementRef": "el6" }] },
{
"columns": [
{
"elementRef": "el7",
"rows": [
{ "columns": [{ "elementRef": "el8" }, { "elementRef": "el9" }] }
]
}
]
}
]
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Customers", "type": "page", "hideHeader": true },
"el1": {
"name": "el1",
"label": "",
"type": "button",
"text": "New customer",
"icon": "add",
"variant": "solid",
"tone": "primary",
"fitContent": true,
"events": [
{ "trigger": "click", "type": "setValue", "setValueTargetPath": "el4", "setValueMode": "template", "setValueValue": "" },
{ "trigger": "click", "type": "setValue", "setValueTargetPath": "el5", "setValueMode": "template", "setValueValue": "" },
{ "trigger": "click", "type": "setValue", "setValueTargetPath": "el6", "setValueMode": "template", "setValueValue": "" },
{ "trigger": "click", "type": "dialog", "dialogName": "el3", "dialogOperation": "open" }
]
},
"el2": {
"name": "el2",
"label": "Customers",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"selectionKey": "column1",
"pageSize": 25,
"showQuickSearch": true,
"rowActionsDisplayMode": "iconButtons",
"columnsConfig": [
{ "key": "column1", "label": "ID", "type": "number", "showInTable": true, "width": "80px" },
{ "key": "column2", "label": "Name", "type": "text", "showInTable": true, "sortable": true },
{ "key": "column3", "label": "Email", "type": "text", "showInTable": true }
],
"rowActions": [
{
"label": "Edit",
"icon": "edit",
"type": "setValue",
"setValueTargetPath": "el4",
"setValueMode": "template",
"setValueValue": "{row.column1}"
},
{
"label": "Edit",
"icon": "edit",
"hideText": true,
"type": "setValue",
"setValueTargetPath": "el5",
"setValueMode": "template",
"setValueValue": "{row.column2}"
},
{
"label": "Edit",
"icon": "edit",
"hideText": true,
"type": "setValue",
"setValueTargetPath": "el6",
"setValueMode": "template",
"setValueValue": "{row.column3}"
},
{
"label": "Edit",
"icon": "edit",
"hideText": true,
"type": "dialog",
"dialogName": "el3",
"dialogOperation": "open"
},
{
"label": "Delete",
"icon": "delete",
"type": "dataSource",
"dataSourceName": "ds3",
"buttonTone": "risk",
"confirmEnabled": true,
"confirmTitle": "Delete customer",
"confirmMessage": "This cannot be undone.",
"reloadCurrentElementAfterSuccess": true
}
]
},
"el3": {
"name": "el3",
"label": "Customer",
"type": "dialog",
"showTriggerButton": false,
"showCloseButton": true,
"dialogWidth": "520px"
},
"el4": { "name": "el4", "label": "ID", "type": "text", "readOnly": true },
"el5": { "name": "el5", "label": "Name", "type": "text", "required": true },
"el6": { "name": "el6", "label": "Email", "type": "text", "inputMode": "email", "required": true },
"el7": {
"name": "el7",
"label": "",
"type": "panel",
"showBorder": false,
"panelPadding": "0px",
"contentJustify": "flex-end",
"contentGap": "8px"
},
"el8": {
"name": "el8",
"label": "",
"type": "button",
"text": "Cancel",
"variant": "outline",
"tone": "neutral",
"fitContent": true,
"events": [{ "trigger": "click", "type": "dialog", "dialogName": "el3", "dialogOperation": "close" }]
},
"el9": {
"name": "el9",
"label": "",
"type": "button",
"text": "Save",
"variant": "solid",
"tone": "primary",
"fitContent": true,
"events": [
{
"trigger": "click",
"type": "dataSource",
"dataSourceName": "ds2",
"validateForm": true,
"reloadElementNames": ["el2"],
"showToastAfter": true,
"toastTitle": "Saved",
"toastVariant": "success"
},
{ "trigger": "click", "type": "dialog", "dialogName": "el3", "dialogOperation": "close" }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers", "method": "GET" } },
{
"name": "ds2",
"title": "Save customer",
"type": "rest",
"params": {
"url": "/api/customers/{el4}",
"method": "PUT",
"body": { "name": "{el5}", "email": "{el6}" }
}
},
{ "name": "ds3", "title": "Delete customer", "type": "rest", "params": { "url": "/api/customers/{row.column1}", "method": "DELETE" } }
],
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}One action carries one type, so filling three fields and opening the dialog is four actions on the same row-action button. They run in order, and only the first needs a visible label; give the rest hideText: true so the toolbar stays clean.
6.14 Dashboard: one table feeding KPI cards and a chart
The table is the single source of data; the cards and the chart read its published state. No second request is issued: a local source in dataPath mode simply points at __table.el3.allRows.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" },
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{ "elementRef": "el1" },
{ "elementRef": "el2" }
]
},
{ "columns": [{ "elementRef": "el5" }] },
{ "columns": [{ "elementRef": "el4" }] },
{ "columns": [{ "elementRef": "el3" }] }
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Sales", "type": "page", "hideHeader": true },
"el1": {
"name": "el1",
"label": "",
"type": "statsCard",
"title": "Orders",
"valueText": "{__table.el3.totalRecords}",
"variant": "info",
"icon": "receipt_long"
},
"el2": {
"name": "el2",
"label": "",
"type": "statsCard",
"title": "Revenue",
"valueText": "{el5}",
"variant": "success",
"icon": "payments"
},
"el4": {
"name": "el4",
"label": "Revenue by month",
"type": "chart",
"chartType": "bar",
"chartHeight": "260px",
"showLegend": false,
"labelKey": "column2",
"valueKey": "column3",
"dataSource": {
"name": "ds2",
"useFor": "value",
"refreshOnChange": true,
"refreshPaths": ["__table.el3.allRows"]
}
},
"el3": {
"name": "el3",
"label": "Orders",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"pageSize": 25,
"showQuickSearch": true,
"columnsConfig": [
{ "key": "column1", "label": "Order", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Month", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Total", "type": "number", "align": "right", "numberMinFractionDigits": 2, "showInTable": true }
]
},
"el5": {
"name": "el5",
"label": "Revenue",
"type": "number",
"hidden": true,
"readOnly": true,
"expression": "sumArray({__table.el3.allRows}, column3)",
"logicExecutionMode": "onChange"
}
},
"dataSources": [
{ "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } },
{
"name": "ds2",
"title": "Orders already loaded by the table",
"type": "local",
"params": { "localMode": "dataPath", "dataPath": "__table.el3.allRows" }
}
],
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}statsCard.title, valueText and trendText, and badge.text, all interpolate {...} tokens, so a card can display live table state directly. A hidden number element is the usual place to park an aggregate that several other elements want to reuse.
hidden: true still needs a place in the layout tree. hidden only suppresses the rendering; an element that is absent from pages is never instantiated at all, so its expression never runs and everything reading it stays empty. Give the helper element its own row and hide it there.
6.15 Aggregates under a table
allRows is the whole loaded set, rows is only what the current page shows after filtering. Pick deliberately: a "total" that changes when the user pages is almost always a bug.
{
"elements": {
"el1": {
"name": "el1",
"label": "Invoices",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"columnsConfig": [
{ "key": "column1", "label": "Invoice", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Status", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Amount", "type": "number", "align": "right", "showInTable": true }
]
},
"el2": {
"name": "el2",
"label": "Rows loaded",
"type": "number",
"readOnly": true,
"expression": "len({__table.el1.allRows})",
"logicExecutionMode": "onChange"
},
"el3": {
"name": "el3",
"label": "Total amount",
"type": "number",
"readOnly": true,
"expression": "sumArray({__table.el1.allRows}, column3)",
"logicExecutionMode": "onChange"
},
"el4": {
"name": "el4",
"label": "Average amount",
"type": "number",
"readOnly": true,
"expression": "avgArray({__table.el1.allRows}, column3)",
"logicExecutionMode": "onChange"
},
"el5": {
"name": "el5",
"label": "Unpaid",
"type": "number",
"readOnly": true,
"expression": "countInArray({__table.el1.allRows}, column2 == \"unpaid\")",
"logicExecutionMode": "onChange"
},
"el6": {
"name": "el6",
"label": "Unpaid total",
"type": "number",
"readOnly": true,
"expression": "sumArray(filterArray({__table.el1.allRows}, column2 == \"unpaid\"), column3)",
"logicExecutionMode": "onChange"
},
"el7": {
"name": "el7",
"label": "Largest invoice",
"type": "text",
"readOnly": true,
"expression": "getFirst(filterArray({__table.el1.allRows}, column3 > 1000), column1)",
"logicExecutionMode": "onChange"
}
},
"dataSources": [
{ "name": "ds1", "title": "Invoices", "type": "rest", "params": { "url": "/api/invoices", "method": "GET" } }
]
}Inside a collection function the row fields are written bare and unquoted (column2 == "unpaid"); everything outside the collection keeps its braces.
6.16 Details side panel instead of an inline expand
The same expand machinery renders in a drawer when expandedDisplayMode is sidePanel. rowClickOpensDetails means the whole row opens it, so no chevron column is needed.
{
"elements": {
"el1": {
"name": "el1",
"label": "Applications",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"expandableRows": true,
"expandedDisplayMode": "sidePanel",
"detailsPanelTitle": "Application details",
"rowClickOpensDetails": true,
"showExpandChevron": false,
"rowHover": true,
"rowExpandActions": [
{
"type": "dataSource",
"dataSourceName": "ds2",
"params": [{ "name": "id", "value": "{row.column1}" }]
}
],
"columnsConfig": [
{ "key": "column1", "label": "Ref", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Applicant", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Submitted", "type": "date", "showInTable": true },
{ "key": "column4", "label": "Motivation", "type": "text", "showInTable": false, "showInDetails": true, "detailLabel": "Why they applied" },
{ "key": "column5", "label": "Reviewer notes", "type": "text", "showInTable": false, "showInDetails": true }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Applications", "type": "rest", "params": { "url": "/api/applications", "method": "GET" } },
{ "name": "ds2", "title": "Mark as viewed", "type": "rest", "params": { "url": "/api/applications/{id}/viewed", "method": "POST" } }
]
}rowExpandActions fire when a row is expanded, which is where "mark as read" or "load the heavy part now" belongs. Columns with showInTable: false and showInDetails: true are exactly what fills the panel.
6.17 Row action that downloads a file
responseMode: "download" turns the response into a file instead of data.
{
"elements": {
"el1": {
"name": "el1",
"label": "Invoices",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"rowActionsDisplayMode": "iconButtons",
"columnsConfig": [
{ "key": "column1", "label": "Invoice", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Issued", "type": "date", "showInTable": true }
],
"rowActions": [
{
"label": "Download PDF",
"icon": "download",
"type": "dataSource",
"dataSourceName": "ds2",
"responseMode": "download",
"responseFileName": "invoice-{row.column1}.pdf"
},
{
"label": "Email a copy",
"icon": "mail",
"type": "dataSource",
"dataSourceName": "ds3",
"condition": "notEmpty({row.column3})",
"showToastAfter": true,
"toastTitle": "Sent",
"toastVariant": "success"
}
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Invoices", "type": "rest", "params": { "url": "/api/invoices", "method": "GET" } },
{ "name": "ds2", "title": "Invoice PDF", "type": "rest", "params": { "url": "/api/invoices/{row.column1}/pdf", "method": "GET" } },
{ "name": "ds3", "title": "Email invoice", "type": "rest", "params": { "url": "/api/invoices/{row.column1}/email", "method": "POST" } }
]
}CSV/Excel/PDF export of the grid itself is different: it is showExport and happens entirely in the browser, with no endpoint to implement.
6.18 Export configuration
{
"elements": {
"el1": {
"name": "el1",
"label": "Customers",
"type": "table",
"lazyLoad": true,
"dataSource": { "name": "ds1", "useFor": "value" },
"tableItemsPath": "data.items",
"tableTotalPath": "data.total",
"pageSize": 50,
"showExport": true,
"exportFileName": "customers-2026",
"exportUseColumnPicker": true,
"exportAllPageSize": 20000,
"columnsConfig": [
{ "key": "column1", "label": "ID", "type": "number", "showInTable": true },
{ "key": "column2", "label": "Name", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Created", "type": "dateTime", "dateFormatPattern": "yyyy-MM-dd", "showInTable": true },
{ "key": "column4", "label": "Internal score", "type": "number", "showInTable": false, "showInDetails": true }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers/search", "method": "TABLE-POST" } }
]
}With exportUseColumnPicker the user chooses which columns land in the file. "Export all" re-issues the same table request with pagingParams.pageSize set to exportAllPageSize, so that number is a real memory decision, not decoration.
6.19 A live table over a websocket
{
"elements": {
"el1": {
"name": "el1",
"label": "Live events",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"pageSize": 50,
"stickyHeader": true,
"virtualScroll": true,
"virtualScrollRowHeight": 40,
"virtualScrollViewportHeight": "520px",
"orderClause": "column1 DESC",
"columnsConfig": [
{ "key": "column1", "label": "Time", "type": "dateTime", "dateIncludeSeconds": true, "showInTable": true, "sortable": true },
{
"key": "column2",
"label": "Level",
"type": "status",
"showInTable": true,
"statusShowIcon": true,
"statusRules": [
{ "condition": "value == 'error'", "label": "Error", "tone": "risk", "icon": "error", "rounded": true },
{ "condition": "value == 'warn'", "label": "Warning", "tone": "warning", "icon": "warning", "rounded": true },
{ "condition": "value == 'info'", "label": "Info", "tone": "info", "rounded": true }
]
},
{ "key": "column3", "label": "Message", "type": "text", "showInTable": true }
]
}
},
"dataSources": [
{
"name": "ds1",
"title": "Event stream",
"type": "websocket",
"params": {
"url": "wss://example.test/events",
"protocols": "",
"message": "{\"subscribe\":\"events\"}",
"messagePath": "payload",
"messageMode": "pushValuesInArray"
}
}
]
}messageMode: "pushValuesInArray" appends each message to the accumulated array, which is what a log-style table wants; replaceCurrent swaps the whole set instead. The buffer is capped at 500 entries, so a view left open overnight cannot grow without end. virtualScroll keeps a long list cheap to render.
6.20 Tables nested in containers
A table is a leaf element, so it sits in a column like any other. Inside tabs that column lives in tabRows; inside a dynamicPanel each entry renders its own copy of the table.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1100px", "widthUnit": "px" },
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "el1",
"tabRows": {
"active": [{ "columns": [{ "elementRef": "el2" }] }],
"archived": [{ "columns": [{ "elementRef": "el3" }] }]
}
}
]
},
{
"columns": [
{
"elementRef": "el4",
"rows": [
{ "columns": [{ "elementRef": "el5" }] },
{ "columns": [{ "elementRef": "el6" }] }
]
}
]
}
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Projects", "type": "page", "hideHeader": true },
"el1": {
"name": "el1",
"label": "Projects",
"type": "tabs",
"tabsPosition": "top",
"tabs": [
{ "value": "active", "label": "Active" },
{ "value": "archived", "label": "Archived" }
]
},
"el2": {
"name": "el2",
"label": "Active projects",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"columnsConfig": [
{ "key": "column1", "label": "Project", "type": "text", "showInTable": true },
{ "key": "column2", "label": "Owner", "type": "text", "showInTable": true }
]
},
"el3": {
"name": "el3",
"label": "Archived projects",
"type": "table",
"dataSource": { "name": "ds2", "useFor": "value" },
"columnsConfig": [
{ "key": "column1", "label": "Project", "type": "text", "showInTable": true },
{ "key": "column3", "label": "Archived", "type": "date", "showInTable": true }
]
},
"el4": {
"name": "el4",
"label": "Teams",
"type": "dynamicPanel",
"addRowButtonText": "Add team",
"panelPadding": "16px",
"panelBorderWidth": "1px",
"panelRadius": "8px"
},
"el5": { "name": "el5", "label": "Team name", "type": "text", "required": true },
"el6": {
"name": "el6",
"label": "Members",
"type": "dynamicTable",
"addRowButtonText": "Add member",
"columns": [
{ "name": "column1", "label": "Name", "type": "text" },
{ "name": "column2", "label": "Role", "type": "select", "options": [
{ "value": "dev", "label": "Developer" },
{ "value": "qa", "label": "QA" }
] }
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Active", "type": "rest", "params": { "url": "/api/projects?state=active", "method": "GET" } },
{ "name": "ds2", "title": "Archived", "type": "rest", "params": { "url": "/api/projects?state=archived", "method": "GET" } }
],
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}Note which repeating element goes where: a grid of editable cells inside a repeating block is dynamicTable; a read-only view of server data would be table. A table inside a dynamicPanel publishes its state under one shared __table.<name> key, so prefer dynamicTable when each entry needs its own independent state.
6.21 A localized table
Column labels, action labels and messages all go through localization.texts when a translate() call or a translated key is used. The simplest route is to author the labels in the default language and let the host supply the rest.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1100px", "widthUnit": "px" },
"pages": [{ "name": "page1", "rows": [{ "columns": [{ "elementRef": "el1" }] }] }],
"elements": {
"page1": { "name": "page1", "label": "Orders", "type": "page", "hideHeader": true },
"el1": {
"name": "el1",
"label": "Orders",
"type": "table",
"dataSource": { "name": "ds1", "useFor": "value" },
"emptyMessage": "No records found",
"loadingLabel": "Kraunama...",
"quickSearchPlaceholder": "Search",
"actionsLabel": "Veiksmai",
"showQuickSearch": true,
"columnsConfig": [
{ "key": "column1", "label": "Numeris", "type": "text", "showInTable": true },
{
"key": "column2",
"label": "Suma",
"type": "number",
"align": "right",
"formatLocale": "lt-LT",
"numberMinFractionDigits": 2,
"numberUseGrouping": true,
"showInTable": true
},
{
"key": "column3",
"label": "Status",
"type": "element",
"elementType": "customHtml",
"showInTable": true,
"element": { "htmlTemplate": "<span>{translate({row.column3})}</span>" }
}
]
}
},
"dataSources": [
{ "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } }
],
"localization": {
"defaultLanguage": "en",
"languages": ["en", "de"],
"texts": {
"de": { "new": "Neu", "paid": "Bezahlt", "late": "Überfällig" },
"en": { "new": "New", "paid": "Paid", "late": "Late" }
}
}
}translate(value) looks the raw value up in localization.texts[<current language>], which is how a raw server code such as paid becomes a readable label without a lookup table in the view.
6.22 Choosing the right table
| The screen needs | Use |
|---|---|
| Users type into every row, add and delete rows | dynamicTable |
| A read-only or lightly interactive grid over server data | table |
| Server owns paging, sorting and filtering | table with lazyLoad: true and TABLE-POST |
| Cards instead of rows | listGrid |
| A repeating block of fields rather than a grid of cells | dynamicPanel |
table never uses column.rows; its cells are columnsConfig, keyed by key. dynamicTable never uses columnsConfig; its cells are columns, keyed by name.
6.23 table property map
Grouped so nothing has to be guessed.
| Group | Properties |
|---|---|
| Data | dataSource tableItemsPath tableTotalPath lazyLoad params orderClause orderDirection |
| Columns | columnsConfig showColumnSettings columnSettingsMode columnSettingsStorageKey columnSettingsDialogTitle columnSettingsDataSourceName columnSettingsDataSourcePath saveColumnSettingsDataSourceName resetColumnSettingsDataSourceName |
| Paging | pageSize pageSizeOptions paginator paginatorStyle paginatorVariant paginatorAlign paginatorMaxButtons showPageSizeSelector prevLabel nextLabel |
| Search & filters | showQuickSearch quickSearchPlaceholder quickSearchParamName quickSearchCondition quickSearchCaseMode searchDebounceMs showDetailedSearch detailedSearchCaseMode enableSavedFilters savedFiltersMode savedFiltersDataSourceName savedFiltersItemsPath saveFilterDataSourceName deleteFilterDataSourceName savedFilterIdKey savedFilterNameKey savedFilterCodeKey savedFilterDescriptionKey savedFilterPayloadKey |
| Actions | rowActions rowActionsDisplayMode rowActionsDropdownButtonStyle rowActionVisibilityPath rowClickActions rowExpandActions headerActions headerActionsPosition headerActionsDisplayMode headerMenuEnabled headerMenuActions headerMenuOptions headerMenuPlaceholder headerMenuDataSourceName headerMenuDataSourceItemsPath headerMenuItemsPath headerMenuLabelKey headerMenuValueKey actionsLabel showActionsHeaderLabel |
| Selection | enableSelection selectionKey selectionActions selectionActionsLabel selectionItemTemplate selectLabel selectAllLabel selectRowLabel autoSelectCondition |
| Inline edit | enableInlineEdit inlineEditStartMode inlineEditSaveDataSourceName inlineEditCancelOnReload |
| Expand & details | expandableRows expandedDisplayMode expandedColumnsConfig expandedDataSourceName expandedDataSourceItemsPath expandedItemsPath expandedTemplate expandedEmptyMessage expandedRowActions showExpandChevron alwaysShowExpandChevron expandLabel collapseLabel rowClickOpensDetails detailsPanelTitle |
| Export | showExport exportFileName exportUseColumnPicker exportAllPageSize |
| Appearance | tableStyle stickyHeader rowHover responsiveMode flatHeaderSurface flatFooterSurface showHeaderControls headerCenterElementsPosition headerTemplate rowTemplate rowTemplateName emptyMessage loadingLabel width tabletWidth mobileWidth hidden visibleIf |
| Virtual scroll | virtualScroll virtualScrollRowHeight virtualScrollViewportHeight virtualScrollBuffer |
Column (columnsConfig[*]) properties:
| Group | Properties |
|---|---|
| Identity | key (required) label showHeaderLabel mobileLabel type showInTable showInDetails detailLabel sortable width align visibleIf |
| Formatting | formatLocale dateFormatPattern dateIncludeSeconds numberMinFractionDigits numberMaxFractionDigits numberUseGrouping |
| Element cells | elementType element controlActiveIf controlEnabledIf |
| Status cells | statusRules statusShowIcon |
| Templates | template templateName templateFieldMap |
| Filters | filterControlType filterOptionsSourceType filterOptions filterOptionsJson filterOptionsPath filterOptionsDataSourceName filterOptionsDataSourceItemsPath filterOptionLabelKey filterOptionValueKey filterMultiValueDelimiter filterToggleTrueValue filterToggleFalseValue |
| Inline edit | editable editorType editorPlaceholder editorOptions editorOptionLabelKey editorOptionValueKey |
| Cell actions | cellActionsEnabled cellActions cellActionsDisplayMode cellActionsPlaceholder |
Enumerations, exact values:
| Property | Values |
|---|---|
columnsConfig[*].type | text date dateTime number element (legacy, still rendered: boolean html status toggleSwitch singleCheckbox) |
columnsConfig[*].align | left center right |
columnsConfig[*].filterControlType | auto text number date dateTime boolean select multiSelect toggle |
columnsConfig[*].editorType | auto text number date textarea checkbox toggle select |
columnsConfig[*].filterOptionsSourceType | static datasource |
statusRules[*].tone | neutral success warning risk primary danger info |
tableStyle | default grid striped |
responsiveMode | auto stack |
rowActionsDisplayMode | dropdown iconButtons buttons |
rowActionsDropdownButtonStyle | borderless default |
headerActionsDisplayMode | buttons dropdown |
headerActionsPosition | start center end |
headerCenterElementsPosition | left center right |
paginatorStyle | default compact segmented |
paginatorVariant | full pager numbers simple |
paginatorAlign | start center end |
quickSearchCondition | %-% !%-% %- -% = != |
quickSearchCaseMode, detailedSearchCaseMode | caseInsensitiveLatin caseSensitive uppercase lowercase |
columnSettingsMode | none localStorage dataSource host |
savedFiltersMode | localStorage dataSource host |
expandedDisplayMode | sidePanel inline |
inlineEditStartMode | action doubleClick |
orderDirection | asc desc |
7. dynamicTable: editable repeating rows
dynamicTable is the editable grid: users add and delete rows and type into cells. Its cells are declared in its own columns array, where each entry is a full element definition.
{
"pages": [
{
"name": "page1",
"rows": [
{ "columns": [{ "elementRef": "el1" }] },
{ "columns": [{ "elementRef": "el2" }] }
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Invoice", "type": "page" },
"el1": {
"name": "el1",
"label": "Invoice lines",
"type": "dynamicTable",
"addRowButtonText": "Add line",
"emptyMessage": "No lines yet",
"maxRows": 50,
"disallowAddRows": false,
"disallowDeleteRows": false,
"confirmRowDeletion": true,
"confirmDeleteTitle": "Remove line",
"confirmDeleteMessage": "Remove this invoice line?",
"confirmDeleteConfirmLabel": "Remove",
"confirmDeleteCancelLabel": "Keep",
"hideRowIf": "{row.column5} == true",
"columns": [
{
"name": "column1",
"label": "Product",
"type": "select",
"strictOptions": true,
"options": [
{ "value": "p1", "label": "Widget" },
{ "value": "p2", "label": "Gadget" }
]
},
{
"name": "column2",
"label": "Qty",
"type": "number",
"min": 1,
"step": 1,
"defaultValue": 1,
"valueStorageType": "number"
},
{
"name": "column3",
"label": "Unit price",
"type": "number",
"step": 0.01,
"valueStorageType": "number",
"visualFormatEnabled": true,
"visualFormatMinFractionDigits": 2,
"visualFormatMaxFractionDigits": 2
},
{
"name": "column4",
"label": "Line total",
"type": "number",
"readOnly": true,
"valueStorageType": "number",
"expression": "{row.column2} * {row.column3}",
"logicExecutionMode": "onChange",
"useTotals": true,
"totalUseGrouping": true,
"totalFractionDigits": 2,
"totalLocale": "lt-LT"
},
{
"name": "column5",
"label": "Cancelled",
"type": "singleCheckbox",
"checkboxLabel": "Cancelled",
"defaultValue": false
}
]
},
"el2": {
"name": "el2",
"label": "Invoice total",
"type": "number",
"readOnly": true,
"expression": "{el1.column4-total}",
"logicExecutionMode": "onChange"
}
}
}The rules that make this work:
- Inside a row, other cells of the same row are
{row.columnX}. A bare{column3}reads a top-level form field with that name, not the cell. useTotals: truepublishes a footer sum. That sum is readable elsewhere as{el1.column4-total}: the table name, a dot, the column name,-total.totalToData: truepublishes the sum without drawing the footer row.hideRowIfis evaluated per row, also against{row.*}.- Column entries are element definitions, so any leaf element type and its own properties are valid there (
selectwithoptions,datepickerwithformat,textwithmaskType, and so on).
Value shape: an array of objects, one per row, keyed by column name.
8. dynamicPanel: repeating groups, with nesting
dynamicPanel repeats a whole layout block. Its children live in the layout tree exactly like a panel's, in column.rows. Each rendered entry gets its own copy.
{
"schemaVersion": 1,
"settings": { "language": "en", "locale": "en-US", "width": "1000px", "widthUnit": "px" },
"pages": [
{
"name": "page1",
"rows": [
{
"columns": [
{
"elementRef": "el1",
"rows": [
{ "columns": [{ "elementRef": "el2" }, { "elementRef": "el3" }] },
{ "columns": [{ "elementRef": "el4" }] },
{ "columns": [{ "elementRef": "el5" }] }
]
}
]
},
{ "columns": [{ "elementRef": "el6" }] },
{ "columns": [{ "elementRef": "el7" }] }
]
}
],
"elements": {
"page1": { "name": "page1", "label": "Projects", "type": "page" },
"el1": {
"name": "el1",
"label": "Projects",
"type": "dynamicPanel",
"addRowButtonText": "Add project",
"removeRowButtonText": "Remove project",
"emptyMessage": "No projects yet",
"maxRows": 10,
"confirmRowDeletion": true,
"confirmDeleteTitle": "Remove project",
"confirmDeleteMessage": "Remove this project and all of its tasks?",
"panelPadding": "16px",
"panelRadius": "8px",
"panelBorderWidth": "1px",
"contentGap": "12px",
"resetChildrenOnHide": true
},
"el2": { "name": "el2", "label": "Project name", "type": "text", "required": true },
"el3": {
"name": "el3",
"label": "Status",
"type": "select",
"strictOptions": true,
"options": [
{ "value": "open", "label": "Open" },
{ "value": "done", "label": "Done" }
]
},
"el4": {
"name": "el4",
"label": "Tasks",
"type": "dynamicTable",
"addRowButtonText": "Add task",
"columns": [
{ "name": "column1", "label": "Task", "type": "text" },
{
"name": "column2",
"label": "Hours",
"type": "number",
"step": 0.5,
"valueStorageType": "number",
"useTotals": true,
"totalFractionDigits": 1
},
{
"name": "column3",
"label": "Rate",
"type": "number",
"step": 0.01,
"valueStorageType": "number"
},
{
"name": "column4",
"label": "Cost",
"type": "number",
"readOnly": true,
"valueStorageType": "number",
"expression": "{row.column2} * {row.column3}",
"logicExecutionMode": "onChange",
"useTotals": true,
"totalFractionDigits": 2
}
]
},
"el5": {
"name": "el5",
"label": "Project cost",
"type": "number",
"readOnly": true,
"expression": "{panel.el4.column4-total}",
"logicExecutionMode": "onChange"
},
"el6": {
"name": "el6",
"label": "Total cost of all projects",
"type": "number",
"readOnly": true,
"expression": "sumArray({el1}, el5)",
"logicExecutionMode": "onChange"
},
"el7": {
"name": "el7",
"label": "Open projects",
"type": "number",
"readOnly": true,
"expression": "countInArray({el1}, el3 == \"open\")",
"logicExecutionMode": "onChange"
}
},
"localization": { "defaultLanguage": "en", "languages": ["en"] }
}Context rules for nested structures, in one place:
| Where you are | How you reach a value |
|---|---|
| Top level of the form | {el1} |
Inside a dynamicTable row | {row.column2} for a sibling cell |
Inside a dynamicPanel entry | {panel.el3} for a sibling field of the same entry |
A dynamicTable column total | {el1.column4-total} at top level |
| A total inside a panel entry | {panel.el4.column4-total} |
| A specific panel entry from outside | {el1[0].el4.column4-total} |
| Across all entries of a panel | sumArray({el1}, el5), countInArray({el1}, el3 == "open") |
| A runtime variable | {__variables.tenantId} |
| A table's own live state | {__table.el2.selectedRows}, {__table.el2.totalRecords} (see 6.5) |
| Host-supplied external context | {__external.userName} |
Inside collection functions the entry fields are written bare and unquoted (el3 == "open"), while anything outside the collection still uses braces (role == {__variables.requiredRole}).
Prefer one aggregate over repeated accumulation. setValue({var1}, sumArray({el1}, el5[].column3)) is safe on every recalculation; sumValue adds again on every run and belongs in an action, never in an expression.
9. Data sources and runtime variables
9.1 Every source type
dataSources[*] is { name, title, type, params }, where params differs per type. type is rest, local, route or websocket.
{
"dataSources": [
{
"name": "ds1",
"title": "Customers (GET)",
"type": "rest",
"params": { "url": "/api/customers", "method": "GET" }
},
{
"name": "ds2",
"title": "Save customer (POST)",
"type": "rest",
"params": {
"url": "/api/customers/{id}",
"method": "POST",
"body": { "name": "{el1}", "segment": "{el2}", "tenantId": "{__variables.tenantId}" }
}
},
{
"name": "ds3",
"title": "Server-side table",
"type": "rest",
"params": { "url": "/api/customers/search", "method": "TABLE-POST" }
},
{
"name": "ds4",
"title": "Static options",
"type": "local",
"params": {
"localMode": "json",
"dataJson": "[{\"id\":\"LT\",\"name\":\"Lithuania\"},{\"id\":\"LV\",\"name\":\"Latvia\"}]"
}
},
{
"name": "ds5",
"title": "Options already in the form data",
"type": "local",
"params": { "localMode": "dataPath", "dataPath": "el9.items" }
},
{
"name": "ds6",
"title": "Resolved route data",
"type": "route",
"params": { "routeDataKey": "customer", "routeDataPath": "data" }
},
{
"name": "ds7",
"title": "Live feed",
"type": "websocket",
"params": {
"url": "wss://example.test/feed",
"protocols": "",
"message": "{\"subscribe\":\"orders\"}",
"messagePath": "payload",
"messageMode": "pushValuesInArray"
}
}
]
}REST URL and body templates use single braces: {el1}, {row.id}, {__variables.tenantId}. Method is one of GET POST PUT PATCH DELETE, plus the special TABLE-POST for lazy tables.
9.2 Element-level dataSource and cascading selects
The element-level dataSource object is IElementDataSource: name, useFor, optionValue, optionLabel, filterOptionsBy, params, refreshOnChange, refreshPaths. Nothing else.
{
"elements": {
"el1": {
"name": "el1",
"label": "Country",
"type": "select",
"showSearch": true,
"strictOptions": true,
"dataSource": {
"name": "ds1",
"useFor": "option",
"optionValue": "id",
"optionLabel": "name"
}
},
"el2": {
"name": "el2",
"label": "City",
"type": "select",
"strictOptions": true,
"disableIf": "isEmpty({el1})",
"resetIf": "isEmpty({el1})",
"logicExecutionMode": "onChange",
"dataSource": {
"name": "ds2",
"useFor": "option",
"optionValue": "id",
"optionLabel": "name",
"params": [{ "name": "country", "value": "{el1}" }],
"refreshOnChange": true,
"refreshPaths": ["el1"]
}
},
"el3": {
"name": "el3",
"label": "Warehouse",
"type": "select",
"strictOptions": true,
"dataSource": {
"name": "ds3",
"useFor": "option",
"optionValue": "id",
"optionLabel": "title",
"filterOptionsBy": "item.cityId == {el2} && item.active == true",
"refreshPaths": ["el2"]
}
},
"el4": {
"name": "el4",
"label": "Loaded name",
"type": "text",
"expression": "{__variables.recordData}.name",
"logicExecutionMode": "onChange"
}
},
"dataSources": [
{ "name": "ds1", "title": "Countries", "type": "rest", "params": { "url": "/api/countries", "method": "GET" } },
{ "name": "ds2", "title": "Cities", "type": "rest", "params": { "url": "/api/countries/{country}/cities", "method": "GET" } },
{ "name": "ds3", "title": "Warehouses", "type": "rest", "params": { "url": "/api/warehouses", "method": "GET" } },
{ "name": "ds4", "title": "Record", "type": "rest", "params": { "url": "/api/records/{__variables.recordId}", "method": "GET" } }
]
}useFor: "option"feeds the option list;useFor: "value"feeds the element's value.params[*].valueresolves form fields,{row.*}and{__variables.*}; the resolved values also fill{placeholder}tokens in the source URL.filterOptionsByfilters the fetched list client-side. The candidate option isitem.*; form fields stay in braces.
9.3 Runtime variables
{
"settings": {
"language": "en",
"variables": [
{ "name": "recordId", "title": "Record id", "sourceType": "route", "source": "id" },
{ "name": "tenantId", "title": "Tenant", "sourceType": "external", "source": "tenantId", "fallbackValue": "" },
{ "name": "apiBase", "title": "API base", "sourceType": "constant", "constantValue": "/api/v2" },
{
"name": "isEdit",
"title": "Edit mode",
"sourceType": "expression",
"expression": "notEmpty({__variables.recordId})",
"refreshPaths": []
},
{
"name": "settingsData",
"title": "Settings",
"sourceType": "dataSource",
"source": "ds1",
"fallbackValue": {},
"includeInDataJson": false
},
{
"name": "draftName",
"title": "Draft name",
"sourceType": "manual",
"targetPath": "el1",
"includeInDataJson": true
}
]
}
}sourceType is exactly one of route external constant expression dataSource manual. Read a variable anywhere as {__variables.name}, never as {name}. A dataSource variable needs an explicit source.
10. Expression cookbook
Every logic field is a direct property of the element. There is no logic wrapper. All of them take JEXL.
{
"elements": {
"el1": { "name": "el1", "label": "Price", "type": "number", "valueStorageType": "number" },
"el2": { "name": "el2", "label": "Quantity", "type": "number", "valueStorageType": "number" },
"el3": {
"name": "el3",
"label": "Total",
"type": "number",
"readOnly": true,
"expression": "{el1} * {el2}",
"logicExecutionMode": "onChange",
"dependsOn": ["el1", "el2"]
},
"el4": {
"name": "el4",
"label": "Customer type",
"type": "radio",
"showInline": true,
"defaultValue": "person",
"options": [
{ "value": "person", "label": "Person" },
{ "value": "company", "label": "Company" }
]
},
"el5": {
"name": "el5",
"label": "Company code",
"type": "text",
"visibleIf": "{el4} == 'company'",
"requireIf": "{el4} == 'company'",
"resetIf": "{el4} != 'company'",
"logicExecutionMode": "onChange"
},
"el6": {
"name": "el6",
"label": "Discount %",
"type": "number",
"disableIf": "{el3} < 100",
"readonlyIf": "{__variables.isEdit} != true",
"logicExecutionMode": "onChange",
"validators": [
{ "type": "min", "value": 0, "message": "Cannot be negative" },
{ "type": "max", "value": 50, "message": "50% is the maximum", "applyIf": "{el4} == 'person'" },
{
"type": "custom",
"condition": "{el6} > 0 && isEmpty({el7})",
"message": "Give a reason for the discount"
}
]
},
"el7": { "name": "el7", "label": "Discount reason", "type": "textarea", "rows": 2 },
"el8": {
"name": "el8",
"label": "Starts",
"type": "datepicker",
"format": "YYYY-MM-DD"
},
"el9": {
"name": "el9",
"label": "Ends",
"type": "datepicker",
"format": "YYYY-MM-DD",
"minValue": "{el8}",
"validators": [
{
"type": "custom",
"condition": "notEmpty({el8}) && notEmpty({el9}) && dateDiffDays({el8}, {el9}) < 1",
"message": "End date must be after the start date"
}
]
},
"el10": {
"name": "el10",
"label": "Summary",
"type": "text",
"readOnly": true,
"expression": "getLabel({el4}) + ': ' + {el3} + ' EUR'",
"logicExecutionMode": "onChange"
},
"el11": {
"name": "el11",
"label": "Tags",
"type": "multiSelect",
"options": [
{ "value": "a", "label": "A" },
{ "value": "b", "label": "B" }
]
},
"el12": {
"name": "el12",
"label": "Has tag A",
"type": "singleCheckbox",
"checkboxLabel": "Tagged A",
"readOnly": true,
"expression": "contains({el11}, 'a')",
"logicExecutionMode": "onChange"
}
}
}Operators JEXL supports: && || ! == != > >= < <= + - * / // % ^ in, the ternary a ? b : c, and the Elvis a ?: b. It does not support ??, ?., arrow functions or any other JavaScript-only syntax.
Built-in functions: isEmpty notEmpty toNumber inRange len contains containsAny containsAll inArray collectValuesFrom sumInArray avgInArray minInArray maxInArray countInArray firstInArray lastInArray joinInArray filterArray mapArray findInArray existsInArray sumArray avgArray getFirst getLast flattenArray today now date day month year weekDay weekDayIndex isWeekend addDays dateDiffDays startOfWeek endOfWeek getVal getValue getLabel getElementProperty getProp translate currentLanguage setValue sumValue pushValue setVar sumVar pushVar setElementProperty runDataSource dbg.
Validator objects accept exactly type, value, message, condition, applyIf. condition is the failing check: the error shows while it is true.
11. Actions and events
events is an array of IElementActionConfig. Same shape for button.events, table.rowActions, table.headerActions, table.selectionActions, cell actions and dialog footer actions.
{
"elements": {
"el1": {
"name": "el1",
"label": "",
"type": "button",
"text": "Save",
"variant": "solid",
"tone": "primary",
"icon": "save",
"iconPosition": "left",
"size": "normal",
"fitContent": true,
"events": [
{
"trigger": "click",
"type": "dataSource",
"dataSourceName": "ds1",
"validateForm": true,
"confirmEnabled": true,
"confirmTitle": "Save changes",
"confirmMessage": "Write these changes to the server?",
"confirmConfirmLabel": "Save",
"confirmCancelLabel": "Cancel",
"params": [
{ "name": "id", "value": "{__variables.recordId}" },
{ "name": "name", "value": "{el3}" }
],
"responseMode": "setValue",
"responseDataPath": "data.id",
"responseTargetPath": "el4",
"reloadOnReturnElementNames": ["el5"],
"showToastAfter": true,
"toastTitle": "Saved",
"toastMessage": "The record was stored",
"toastVariant": "success",
"toastPosition": "bottom-right",
"toastAutoHide": true,
"toastAutoHideMs": 3000
}
],
"menuActions": [
{ "label": "Save and close", "type": "submit", "validateForm": true },
{ "label": "Discard", "type": "navigate", "navigateTo": "/records" }
]
},
"el2": {
"name": "el2",
"label": "",
"type": "button",
"text": "Download report",
"variant": "outline",
"tone": "neutral",
"fitContent": true,
"events": [
{
"trigger": "click",
"type": "dataSource",
"dataSourceName": "ds2",
"responseMode": "download",
"responseFileName": "report.pdf"
}
]
},
"el3": { "name": "el3", "label": "Name", "type": "text" },
"el4": { "name": "el4", "label": "Saved id", "type": "text", "readOnly": true },
"el5": { "name": "el5", "label": "History", "type": "table", "columnsConfig": [] },
"el6": {
"name": "el6",
"label": "Segment",
"type": "select",
"options": [
{ "value": "a", "label": "A" },
{ "value": "b", "label": "B" }
],
"events": [
{
"trigger": "change",
"type": "setOptions",
"setOptionsTargetElement": "el7",
"setOptionsMode": "contextPath",
"setOptionsValue": "lookup.subSegments"
},
{
"trigger": "change",
"type": "reloadElements",
"reloadElementNames": ["el5"],
"condition": "notEmpty({el6})"
}
]
},
"el7": { "name": "el7", "label": "Sub-segment", "type": "select", "options": [] }
}
}Action fields by type:
type | Required / typical fields |
|---|---|
navigate | navigateTo, openInNewTab, beforeNavigateDataSourceName |
dataSource | dataSourceName, params, responseMode (none download setValue), responseDataPath, responseTargetElement, responseTargetPath, responseFileName, reloadCurrentElementAfterSuccess, reloadOnReturnElementNames, reloadDataSourceNames |
setValue | setValueTargetPath, setValueMode (contextPath template json expression), setValueValue |
setOptions | setOptionsTargetElement, setOptionsMode, setOptionsValue |
reloadElements | reloadElementNames |
toast | toastTitle, toastMessage, toastVariant, toastPosition, toastAutoHide, toastAutoHideMs, toastShowIcon, toastIcon |
dialog | dialogName, dialogOperation (open close toggle) |
sendMessage | dataSourceName (a websocket source), messagePayload |
validate | none; validates the whole form |
submit | none; validates, then fires onComplete with { isValid, data, issues } |
Common to all: trigger (click submit input change blur focus beforeLoad onLoad afterLoad always), label, icon, hideText, condition, validateForm, debounceMs, confirmEnabled, confirmOnDirty, confirmTitle, confirmMessage, confirmConfirmLabel, confirmCancelLabel, buttonVariant (filled outlined text), buttonTone (primary risk neutral), useCustomButtonStyle, showToastAfter and the toast* fields.
Note the two different button vocabularies: a button element uses variant (solid outline text) and tone (primary neutral success info warning risk); an action rendered as a button uses buttonVariant (filled outlined text) and buttonTone (primary risk neutral).
12. Multi-page stepper
Several entries in pages become steps. Each still needs its twin in elements.
{
"schemaVersion": 1,
"settings": {
"language": "en",
"locale": "en-US",
"width": "760px",
"widthUnit": "px",
"pageNavigationMode": "stepper",
"stepperPosition": "top",
"pageNavigationPosition": "bottom",
"allowStepWithoutValidation": false,
"actionButtonsPosition": "bottom",
"showSubmitButton": true,
"showValidateButton": false,
"showValidationIssuesModal": true
},
"pages": [
{ "name": "page1", "rows": [{ "columns": [{ "elementRef": "el1" }, { "elementRef": "el2" }] }] },
{ "name": "page2", "rows": [{ "columns": [{ "elementRef": "el3" }] }] },
{ "name": "page3", "rows": [{ "columns": [{ "elementRef": "el4" }] }] }
],
"elements": {
"page1": { "name": "page1", "label": "Applicant", "type": "page" },
"page2": { "name": "page2", "label": "Address", "type": "page" },
"page3": {
"name": "page3",
"label": "Review",
"type": "page",
"visibleIf": "notEmpty({el1}) && notEmpty({el2})"
},
"el1": { "name": "el1", "label": "First name", "type": "text", "required": true },
"el2": { "name": "el2", "label": "Last name", "type": "text", "required": true },
"el3": { "name": "el3", "label": "Street", "type": "text", "required": true },
"el4": {
"name": "el4",
"label": "",
"type": "customHtml",
"htmlTemplate": "<p>Submitting as <b>{el1} {el2}</b>, {el3}</p>"
}
},
"localization": {
"defaultLanguage": "en",
"languages": ["en", "de"],
"texts": {
"en": { "submitButtonText": "Submit" },
"de": { "submitButtonText": "Absenden" }
}
}
}13. Pre-return checklist for any of these
- Every
elementRefresolves; everyelementskey equals itsname. - Every
pages[*].namehas anelementstwin withtype: "page". - No element definition holds children (
rows,columns,children,items); the exception isdynamicTable.columnsandtable.columnsConfig, which are cell definitions, not layout. - No column object holds anything besides
elementRef,rows,tabRows. tablecolumns usekey.dynamicTablecolumns usename.statusRules[*].conditionusesvalue;rowActions[*].conditionusesrow.*.- Action and element-datasource
paramsare[{ "name", "value" }]; table request params are[{ "paramName", "paramValue" }]. - Runtime variables are read as
{__variables.x}. - Logic fields sit directly on the element; validators use
condition/applyIf. - Every type string matches the canonical list exactly, casing included.
