Skip to content

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

  1. A "New" button clears the form's page-state keys and opens the dialog.
  2. Each field is an input block (core.text-input, core.select, core.date-picker, core.number-input, core.checkbox, core.lookup) whose value binds a page-state key and whose committed event writes ${event.new} back to that key.
  3. Save calls POST /api/v1/entities/<name>/records with a params object 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

  • committed fires with event.new. The value the user just entered is ${event.new}. Not event.value, not event.record. This is true for text inputs, selects, date pickers, checkboxes, and core.lookup.
  • The handler must write back to state. committedsetValue 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 the params object — 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 setValue a real true/10; only defaultValue in 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.editingIdPUT .../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

SymptomCauseFix
Save sends null for a field the user filled incommitted handler missing or reads event.valuewrite setValue page.<key> = ${event.new}
dialog re-opens with last row's datakeys cleared on close instead of on "New"clear in the "New" button chain
grid doesn't show the new rowno refreshTrigger toggle in the Save chainadd setValue page.gridRefreshTick = ${!page.gridRefreshTick}
lookup saves a name, not an idbound the label instead of ${event.new}${event.new} is the id
POST 400s on an enum fieldvalue not one of typeParams.enumValuescheck the entity's enum list