Skip to content
academia.sh

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&note=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&note=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; with post it is written to the body and the url’s query is kept.
  • The default encoding is the same in both methods: name=value pairs, 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.
  • fieldset and legend are 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.

Start typing to search.

↑↓ Esc navigate · open · close