Create and edit records
form is the region that writes one row. It renders a control per field, validates the
submission, and writes it in one transaction.
Add a form
Section titled “Add a form”page member "Member" { route: /members/{member} new_route: /members/new access: board
form of members { fields: first_name, last_name, email, fee_group, joined_on actions: save, delete success: "Member saved." }}One page serves both addresses. On /members/new the form is empty and a save inserts. On
/members/42 the form is filled and a save updates.
actions: decides which buttons exist. A delete renders as a separate confirmation that is two
deliberate clicks away, and it needs no JavaScript.
What a save does
Section titled “What a save does”A save is a POST, then a redirect, then a GET. The message survives the redirect, so a reload never repeats a write.
Before anything is written, the engine checks the submission against the column types and against every rule the table declares. If a check fails, the form renders again with the exact text the person typed and a message on each field that failed.
The form does not reformat the other forty fields while it rejects three. A person has to find their own mistake.
Fill in a value the user never types
Section titled “Fill in a value the user never types”set writes a column from the request rather than from a control. The column gets no control
at all, so a request cannot supply it.
page signup "Sign up" { route: /events/{event}/signup access: member
form of event_signups { fields: note actions: save
set event = :route.event set person = :user.person_id }}A hidden field can be posted by anyone. A set is not readable from the request at all, which
is the difference that matters.
Refuse a save with a reason
Section titled “Refuse a save with a reason”deny when refuses the whole form when a condition holds.
form of event_signups { fields: note actions: save
set event = :route.event
deny when count(event_signups.event) >= event.capacity "This event is fully booked."}The message replaces the form on a GET, so nobody fills in a form that cannot be saved. On a POST the same rule is checked again inside the write’s own transaction. Two people who press save at the same moment cannot both pass a capacity check.
Check a value against the row
Section titled “Check a value against the row”A table can declare rules that the engine evaluates against the row that a save produces.
table events "Events" { display: name
name text required "Name" starts_at datetime required "Starts" ends_at datetime "Ends" capacity int "Capacity" { min: 1 }
check ends_at is empty or ends_at >= starts_at "The end must not be before the start."
require capacity when kind = limited "A limited event needs a capacity."
unique(name, starts_at) "An event with this name already exists on that date."}Each rule carries the sentence a person reads. A database constraint that fires is mapped back to the author’s message, so a reader never sees the constraint name.
Let the user create a missing option
Section titled “Let the user create a missing option”A ref field offers the rows that exist. Sooner or later the row somebody needs does not exist
yet, and they have a half-filled form on screen.
Name the page that creates one:
form of events { fields: title, category
field category { add_via: admin_category }}The engine draws a control beside the dropdown. The reader opens it, fills in the new category, and saves. The category becomes the selected option, and what they had typed is still there.
Three things this does not change, and they are the reason it is one line:
- The named page decides who may create a row. A reader that page does not admit sees no control. Do not write a second rule here.
- The named page decides what a save does — its fields, its checks, its refusals. The refusal appears in the dialog, in the words that page would have used.
- The save is validated the same way. The option the browser adds carries a row id, and the form checks every id it receives against the rows that reader may choose.
With JavaScript turned off the control is a link to the create page. The reader makes the row there and comes back. Tell your users nothing about this: it is the walk they take today.
See add_via: for the
compile errors.
Hide a column from a role
Section titled “Hide a column from a role”Mark a column sensitive, then grant it in the rule of each role that can use it. A form
renders a control for a granted column only. See
Control who sees what.
What a form cannot write
Section titled “What a form cannot write”The engine owns some columns and a request can never supply one, even if it names it:
- the primary key.
- the audit columns
created_at,created_by,updated_at,updated_by. - a
slugand anunguessable. - the lifecycle state column.
- the metadata columns of an
img.
A column like this renders as a value and not as a control. A request that names one anyway is ignored, and the engine writes a log line naming the column and the reason.