YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Performance▾
Native▾
Development▾

Form From Schema

Once the model's schema is designed, a form is a thin layer over it. The shell around the form prepares the data, and the Template only draws the fields.
Words used on this page
Template
The client component in <Model>.Template.tsx that draws the form's fields.
articleForm
The store's copy of the record being written. st.use.articleForm() reads it.
edit shell
A wrapper such as Load.Edit or Model.Edit that fills, opens and saves the form.
fetch.slice.<name>
Tells a shell which model to save and which list a new record joins.
Pick the shell by where the form opens:
Load.Edit
A page that creates or edits one record, such as page/…/new.tsx or page/…/edit.tsx.
Model.Edit
An edit button on a list row or a card that opens a modal. It goes in Article.Util.tsx.
Model.ViewEditModal
A card click opens the detail, which then turns into the form. It goes in Article.Zone.tsx.
  • One Template serves all three. It reads st.use.articleForm() and never checks whether it is creating or editing.
  • The shell decides the rest: what fills the form first, where saving leads, and whether it is a page or a modal.

Keep The Template Simple

A Template does not decide where the form came from. It reads the current form state and connects each field to its store setter.
st.use.articleForm()
The form state: defaults for a new record, the loaded record for an edit.
st.do.set<Field>OnArticle
One setter per field, such as setTitleOnArticle. Hand it to onChange as is.
Field.*
A label plus a control: Field.Text, Field.TextArea, Field.ToggleSelect, Field.Date and more.
A Template for an article with a title, a body and a status:
apps/koyo/lib/article/Article.Template.tsx
  • "use client" on line 1, and no useState. Every value lives in articleForm, which is what lets create, edit and the modal share this file.
  • An enum goes straight into items. Field.ToggleSelect labels each value from the dictionary.
  • Normalize the input with transform, such as transform={(v) => v.toLowerCase()} on Field.Text. The setter itself still goes to onChange untouched.

Create With SSR

When the page already knows some values, put them in a seed object and hand it to Load.Edit. Parent ids, the current org, a default status and values from the URL all belong here.
A new-article page under a board, rendered on the server with the board already filled in:
apps/koyo/page/board/[boardId]/article/new.tsx
  • A seed opens a new form. Fields you leave out take the model's defaults, and the user never has to pick a hidden value such as the parent id.
  • type="form" draws the form in place with a save button under it. Leave it out and the form opens in a modal instead.
  • onSubmit is where to go after saving. [articleId] in the path becomes the new record's id, so "/article/[articleId]" opens what was just created.
  • A value the page has to fetch, such as a parent's setting, is awaited in the page and goes into the same seed.

Update Page

For a full edit page, fetch the record on the server and pass it to Load.Edit. The Template is exactly the one the create page uses:
apps/koyo/page/article/[articleId]/edit.tsx
  • fetch.editArticle fills the form with the saved record. The save button then reads Update instead of Create.
  • The articleEdit promise goes across un-awaited. The page is sent at once, and a skeleton (or your loading) holds the form's place until the record lands.
  • Await when the page needs the record itself, for a heading or a URL: const { article, articleEdit } = await fetch.editArticle(articleId). The page then waits for the record before it is sent.

Edit In A Modal

When the user is already looking at a list or a card, editing in a modal is faster than moving to a new page. There are two shapes:
Model.Edit
An Edit button that opens the form in a modal. Put it in a row, a dropdown or a card.
<Model.Edit slice modelId renderTitle>
Model.ViewEditModal
A card click opens the detail view, and its Edit button turns the same modal into the form.
<Model.ViewEditModal slice renderView renderTemplate>
Model.Edit — an edit button
A Util that draws the button and the modal for one article:
apps/koyo/lib/article/Article.Util.tsx
  • A click loads the record. It calls st.do.editArticle(articleId), which fetches the record into articleForm and opens the modal.
  • Saving closes the modal and updates the record in every list already on screen.
  • renderTitle="title" titles the modal with the model name and the form's title. trigger replaces the default Edit button.
  • Button and modal in different places? Model.Edit is Model.EditWrapper (the trigger) plus Model.EditModal id={articleId} (the modal), so use the two separately.
Model.ViewEditModal — view, then edit
One modal beside the list serves every card in it:
apps/koyo/lib/article/Article.Zone.tsx
  • Model.ViewWrapper opens the view. A card click calls st.do.viewArticle(id), and the modal draws renderView.
  • Edit swaps in renderTemplate, and Save returns to the view with the updated record. The ⋮ menu holds Remove, and menu={false} hides it.
  • renderTitle, editLabel and saveLabel change the modal title and the two button labels.
  • It lives in a Zone, not a page. renderView and renderTemplate are functions, and a server page cannot pass a function to a client component.

Options And Tips

Load.Edit hands these props on to Model.EditModal, which takes them too. On Model.EditModal, onSubmit and onCancel may also be functions.
Load.Edit props
sliceSliceMeta
Required. fetch.slice.<name>: the model to save and the list a new record joins.
editPartial<Model> | ClientEdit
Required. A seed object opens a new form; fetch.edit<Model> opens a saved record.
type"modal" | "form" | "empty"default "modal"
form draws the form in place with a save button; empty draws the fields only.
modalstringdefault "edit"
The store's modal name that opens this form. Give a second form of the same model its own name.
onSubmitstring
After saving: a path, back, or reset. [articleId] in a path becomes the saved id.
onCancelstring
When the modal closes: a path, back, or reset. type=form draws no cancel control.
submitOptionCreateOption
Store options for the save. { path: "self" } also writes the saved record into self.
submitTextstring
Save button label. Without it, the button reads Create or Update plus the model name.
renderSubmitbooleandefault true
false hides the save button, so you can call st.do.submitArticle() from your own.
checkSubmitbooleandefault true
Keeps save disabled until the form passes the model's input rules.
loadingReactNodedefault Loading.Skeleton
Shown while an un-awaited edit promise is still pending.
draftboolean | stringdefault true
Draft recovery. false turns it off; a string names the scope.
classNamestring
Wrapper classes. modalClassName and submitClassName style the modal and the save button.
Tips
  • Reuse one Template for the create page, the update page and the edit modal.
  • Do not ask the user for hidden values such as a parent id. Prepare them on the server and put them in the seed.
  • Page moves go in onSubmit, store writes in submitOption. On a profile form, submitOption={{ path: "self" }} keeps st.use.self() current after saving.
  • When field logic grows, split it into small field groups, but keep the Template as the form's owner.
  • Never save form values yourself. The shell keeps a draft as the user types and offers it back on the next open; draft={false} turns that off.

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman