Audience: Contributor
Stored page shape, builder insert path, trim/persist checklist, and worked trees from real children.json files.
See also: Models · Catalog · Change playbooks
This is the primary guide for writing page trees.
Pages/templates/blocks persist an array of IElementReference as children.
AJV validates a wrapper:
{ "type": "page", "children": [ /* same array */ ] }
against generated page.schema.json (npm run schemas:page). Do not store the wrapper in the DB — only the inner children array.
Page-level children are usually:
[
{ "type": "ion-header", "children": [ /* toolbar / title */ ] },
{ "type": "ion-content", "children": [ /* body */ ] },
{ "type": "ion-footer", "children": [ /* optional */ ] }
]
Confirm allowed nesting in the Catalog before inventing structure.
Palette insert uses catalogElementToInstance (ui/main/src/app/service/page-builder-insert.util.ts):
value is empty (ValueOps.isEmpty)name, type, value, fallback, variance, resultExpr (drop responsive, groups, option bags)type, those props, children, actions, data — not root validators (validators nested under children.json still arrive via children)children comes from the catalog definition (children.json / &def.children)Then ContentModel.addChildByDef(...) places the instance.
Builder “can drop inside” checks:
getElementDefinition(type)?.children !== undefined
So a host with children.json (even []) accepts insert-inside. A host that only has schema properties.children without &def.children may not. Always use the catalog’s Insert inside column.
Schema anyOf is what AJV enforces (warn-only on save).
ElementModel.definition → trimmed IElementReference (blocks → BlockOps.createInstance)ElementReferenceOps.trimSchemaService.validateChildren — warn-only; invalid trees can still persistDo not rely on the server to reject bad types or illegal children.
When writing or importing page JSON (API, pack import, HTML clone):
children array only — not the AJV { "type": "page", "children" } wrapperprops / actions / validators / children)id, DOM lkt_id, or builder versionlkt-block instances: blockId (+ optional data.overrides) — never expanded master childrenraw or expr on each valueSee also Operations for callers (save/publish/import).
Never put a string/HTML as a child. Use:
{
"type": "text",
"props": [
{ "name": "html", "value": { "raw": "Hello" } },
{ "name": "wrapper", "value": { "raw": "span" } }
]
}
Instance node (persist this shape only):
{
"type": "lkt-block",
"props": [
{ "name": "blockId", "type": "none", "value": { "raw": 42 } }
]
}
Optional overrides: data.overrides keyed by relative element path. Runtime expands the master; do not serialize expanded children on the instance.
builder/block is in page.schema.json but not upserted by DbInit — instances are created via BlockOps, not the element palette catalog path.
lkt-tabs-shell / ion-tab-button (tab + pageId props) are the source of truth. settings.tabsShell is an optional cache — prefer walking the tree.
children.json)ion-button insertFrom ionic/button/children.json:
{
"type": "ion-button",
"children": [
{
"type": "text",
"props": [
{ "name": "html", "value": { "raw": "Button Text..." } },
{ "name": "wrapper", "value": { "raw": "span" } }
]
}
]
}
[
{
"type": "ion-header",
"children": [
{
"type": "ion-toolbar",
"children": [
{
"type": "ion-title",
"children": [
{
"type": "text",
"props": [
{ "name": "html", "value": { "raw": "Home" } }
]
}
]
}
]
}
]
},
{
"type": "ion-content",
"children": [
{
"type": "p",
"children": [
{
"type": "text",
"props": [
{ "name": "html", "value": { "raw": "Hello" } }
]
}
]
}
]
}
]
Verify each parent’s allow-list in the catalog before shipping — the snippet above is illustrative of shape, not a claim that every nesting is schema-legal without checking.
See lokakrt/form/children.json for a full starter (ion-item + ion-input + validators + lkt-form-error + submit). Copy from that file rather than inventing form wiring.