Skip to content

Compose a contact form in a page layout

form-basics explains the form operation itself. This recipe adds the layout and composition around that operation: a contained contact section, an explanatory heading, grouped inputs, and a clear completion control.

  • Create or look up the target model before authoring this page. Substitute its real, per-tenant UUID for every <model-id> below; do not publish a literal placeholder or another tenant’s ID.
  • Use the renderer contract, not the block-pack editor schema, for the form source. The block-pack schema’s form entry has empty contentFields, even though FormBlock.svelte reads these source fields.
  • The target model must permit the intended form write. For a public contact intake, configure public create deliberately; for an authenticated flow, verify the consumer session and policy first.

The complete tree below uses the form contract: source.target_mode: "create", source.model_id, and source.on_success_toast. source.on_success_redirect, source.on_success_set_field, and source.defaults are optional source controls; add only the ones the page needs. Each child input names its model field with binding.field and defers its value with save_mode: "submit". The submit control is a button whose display.type is exactly "submit".

{
"blocks": [
{
"id": "contact-section",
"type": "container",
"display": {"layout": "flex", "direction": "column", "gap": 24, "full_width": true},
"style": {"class": "mx-auto max-w-2xl px-6 py-12"},
"blocks": [
{"id": "contact-heading", "type": "heading", "content": {"text": "Contact us", "level": "h1"}},
{"id": "contact-intro", "type": "text", "content": {"text": "Tell us how we can help and we will reply soon."}},
{
"id": "contact-form",
"type": "form",
"source": {
"target_mode": "create",
"model_id": "<model-id>",
"on_success_toast": "Thanks — we received your message.",
"defaults": {"source": "website"}
},
"display": {"layout": "flex", "direction": "column", "gap": 16},
"blocks": [
{"id": "contact-name", "type": "input", "content": {"label": "Name", "placeholder": "Your name", "required": true}, "binding": {"field": "name", "save_mode": "submit"}},
{"id": "contact-email", "type": "input", "content": {"label": "Email", "placeholder": "you@example.com", "required": true}, "display": {"type": "email"}, "binding": {"field": "email", "save_mode": "submit"}},
{"id": "contact-message", "type": "textarea-block", "content": {"label": "Message", "placeholder": "How can we help?", "required": true}, "binding": {"field": "message", "save_mode": "submit"}},
{"id": "contact-submit", "type": "button", "content": {"label": "Send message", "variant": "default"}, "display": {"type": "submit"}}
]
}
]
}
]
}

Keep optional success behavior at the form level. For example, on_success_redirect can send a visitor to a thank-you page, and on_success_set_field can write a named value into the enclosing form scope after success. defaults pre-populates values that should be submitted even without a visible input; its keys must be valid fields on the chosen model.

form.source.form_id changes which user a rule sees as the actor. A direct authenticated consumer create stamps the application owner, while an embedded model-bound public form submit stamps the validated app-user as submitted_by. Choose the path intentionally before attaching an actor-sensitive rule.