Lesson 20 / 25
Form Structure
The entry list a submission produces, the effect the submission method has on the url and the body, the reasoning behind choosing a method, and grouping fields.
Contents
The document is now structured, sectioned, and carries media — but it is still one-way. The reader receives information, but cannot give any back. This topic opens that direction.
A form is the mechanism that turns values taken from the user into a request. The HTTP Request and Response lesson in the How the Internet Works course defined the parts of a request — the request line, the headers, the body. This lesson looks at how the values that feed those parts are collected in the document, and in what shape they are written.
The Parts of a Form
The form element is a unit of submission. Two attributes determine where and how the
submission happens.
action is the url the request goes to. If it is not written, the document’s own url is
used.
method is the HTTP method: get or post. If it is not written, get is assumed.
Of the fields inside a form, only the ones with a name are submitted. The name
attribute is what enters a field into the submission; a field with no name still appears on
screen and can have a value typed into it, but it never reaches the server.
The Entry List
When submission starts, the browser walks the fields inside the form and builds an entry list of name-value pairs. This list is ordered and follows the fields’ order in the document; the same name can appear more than once.
Some fields never enter the list: ones with no name, ones that are disabled, and unchecked checkboxes. This last rule is easy to miss — an unchecked checkbox is not submitted with a “false” value, it is not submitted at all.
The Method Determines the Body
Once the entry list is built, where it gets written depends on the method. The script below opens a local server, makes both submissions, and prints what the server sees.
// submission.mjs — how get and post submissions look on the server import { createServer } from "node:http"; // The name-value pairs the form produces (the entry list). const entryList = [ ["station", "north-slope"], ["measurement", "temperature"], ["value", "-4.2"], ["note", "windy & clear — no rain"], ]; const server = createServer((request, response) => { const chunks = []; request.on("data", (chunk) => chunks.push(chunk)); request.on("end", () => { const body = Buffer.concat(chunks); console.log("request line :", request.method, request.url); console.log("content-type :", request.headers["content-type"] ?? "(none)"); console.log("body :", body.length ? JSON.stringify(body.toString()) : "(empty)"); console.log("---"); response.writeHead(204).end(); }); }); server.listen(0, "127.0.0.1", async () => { const base = "http://127.0.0.1:" + server.address().port; const query = new URLSearchParams(entryList).toString(); // method="get": the action url's query part is erased, replaced with the entry list. const url = new URL("/record?old=value", base); url.search = query; await fetch(url); // method="post", default enctype: the entry list is written to the body. await fetch(new URL("/record?old=value", base), { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: query, }); server.close(); });
request line : GET /record?station=north-slope&measurement=temperature&value=-4.2¬e=windy+%26+clear+%E2%80%94+no+rain content-type : (none) body : (empty) --- request line : POST /record?old=value content-type : application/x-www-form-urlencoded body : "station=north-slope&measurement=temperature&value=-4.2¬e=windy+%26+clear+%E2%80%94+no+rain" ---
The output says four things.
The get method sends no body. The list is written into the url’s query part. As a
result, the values show up in the browser’s history, in bookmarks, and in server logs.
The action url’s existing query is erased. The old=value pair from the request line is
gone. Writing a query onto an action url does nothing on a form submitted with get; it has
to be carried as a hidden field instead.
The post method writes the list to the body and keeps the url’s query intact.
The encoding is the same either way. In both cases the list becomes name=value pairs
joined with &; spaces become +, non-ASCII characters and & get percent-encoded. The
separator inside windy & clear — no rain became %26, and the em dash became %E2%80%94 —
the same percent-encoding as in the Links lesson. This format is called
application/x-www-form-urlencoded, announced in the Content-Type header on a post
submission.
Choosing a Method
The choice is not a habit, it follows from the nature of the request. The two properties defined in the HTTP Request and Response lesson apply directly here: a safe method makes no change on the server, an idempotent method leaves the same state no matter how many times it runs.
get is safe. It is used only for submissions that make no change on the server: a search, a
filter, a query. That the result can be shared as a url and bookmarked is a gain.
post is not safe. Every submission that changes something on the server uses this method:
adding a record, sending a notification. The values do not show up in the url, and the browser
warns before repeating the submission.
Submitting a record with get means that sharing the link, or reloading the url, redoes the
operation. This distinction is not a privacy measure — a post body is unencrypted too;
protection comes from the transport layer described in the Role of HTTPS lesson.
Grouping Fields
The fieldset element groups fields that belong together, and the legend element gives the
group a name, written as the group’s first child.
<fieldset> <legend>Measurement source</legend> <p><label><input type="radio" name="source" value="automatic" checked> Automatic</label></p> <p><label><input type="radio" name="source" value="manual"> Entered manually</label></p> </fieldset>
Grouping is not optional for radio buttons. The individual labels say only “Automatic” and
“Entered manually” — it is the legend text that says what the group is a choice of. A screen
reader announces the group name along with each option.
The disabled attribute written on fieldset disables every field inside it, and disabled
fields do not enter the entry list; in a multi-step form, this is how a whole section is shut
off at once.
The Submit Button
The button element’s type attribute takes three values: submit submits the form, reset
returns the fields to their initial values, button does nothing. A type attribute left
unwritten inside a form counts as submit — an ordinary button placed inside a form submits it
without anyone intending that.
The formaction and formmethod attributes override the form’s own settings for the
submission made with that button; this is how the same form is submitted to two different urls.
Fields Outside the Form
Which form a field belongs to usually follows from nesting: every field sitting inside a
form element belongs to it. Forms cannot be nested, so this relation is single-valued.
Nesting does not always give the wanted layout — for example, fields inside a table that
belongs to a form outside the table. In this case, the form attribute written on the field
announces the id of the form it belongs to.
<form id="correction" action="/measurement/correction" method="post"></form> <table> <tr> <th scope="row">Monday</th> <td><input form="correction" name="mon" type="number" value="-4"></td> </tr> </table> <button type="submit" form="correction">Submit correction</button>
A submit button can use this attribute too; the form itself stays empty and carries only the submission settings.
The autocomplete attribute controls whether the browser suggests values entered before. When
written at the field level, it also announces the kind of value expected; this announcement is
the one mechanism that makes a form fillable for users with a cognitive or motor difficulty. It
is turned off only for fields that should not be stored on a shared machine.
In the Station Document
<section aria-labelledby="notice"> <h2 id="notice">Report a Measurement Correction</h2> <form action="/measurement/correction" method="post"> <fieldset> <legend>Measurement information</legend> <p><label for="station">Station code</label> <input id="station" name="station" type="text" value="north-slope"></p> <p><label for="value">Measured value (°C)</label> <input id="value" name="value" type="number"></p> </fieldset> <p><button type="submit">Submit correction</button></p> </form> </section>
post is chosen as the method because the submission creates a record on the server. The
fields are gathered into a group, and the group is given a name.
Summary
- A form builds an entry list from its fields; only fields that have a name, are not disabled, and are checked enter this list.
- With
get, the list is written into the url’s query part and the action url’s existing query is erased; withpostit is written to the body and the url’s query is kept. - The default encoding is the same in both methods:
name=valuepairs, space as+, special characters percent-encoded. - The choice of method follows from the safe and idempotent properties; a submission that
changes something on the server is made with
post. fieldsetandlegendare mandatory for option groups: only the group name says what the group is a choice of.
Next Step
This lesson built a form with a single kind of field. But the data to be collected is not always free text: a number, a date, a time, a choice, a multiple choice, a file. The next lesson looks at input types, and shows how much of the type information survives submission — how all of them reach the server — through a real body.
To keep your progress and take notes, Log in
My notes
Log in to take notes.