Appearance
Add a create / edit form
What you're doing
Adding a dialog to a list page that creates (or edits) a row. On this platform a "form" for a list page is not a separate artifact — it's a core.dialog block on the page, containing input blocks bound to page state, plus a Save button whose action chain POSTs (or PUTs) to the entity.
The pattern
- A "New" button clears the form's page-state keys and opens the dialog.
- Each field is an input block (
core.text-input,core.select,core.date-picker,core.number-input,core.checkbox,core.lookup) whosevaluebinds a page-state key and whosecommittedevent writes${event.new}back to that key. - Save calls
POST /api/v1/entities/<name>/recordswith aparamsobject built from the page-state keys, closes the dialog, toggles the grid refresh, and shows a toast.
The complete example
From the tutorial's equipment-register.json. The "New" button:
json
{
"blockType": "core.button",
"properties": { "labelKey": { "source": "static", "value": "office-equipment.equipment-register.newBtn" },
"variant": { "source": "static", "value": "contained" } },
"events": { "clicked": { "source": "action-chain", "actions": [
{ "id": "a0", "order": 0, "type": "setValue", "config": { "field": "page.formAssetTag", "value": null } },
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formName", "value": null } },
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.formCategoryId", "value": null } },
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.formStatus", "value": "AVAILABLE" } },
{ "id": "a4", "order": 4, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": true } }
] } }
}One text field inside the dialog:
json
{
"blockType": "core.text-input",
"properties": { "value": { "source": "binding", "binding": { "scope": "page", "key": "formAssetTag" } } },
"events": { "committed": { "source": "action-chain", "actions": [
{ "id": "a0", "order": 0, "type": "setValue",
"config": { "field": "page.formAssetTag", "value": "${event.new}" } }
] } }
}The Save button:
json
{
"blockType": "core.button",
"properties": { "labelKey": { "source": "static", "value": "office-equipment.equipment-register.form.saveBtn" },
"variant": { "source": "static", "value": "contained" } },
"events": { "clicked": { "source": "action-chain", "actions": [
{ "id": "a0", "order": 0, "type": "callApi",
"config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_equipment/records", "httpMethod": "POST",
"params": {
"asset_tag": "${page.formAssetTag}",
"name": "${page.formName}",
"category_id": "${page.formCategoryId}",
"status": "${page.formStatus}"
} },
"output": "createResult" },
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": false } },
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.gridRefreshTick", "value": "${!page.gridRefreshTick}" } },
{ "id": "a3", "order": 3, "type": "showToast", "config": { "message": "Saved." } }
] } }
}Line by line — the rules that bite
committedfires withevent.new. The value the user just entered is${event.new}. Notevent.value, notevent.record. This is true for text inputs, selects, date pickers, checkboxes, andcore.lookup.- The handler must write back to state.
committed→setValue page.<key> = ${event.new}. Without this the input shows what you type but page state never updates, and Save sends nulls. - Save reads from
${page.<key>}in theparamsobject — the same keys the inputs write and the "New" button clears. - Clear the keys in the "New" button, not on dialog close — so re-opening always starts blank (or with defaults like
status: "AVAILABLE"). - Booleans and numbers flow through as their JSON types when you
setValuea realtrue/10; onlydefaultValuein an entity file must be a string.
Editing an existing row
Same dialog. The grid's rowClicked event fetches the record and populates the same page-state keys, then opens the dialog:
json
"events": { "rowClicked": { "source": "action-chain", "actions": [
{ "id": "a0", "order": 0, "type": "callApi",
"config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_equipment/records/${event.id}", "httpMethod": "GET", "params": {} },
"output": "detail" },
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formAssetTag", "value": "${detail.asset_tag}" } },
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.editingId", "value": "${event.id}" } },
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": true } }
] } }Save then branches on page.editingId — PUT .../records/${page.editingId} if set, POST if not. (${event.id} is the clicked row's id.)
A lookup field (foreign key)
json
{
"blockType": "core.lookup",
"properties": {
"value": { "source": "binding", "binding": { "scope": "page", "key": "formCategoryId" } },
"recordType": { "source": "static", "value": "OeqCategory" },
"optionsSourceKey": { "source": "static", "value": "oeq-category-search" }
},
"events": { "committed": { "source": "action-chain", "actions": [
{ "id": "a0", "order": 0, "type": "setValue",
"config": { "field": "page.formCategoryId", "value": "${event.new}" } }
] } }
}optionsSourceKey is the name of a search Data Service. recordType is a documentation-only label. ${event.new} is the picked record's id. See the lookup recipe.
How to verify it worked
Publish, then drive it from the API the same way the Save chain does:
bash
erp api post "/api/v1/entities/oeq_equipment/records" \
--body '{"asset_tag":"LAP-001","name":"Dell Latitude 7440","category_id":1,"status":"AVAILABLE"}'json
{ "id": 1, "created_by": "you@example.com", "asset_tag": "LAP-001",
"name": "Dell Latitude 7440", "status": "AVAILABLE", "warranty_alert": false }If that works, the dialog works.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
Save sends null for a field the user filled in | committed handler missing or reads event.value | write setValue page.<key> = ${event.new} |
| dialog re-opens with last row's data | keys cleared on close instead of on "New" | clear in the "New" button chain |
| grid doesn't show the new row | no refreshTrigger toggle in the Save chain | add setValue page.gridRefreshTick = ${!page.gridRefreshTick} |
| lookup saves a name, not an id | bound the label instead of ${event.new} | ${event.new} is the id |
POST 400s on an enum field | value not one of typeParams.enumValues | check the entity's enum list |