Forms & Form State
Learning Objectives
- You can collect form data using native HTML controls and FormData.
- You can represent submission state without losing the user’s input.
- You can distinguish local form feedback from a completed server operation.
So far, our browser application has mostly read data. A form introduces a different direction: the user proposes a change, and the application decides what to do with it. Before adding a network request, let’s look at the browser behavior we already have.
Use the practice application (practice-typed-http/) for the proposal preview on its /lab page, and keep the complete example pages available for comparison. The workshop exercise supplies its own editor and browser checks.
Start with a native form
A form groups named controls. On submission, the browser constructs data from the controls and sends it to the form’s action. A submit button can be activated with a pointer or keyboard; pressing Enter in an appropriate input can also submit the form.
This form would send a GET request to /catalog with a query string containing the values for the named controls:
<form action="/catalog" method="GET"> <label for="query">Search the catalogue</label> <input id="query" name="query" type="search" /> <button type="submit">Search</button></form>Switch to the browser view, enter a search term, and submit with Enter. The embedded request preview shows the GET request URL below the form without sending it. On a real page, the browser would navigate to that URL; a catalogue endpoint would supply the results.
The name, not the id, determines the submitted field name. id connects the label to the control. A GET form puts fields into a query string and is appropriate for retrieving a filtered view. It is not an appropriate way to delete or publish a resource. The HTML form reference describes this browser contract.
This part uses JavaScript to connect forms to the Hono API. The last part of the course discusses progressive enhancement: forms that work without JavaScript and gain additional interaction when JavaScript is available.
Read form values with FormData
Create client/src/lib/components/ProposalForm.svelte. The following component previews a conference proposal locally; server persistence comes later.
<script lang="ts"> let message = $state("");
const preview = (event: SubmitEvent & { currentTarget: HTMLFormElement }) => { event.preventDefault(); const data = new FormData(event.currentTarget); const submittedTitle = String(data.get("title") ?? "").trim(); const submittedTrack = String(data.get("track") ?? ""); message = submittedTitle ? `Preview: ${submittedTitle} (${submittedTrack})` : "Enter a title before previewing."; };</script>
<form onsubmit={preview}> <label for="proposal-title">Title</label> <input id="proposal-title" name="title" required maxlength="120" /> <label for="proposal-track">Track</label> <select id="proposal-track" name="track"> <option value="web">Web</option> <option value="data">Data</option> </select> <button type="submit">Preview proposal</button> <p role="status">{message}</p></form>preventDefault() replaces the native submission for this interaction. FormData takes a snapshot of the form’s named controls. Read that snapshot before an asynchronous gap: the user might edit the form while a request is running. These controls keep their own editable values; only the preview message needs reactive state. A FormData snapshot does not turn the input into trusted server data.
Mount the component by replacing client/src/routes/lab/+page.svelte:
<script lang="ts"> import ProposalForm from "#lib/components/ProposalForm.svelte";</script>
<h1>Proposal preview</h1><ProposalForm />Open /lab in the running practice client. Try submitting with the keyboard. Both Enter and a button click trigger the form’s submit event and run the same handler.
How controls contribute FormData values
A text field contributes a string. An unchecked checkbox normally contributes no entry. Multiple controls with the same name require getAll(). A file control can contribute a File. Disabled controls are omitted.
For a checkbox, test presence or use a documented explicit value. For a numeric input, decide how empty input and invalid numbers are represented before conversion. The next validation chapter examines this more carefully.
Which controls are submitted?
0 / 5 points
A form has <input id="title" value="Clay">, an unchecked checkbox named
equipment, and a disabled input named room with value B204. There are no other
controls. What entries does new FormData(form) contain?
Change the preview’s title and track and submit again. The second message comes
from another new FormData(...) call. The object from the first submission would
remain a snapshot of the earlier controls. Now predict the entries when controls
have names and a checkbox is selected.
Predict the changed snapshot
0 / 5 points
A form contains exactly three enabled controls: a text input with
name="title" value="Clay", a checked checkbox with
name="equipment" value="yes", and a text input with
name="room" value="B204". What entries does new FormData(form) contain?
Use the same native-form mechanism in a workshop preview. The supplied editor adds a checkbox and a conditionally required note. Keep this activity local: the status message describes the submitted snapshot, and no API operation is needed.
Preview a workshop proposal
0 / 20 points
Files in the editor
The editor below contains all 2 files for this exercise, although only one opens initially. Use the Files panel to expand the folders and select a filename. If the panel is collapsed, choose Show file tree in the editor toolbar.
- Edit
lib/components/Workshop.svelte. - Keep unchanged
routes/+page.svelte.
The page renders the component with no API connection.
The grader supplies the Svelte 5/SvelteKit application and browser checks.
Editor paths are relative to the local client’s src/ directory; keep the
shorter paths shown here in the editor.
Task
Complete lib/components/Workshop.svelte. The supplied page renders it without an API.
Use a native form with named, labelled controls: Title (title, required), Level
(level, values introductory and advanced, initially introductory), Equipment
needed (equipment, initially unchecked), and Equipment note (note). The note
is required exactly when equipment is selected. Keep the note visible in both states.
Keep an initially empty <p role="status"> in the page. Prevent navigation
in the form submit handler and collect a fresh FormData snapshot, and show the trimmed
values in a status region: TITLE | LEVEL | NOTE when equipment is selected and
TITLE | LEVEL | No equipment otherwise. The submit button is Preview workshop.
Preserve entered values and render user text as text. There is no persistence.
Check both checkbox branches, native required validation, and a second submission
with changed values. The browser check covers all four behaviors for 20 points.
This exercise observes native controls and FormData in the supplied page. It makes no claim to validate an API request; later form exercises use Zod.
Check and submit
Choose Submit solution below the editor to run the grading checks and read the feedback. All editor files, including unchanged files and files you have not opened, are submitted together. There is no separate Run preview. Grading does not require a local application. Keep file paths and exported names as specified; additional observations belong in your own notes.
Each named browser check awards 5 points independently. Read its feedback to identify the behavior to revise.
Show pending feedback
The local preview finishes immediately. A network request takes time, so the next chapter adds a pending flag and a feedback message. Set pending before awaiting the operation and clear it when the operation settles. Keep the editable input separate so a rejected request does not erase the user’s work.
Disabling the submit button while pending is useful feedback and reduces accidental double clicks. It is not a concurrency guarantee: another tab or another HTTP client can still send the same request.
Figure 1 follows a submission from idle through pending to confirmation or failure feedback, with a deliberate user action starting the next attempt.
Check Your Understanding
- How do named controls and a form-level submit handler support native form submission?
- Why should form values be captured before awaiting a network operation?
- What should happen to user input after a rejected submission?