4. The Form
What we want
Section titled “What we want”A form for creating a new note. Submitting the form dispatches a note.create command. Validation is done by the browser. The form has no idea what handles the command.
Add a “new note” button
Section titled “Add a “new note” button”In the sidebar template:
function renderNewNoteButton(): string { return ` <kit-button meta-command="dialog.open" meta-prop-target="new-note-dialog" > New note </kit-button> `}
export function renderNoteList(notes: Note[]): string { return ` <kit-boundary surface="note-list" feature="notes"> ${renderNewNoteButton()} <ul class="note-list">${notes.map(renderNoteItem).join('')}</ul> </kit-boundary> `}The button declares meta-command="dialog.open" with target "new-note-dialog". The built-in dialogModule() (already installed in chapter 1) handles it.
Add the dialog with the form
Section titled “Add the dialog with the form”At the bottom of the layout, just inside <kit-shell>:
<kit-dialog id="new-note-dialog" close-event="new-note.dismissed"> <form id="new-note-form"> <h2>New note</h2>
<kit-field label="Title" required> <input name="title" autocomplete="off" /> </kit-field>
<kit-field label="Body"> <textarea name="body" rows="6"></textarea> </kit-field>
<menu> <kit-button type="button" meta-command="dialog.close" meta-prop-target="new-note-dialog" > Cancel </kit-button> <kit-button type="submit">Save note</kit-button> </menu> </form></kit-dialog>Three things to notice:
<kit-field label="Title" required>provides the visible label, asterisk, andrequiredpropagation. The<input>inside is real and natively validated.- The Cancel button has
meta-command="dialog.close"— declarative close, no JS handler needed. - The Save button is a native
submitbutton. The form’ssubmitevent handles dispatch.
Hook the form submit to a command
Section titled “Hook the form submit to a command”const form = document.getElementById('new-note-form') as HTMLFormElementform.addEventListener('submit', async (e) => { e.preventDefault() const data = Object.fromEntries(new FormData(form).entries()) as Record<string, string>
const result = await shell.runtime.command({ type: 'note.create', payload: { title: data.title.trim(), body: data.body.trim(), }, })
if (result.ok) { form.reset() await shell.runtime.command({ type: 'dialog.close', payload: { target: 'new-note-dialog' }, }) }})The form does three things:
- Prevents default submission.
- Reads the form data with
FormData— a built-in. - Dispatches a
note.createcommand and awaits the result.
If anything fails, the form stays open. If it succeeds, we reset the form and close the dialog via another command — even closing the dialog is dispatched, not directly called.
A handler — temporarily
Section titled “A handler — temporarily”Right now no module handles note.create. The console will show:
[kit:diagnostic] command.unhandled { commandType: 'note.create' }Add a temporary in-memory handler in main.ts:
let notes = [...seedNotes]
shell.runtime.handleCommand('note.create', (cmd) => { const note = { id: `n_${crypto.randomUUID().slice(0, 8)}`, title: String(cmd.payload?.title ?? ''), body: String(cmd.payload?.body ?? ''), updatedAt: Date.now(), } notes = [note, ...notes] document.getElementById('main')!.innerHTML = renderApp(notes) return { id: note.id }})Now: open the dialog, fill the form, click Save. The dialog closes, the new note appears at the top of the list. Refresh — the note is gone (we haven’t built persistence yet).
What we have now
Section titled “What we have now”A working create flow that:
- Uses native form validation (try submitting an empty title — the browser stops you).
- Dispatches a command instead of calling a save function directly.
- Closes the dialog via a command, not a direct call.
The form, the dialog, and the buttons don’t import a save function. They speak through commands.
A note on the temporary handler
Section titled “A note on the temporary handler”We hand-rolled note.create in main.ts for now. In the next chapter we’ll move it into a real module — and the module will also persist notes to localStorage, so refreshing keeps them.
What’s next
Section titled “What’s next”The persistence module. The first non-trivial module you’ll author from scratch. It exposes a provider, handles three commands, and emits two events for downstream observers.