A reading plan has several kinds of information: a book title, a weekly page target and a preferred reading format. A form must express those meanings before a program can process the values. It must also distinguish entering data, accepting it, displaying a result and retaining it for a later visit. These are separate steps, with different evidence of success.
This unit develops a small reading-plan example. It connects the DOM, event and object-state model to form controls, validation, promises and the response-handling pattern used by Fetch.
Controls describe values; labels describe their meaning
Definition
Form control and submitted entry
A form control is an interface through which the user enters or chooses a value. A submitted form entry normally pairs a control's name with its current value. An id identifies an element in the document; it is not a substitute for the submitted name.
Choose the control from the meaning of its value:
| Control | Appropriate use | Important distinction |
|---|---|---|
input type="text" | A short book title | Its value is text, even if it contains digits. |
textarea | A question spanning several lines | Read its current value, rather than its initial child text. |
input type="number" | A numeric target | Constraints such as min, max and step describe allowed values. |
input type="email" or url | A value with a defined syntax | A syntax check does not establish that an address exists. |
input type="checkbox" | An independently chosen option | Several checkboxes can be selected. |
input type="radio" | One choice from a set | Controls in this form share a name to form a group. |
select and option | One choice from a displayed menu | The selected option supplies its value. |
Other types, including date, color, search and tel, communicate additional input intentions and may change the browser's interface. For example, tel can influence a mobile keyboard without imposing a universal telephone-number format. A password control masks its display; that appearance does not determine how the form is transmitted.
Associate a label's for with the control's unique id. This gives the text a defined relationship to the control and allows a reader to select the control through its label. A fieldset groups related controls; its legend names the group. Each radio button still needs its own label and a distinct ID.
Worked example
A reading-plan form
Place this markup in the body of a standalone HTML document. The scripts developed below go after the form.
<form id="reading-plan">
<p>
<label for="book">Book title</label>
<input id="book" name="book" type="text" maxlength="80" required>
</p>
<p>
<label for="pages">Pages this week</label>
<input id="pages" name="pages" type="number"
min="1" max="300" step="1" required>
</p>
<fieldset>
<legend>Preferred format</legend>
<input id="paper" name="format" type="radio" value="paper" checked>
<label for="paper">Paper</label>
<input id="digital" name="format" type="radio" value="digital">
<label for="digital">Digital</label>
</fieldset>
<p>
<label for="topic">Main topic</label>
<select id="topic" name="topic">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="javascript">JavaScript</option>
</select>
</p>
<p>
<input id="discussion" name="discussion" type="checkbox" value="yes">
<label for="discussion">Include a discussion session</label>
</p>
<button type="submit">Record plan</button>
<button type="reset">Restore initial values</button>
</form>
<p id="plan-status" role="status"></p>
The IDs support labels and DOM selection. The radio controls share name="format", but their values distinguish the choices. The checked attribute establishes the initial paper selection. Reset restores initial values, including that choice; it does not necessarily make every control empty.
Button types also matter. A submit button starts the form's submission behavior; a type="button" button needs its own event logic. A reset button restores defaults. Specify the type so that adding an auxiliary button does not accidentally start a submission.
From controls to name–value data
new FormData(form) constructs entries from the form's successful controls at that moment. An enabled control generally needs a name. Unchecked checkboxes and radio buttons contribute no entry. Disabled controls are omitted. A named read-only text control can still contribute an entry: read-only and disabled describe different states.
If the reader enters Web foundations, chooses 24 pages, leaves paper selected, chooses HTML and leaves the checkbox unchecked, the entries are:
book = Web foundations
pages = 24
format = paper
topic = html
The pages form entry is the string "24". Conversion is a program decision. A number input additionally provides valueAsNumber; an empty or unconvertible value produces NaN. FormData.get("format") gives the selected radio value. For a repeated name, getAll(name) retrieves all its entries. data.has("discussion") expresses whether the checkbox contributed an entry.
Checkpoint
Which name identifies the chosen format?
A form has radio controls with IDs paper and digital, both named format. The reader selects digital. Which entry is submitted? What happens if both controls instead use id="format" but have no name?
Solution · Separate document identity from entry names
The selected control contributes format=digital. The ID is used within the document, while the name becomes the data key. Giving both controls the same ID makes the document identity ambiguous, does not establish the intended radio group, and does not supply the missing submission name. Use distinct IDs and the shared name.
Native submission and a scripted submission
With ordinary form submission, action identifies the destination and method selects how entries are sent. A missing method normally selects GET; a missing action normally uses the current document URL.
| Method | Where ordinary form data goes | Typical meaning |
|---|---|---|
| GET | An encoded query in the destination URL | Retrieve results for supplied search criteria. |
| POST | The request body | Submit data for an operation handled by the server. |
An ordinary POST form normally uses application/x-www-form-urlencoded; file-upload forms use a different encoding. Selecting POST does not automatically produce JSON. A script constructing a JSON request must serialize the object and choose a matching content type. The method also does not, by itself, create a file or define an application's storage policy.
A handler for the form's submit event can call event.preventDefault() and perform its own processing. Handle the form event, rather than only a particular button's click, so that keyboard submission follows the same logic. The browser's normal constraint-validation step runs before a user-initiated submit event is dispatched; an invalid field can prevent that event from reaching the handler.
Validation checks a stated rule
HTML constraints cover common rules: required rejects an empty required value, min and max bound a numeric value, and step="1" restricts the number input to whole steps. They do not express every application rule. For instance, a required text field containing only spaces is not empty in the same sense as a book title with meaningful characters.
The validity API provides distinct operations:
checkValidity()returns whether the applicable constraints are satisfied.reportValidity()also requests the browser's validation feedback.setCustomValidity(message)sets an application-specific error. An empty string clears that custom error.
A custom error must be cleared when the relevant input changes, otherwise a corrected control can remain invalid. Present a useful explanation with the field; :valid, :invalid, :required, :optional, :enabled, :disabled, :read-only and :focus can support the visual treatment. An attribute selector such as input[type="text"] targets one control type. Styling identifies a state but does not define the validation rule. Client checks give immediate feedback; a real receiving service must independently validate the values it accepts.
Asynchronous work has an order
An event handler runs JavaScript instructions in order. Waiting for input/output is different: an operation can begin, return control, and provide its result later. On the browser's main JavaScript thread, one callback runs at a time. The browser can manage network activity while JavaScript continues other work.
A callback is a function supplied to other code for that code to invoke. The surrounding operation determines when it runs: callbacks are not automatically asynchronous. Nested callbacks can express dependent work, but several layers of nesting make the sequence and its error paths harder to follow.
Definition
Promise and asynchronous continuation
A promise represents an eventual result. It starts pending and can settle as fulfilled with a value or rejected with a reason. .then() attaches a continuation and returns a new promise; a thrown error rejects that next promise. .catch() handles rejection in the chain.
Returning the next asynchronous operation from a .then() handler makes the chain wait for it. Without that return, an inner operation can continue separately while the outer chain advances. async functions also return promises. Inside such a function, await pauses that function's continuation until the awaited promise settles; it does not freeze all browser execution. A rejection at an awaited step behaves like a thrown error and can be handled with try/catch.
Worked example
Trace synchronous instructions, a microtask and a timer
Treat this script as the current task. Record its synchronous output before considering the scheduled continuations.
console.log("A");
setTimeout(() => console.log("D"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("B");
For this script, the output is A, B, C, D. Synchronous instructions finish first. The fulfilled promise's continuation runs as a microtask before the later timer task. A zero-delay timer schedules later work; it does not interrupt the current script or guarantee an exact execution time. Repeating work can be scheduled with setInterval; retain its returned ID and pass it to clearInterval when repetition should stop.
Checkpoint
Does an async declaration make every line wait?
An async function calls request(...) and immediately reads a variable that its .then() handler will assign. Why can it still observe the old value? Give a direct way to express the dependency.
Solution · Await the operation whose result is needed
Declaring the outer function async does not automatically wait for each promise created inside it. Execution proceeds past an unawaited call. Obtain the response with const response = await request(...), then await its body conversion before using the resulting data. Alternatively return a promise chain that performs those dependent steps in order.
Fetch separates obtaining a response from reading its body
AJAX names the pattern of retrieving or submitting data asynchronously and updating a page without a full navigation. Its historical name mentions XML, but the pattern also applies to JSON and text. XMLHttpRequest provides an older request interface; Fetch expresses the response flow using promises.
Browser fetch() returns a promise for a Response. Its fulfillment means that a response is available, not that every application operation succeeded. A response with status 404 or 500 normally fulfills that promise; inspect response.ok or response.status. ok is true for status codes from 200 through 299.
The body is another step: await response.json() reads and parses JSON, whereas await response.text() obtains text. A JSON body parse can fail even after an HTTP 200 response. Body-reading methods consume the body, so select the needed representation rather than trying to read the same response repeatedly.
| Failure | Where it appears | Appropriate reasoning |
|---|---|---|
| Network or browser access failure | The Fetch promise rejects. | No usable response was obtained by this call. |
| HTTP error status | A response is obtained with ok === false. | Inspect the status and the service's documented error representation. |
| Invalid JSON | response.json() rejects. | Receiving bytes and parsing the expected representation are different steps. |
| Application rule rejected | The service's contract reports rejection. | Preserve the user's input and explain the applicable rule. |
An origin consists of scheme, host and port. Cross-origin response access is governed by the browser's CORS rules and the receiving service's response headers; changing a request method does not grant that access. For actual local HTTP examples, open the page through its development server. A file: document and an HTTP document have different loading environments.
A response model entirely in memory
Worked example
Follow requests through one store instance
The following function supplies a small request interface using real Response objects. Its state lives in the returned function's closure. The resource string memory:reading-plan selects that model; the function makes no network request.
function createReadingStore() {
let plan = null;
const reply = (status, data) => new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json" }
});
return async function mockFetch(resource, options = {}) {
await Promise.resolve();
if (resource !== "memory:reading-plan") {
return reply(404, { error: "Unknown resource" });
}
const method = options.method ?? "GET";
if (method === "GET") return reply(200, { plan });
if (method !== "PUT") return reply(405, { error: "Unsupported method" });
let incoming;
try {
incoming = JSON.parse(options.body);
} catch {
return reply(400, { error: "Expected JSON" });
}
if (incoming === null || typeof incoming !== "object" ||
typeof incoming.book !== "string" ||
incoming.book.trim().length === 0 || incoming.book.length > 80 ||
!Number.isInteger(incoming.pages) ||
incoming.pages < 1 || incoming.pages > 300 ||
!["paper", "digital"].includes(incoming.format)) {
return reply(422, { error: "Invalid reading plan" });
}
plan = {
book: incoming.book.trim(),
pages: incoming.pages,
format: incoming.format
};
return reply(200, { plan, storage: "memory" });
};
}
const request = createReadingStore();
The model supports GET to read its current value and PUT to replace that value. Its custom service contract validates three fields; the survey's topic and discussion options illustrate control types but are not stored by this model. The temporary incoming object is validated before plan changes. Invalid input therefore leaves the previous plan intact.
The returned value is called mockFetch to make its role explicit. It is a small teaching model of request/response processing, not a complete implementation of the browser Fetch API.
Start with a fresh instance, await each operation, and compare the response with the stored state. Run this after the model definition; it uses a separate sample instance.
async function traceStore() {
const sample = createReadingStore();
const initial = await sample("memory:reading-plan");
console.log(initial.status, (await initial.json()).plan);
const write = await sample("memory:reading-plan", {
method: "PUT",
body: JSON.stringify({ book: "Web sketches", pages: 12, format: "digital" })
});
console.log(write.status, (await write.json()).plan.book);
const read = await sample("memory:reading-plan");
console.log(read.status, (await read.json()).plan.pages);
const invalid = await sample("memory:reading-plan", {
method: "PUT",
body: JSON.stringify({ book: "Web sketches", pages: 0, format: "digital" })
});
console.log(invalid.status, (await invalid.json()).error);
const afterFailure = await sample("memory:reading-plan");
console.log(afterFailure.status, (await afterFailure.json()).plan.pages);
const fresh = await createReadingStore()("memory:reading-plan");
console.log(fresh.status, (await fresh.json()).plan);
}
traceStore().catch(error => console.log(error.message));
| Operation | Status | Observation |
|---|---|---|
| Initial GET | 200 | null |
| Valid PUT | 200 | Web sketches |
| GET after success | 200 | 12 pages |
| PUT with zero pages | 422 | Invalid reading plan |
| GET after failure | 200 | Still 12 pages |
| GET on a fresh store | 200 | null |
Validation runs before replacement, so the rejected write cannot erase the accepted plan. The final read uses a different closure; its empty result distinguishes per-instance memory from shared or durable storage.
Connect the validated form to the model
Put the next script after the previous one and after the form. The custom book-title rule rejects whitespace-only text. The submit button is disabled while this handler is awaiting a result, and the finally block restores it after either outcome.
const form = document.querySelector("#reading-plan");
const book = document.querySelector("#book");
const pages = document.querySelector("#pages");
const status = document.querySelector("#plan-status");
const submit = form.querySelector('button[type="submit"]');
book.addEventListener("input", () => book.setCustomValidity(""));
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (book.value.trim() === "") {
book.setCustomValidity("Enter a book title with visible characters.");
book.reportValidity();
return;
}
const data = new FormData(form);
const plan = {
book: book.value.trim(),
pages: pages.valueAsNumber,
format: data.get("format")
};
submit.disabled = true;
status.textContent = "Recording...";
try {
const response = await request("memory:reading-plan", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(plan)
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const receipt = await response.json();
status.textContent = `Recorded in memory: ${receipt.plan.book}`;
form.reset();
} catch (error) {
status.textContent = error.message;
} finally {
submit.disabled = false;
}
});
The handler clears the form only after both the response check and JSON reading succeed. An error leaves the entered values available for correction. The visible result uses textContent, so the book title is displayed as text.
To inspect the current in-memory value, run this additional function after the scripts above:
async function inspectPlan() {
const response = await request("memory:reading-plan");
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();
console.log(data.plan);
}
inspectPlan().catch(error => console.log(error.message));
Awaiting a write before starting a dependent read expresses a causal order: validate, update the model, receive its acknowledgement, then read. Starting the read before the write has completed does not express that dependency. Disabling the submit button limits ordinary repeated clicks; it is not a general ordering rule for every possible caller or multiple browser tabs.
Common mistake
An acknowledgement must be interpreted using the storage contract
This model returns success after updating a closure variable. Calling GET on the same instance returns that value. Reloading the page or creating another store initializes plan to null again. Durable storage requires a service with a defined persistence operation and completion guarantee; a successful response alone does not specify that guarantee.
Exercises
Checkpoint
Exercise 1: predict the submitted entries
The book control is named book and marked readonly. The pages control is named pages and disabled. Paper is selected, and discussion is unchecked. Which of those controls contributes an entry to new FormData(form)? Does readonly make the book a trustworthy value to a receiving service?
Solution · Exercise 1 solution
The named read-only book control contributes its value. The disabled pages control contributes none. The checked paper radio contributes format=paper; the unchecked discussion checkbox contributes none. Read-only affects this interface, not the receiving service's validation duty: a request can be constructed independently of the form.
Checkpoint
Exercise 2: follow the validation state
The book field contains three spaces. Explain why required alone is insufficient, how the handler marks the error, and why its input listener clears the custom message.
Solution · Exercise 2 solution
The raw value has characters, so the required constraint alone need not reject it. The application requires visible title content and checks the trimmed value. setCustomValidity records that rule's error and reportValidity requests feedback. Clearing the custom error on the next input lets the corrected value be evaluated instead of retaining an obsolete error.
Checkpoint
Exercise 3: identify the failed step
Case A obtains an HTTP 500 response with valid JSON. Case B obtains HTTP 200 but its body is the text not JSON. Case C fails before a usable response is available. Which promise or check detects each case?
Solution · Exercise 3 solution
A is detected by the response-status check; the Fetch promise can still be fulfilled. B is detected when the JSON-reading promise rejects. C rejects the Fetch operation itself. A shared catch may present the final error, but these are different stages of the computation.
Checkpoint
Exercise 4: distinguish a stored value from persistence
One store accepts a valid 24-page plan. A GET using that instance returns it. A new createReadingStore() instance returns null. Explain both results and what additional fact would be needed before a user interface could promise that the plan survives a reload.
Solution · Exercise 4 solution
The first instance retains the plan in its own closure. A new instance has a separate closure initialized to null. To promise survival across reloads, the application needs a persistence mechanism and evidence that its storage operation completed with that meaning. The model's storage: "memory" acknowledgement deliberately describes a shorter lifetime.