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.
Workspace▾
App & Library▾
Domain▾
Scalar▾
model.Template.tsx
<Model>.Template.tsx is a module's form: the fields a person fills in to create or edit one record. Most exports are whole forms, but a Template may also export a small piece of one, such as a submit button, an onboarding step or a preview block.A Template only connects the screen to the store. Anything that needs a decision lives somewhere else:
The work
Template
*.Template.tsx
Elsewhere
Drawing the form
Field.*
✓
Labelled controls, each bound to one field of the form draft.
submit button · step · preview
✓
Small interaction pieces that belong to one form.
l("<model>.<field>")
✓
Labels and help text from the module dictionary.
Deciding and saving
business rule
✓
Validation and state transitions go in constant, document and service.
access check
✓
Who may save is decided by the guards in the signal.
fetch.*
✓
A server call and its toasts go in a store action.
open · load · submit
✓
An edit shell does this around the Template.
✓Belongs hereNot here
Words used on this page
TermDescription
<model>Form
The store's draft of the record being edited, such as
ticketForm.st.do.set<Field>On<Model>
The setter the store generates for each field, such as
setTitleOnTicket.fetch.slice.<name>
Tells a Field or a shell which model and which list it works with.
edit shell
A wrapper such as
Load.Edit or Model.Edit that loads, opens and submits the form.File Convention
A Template sits in the module folder, beside the model it edits. Every field reads and writes the store, which exists only in the browser, so its first line is always
"use client".Path
apps/<app>/lib/<model>/<Model>.Template.tsxDatabase and scalar modules may have one. Service modules may not.
First Line
"use client";Always, on line 1 above the imports.
Exports
General · Phone · SubmitPhone · PhoneCodeNamed arrow components. General is the model's main form.
Used As
<Ticket.Template.General />Pages and shells reach it through the model namespace from @apps/<app>/client.
- Name the main form
General.Model.AdminPanelusesTemplate.Generalas its form, and falls back to the first export. - No
useStatein a Template. Form values live in the store.akan quality ssrflags anyuseStatein a Template asakan.ssr.template-client-state.
Standard Form Template
A standard form reads the draft from the store, takes its labels from the dictionary, and writes each field through a generated setter:
apps/koyo/lib/ticket/Ticket.Template.tsx
st.use.ticketForm()reads the draft. The form re-renders whenever the draft changes.labelanddescare dictionary keys.descshows as a help tooltip beside the label.st.do.setTitleOnTicketis generated. The store makes one setter per field, and you hand it over as it is.Layout.Templatespaces the fields evenly. The caller can still adjust it throughclassName.


Pass the setter by reference, never inside an arrow.
onChange={(v) => st.do.setTitleOnTicket(v)} works the same for a person, but the field is no longer published to the in-page agent, and lint rejects it (no-unpublished-form-setter). To clean up a value, use the Field's transform prop; to write another field too, add a _postSet<Field> method to the store.Field Patterns
Field.* components are ready-made form controls with a label row. Pick the one that matches the model field, then connect value and onChange to the store.| Model field | Field |
|---|---|
| ↳ Note | |
| String | Field.Text |
TextArea, Email, Phone and Password are variants for special text. | |
| Int · Float | Field.Number |
DoubleNumber holds two numbers in one row, such as a range. | |
| Boolean | Field.Switch |
| A labelled on/off toggle. | |
| Date | Field.Date |
showTime adds the time of day, and DateRange takes a from/to pair. | |
| enumOf(...) | Field.ToggleSelect |
Each value gets a translated label, and MultiToggleSelect takes an array. | |
| [String] | Field.Tags |
TextList keeps the order and lets the user drag rows. | |
| relation to a model | Field.Parent |
Children takes an array, and ParentId / ChildrenId take ID fields. | |
| File | Field.Img |
Imgs takes [File] and File / Files take other files, all from @libs/shared/ui. | |
| rich text | Field.Rich |
A rich-text editor with attachments, from @libs/shared/ui. | |
| embedded objects | Field.List |
| You render one row; the field draws the add and remove buttons. | |
The basic members come from
akanjs/ui. Field from @libs/shared/ui holds them all and adds Rich, Img, Imgs, File, Files, Coordinate and Postcode, so import it from there. Here are four fields that need more than value and onChange, added to the same form:apps/koyo/lib/ticket/Ticket.Template.tsx
Field.Parentpicks a related model. The options come from theslicelist, andrenderOptiondraws each one.Field.ToggleSelecttakes anenumOfclass asitems. Each value becomes a translated button.Field.Imguploads throughslice. It stores the uploadedFilein the form, andnullablemarks the label as optional.Field.RichneedsvaluePath, the field's key in the form.addFilereceives each file uploaded in the editor, here the generatedadd<Field>On<Model>of a[File]field.
When no Field fits the interaction,
Input from akanjs/ui, a button or your own app component is fine, but never a bare <input> for a model field. The full prop list of every member is in Form Controls, linked at the end of this page.Split Components
A Template can export several small components. Split a large form by business step or by UI job, instead of putting everything into
General.The
user module in libs/shared splits phone sign-up into an input and the button that sends the code. Here it is, trimmed:libs/shared/lib/user/User.Template.tsx
- One export per piece. A page places
<User.Template.Phone />and<User.Template.SubmitPhone />wherever its layout needs them. - The pieces share state through the store. Both read
st.use.phone(), so no props pass between them. - The setter still goes by reference. The store's
setPhoneformats the number itself, so the input needs no wrapper. - A call with arguments goes in an arrow.
onPressEnterandonClickhanduserIdandphoneto a store action, called withvoid.
Opening A Template
A Template only draws fields. An edit shell around it fills the form state, opens the form and submits it. Pick the shell by where the form opens:
| Shell | Use it when | What it draws |
|---|---|---|
| Load.Edit | A page already holds the record to edit, or a partial new form. | The form in the page, in a modal, or as bare fields, chosen by type. |
| Model.Edit | A list row, a dropdown or a Unit needs an edit button. | An Edit button, or your trigger, plus the edit modal. |
| Model.New | A screen needs a button that creates a record. | A New button, or your trigger, plus the form modal. |
| Model.NewWrapper | Any element, such as an empty-list call to action, should open a new form. | Only the trigger, so pair it with a Model.EditModal. |
- The shell keeps a draft, so never save form values yourself. It stores the form as the user types and offers it back on the next open.
draft={false}turns this off, anddraft="<scope>"names the scope when neither the id nor the seed identifies it.
Load.Edit in a page
Use
Load.Edit when the page already knows what to edit. The Template inside stays a client component. For a new record, pass a partial model as edit:apps/koyo/page/ticket/new.tsx
To edit an existing record, fetch its edit object with
fetch.edit<Model> and pass that instead:apps/koyo/page/ticket/[ticketId]/edit.tsx
typepicks where the form appears."form"draws it in the page with a submit button, and"empty"draws the fields alone. The default"modal"draws it in a modal.onSubmitandonCanceltake"back","reset"or a path. In a path,[ticketId]becomes the id of the saved record.editmay be an unawaited promise.const { ticketEdit } = fetch.editTicket(ticketId)streams it in; a skeleton, or yourloading, shows until it lands.
Before the Template renders, Load.Edit writes these keys into the store:
| State key | Given an edit object | Given a partial form |
|---|---|---|
| <model> | The full model, built from the edit object. | null |
| <model>Loading | false | Left as it was. |
| <model>Form | An editable copy of the model. | The default values merged with edit. |
| <model>FormLoading | false | false |
| <model>Modal | The modal prop, or "edit". | The modal prop, or "edit". |
| <model>ViewAt | When the server read the record, used to re-read a stale one. | Left as it was. |
Model.Edit for an edit modal
Use
Model.Edit when a list row, a dropdown or a Unit needs an edit button. It draws the button and the modal that holds the Template:apps/koyo/lib/ticket/Ticket.Util.tsx
- A click loads the record. The button calls
st.do.editTicket(ticketId), which fetches it into the form and opens the modal. renderTitle="title"titles the modal with the model name and the form'stitle.triggerreplaces the default Edit button.
Model.NewWrapper to open a new form
Model.NewWrapper turns any element into a button that opens a new form. It draws only the trigger, so a Model.EditModal for the same slice draws the Template:apps/koyo/lib/ticket/Ticket.Zone.tsx
partialseeds the form. A click calls the generatedst.do.newTicket()with it as the starting values.Model.Newis this pair in one component. Reach forModel.NewWrapperwhen the trigger and the modal sit in different places.
Rules At A Glance
Everything above, as a checklist to run before you finish a Template:
"use client"on line 1, always. A Template reads the store, which only the browser has.- Wrap the fields in
Layout.Templateso every form keeps the same spacing. - Take every label from the dictionary. Write
label={l("ticket.title")}anddesc={l("ticket.title.desc")}, never hard-coded text. - Pass generated setters by reference. Clean a value with the Field's
transform, not with an arrow around the setter. - Keep form values in the store, not in
useState. Read the form withst.use.<model>Form(). - Use a plain control when no Field fits.
Input, a button or your own component is fine, but a model field never gets a bare<input>. - Keep business decisions out. They belong in constant, document, service, signal or a store action, and a Template calls no
fetch.*. - Split large forms into named components such as
General,PhoneandSubmitPhone. - Let a shell open the form.
Load.Editfor a page that has the data,Model.Editfor an edit modal,Model.NeworModel.NewWrapperfor a new-form button.
Read next