Respond to user input with DOM events
Outcome
Section titled “Outcome”You will add a form to the project-status page and use DOM events to validate its input and show a result without reloading the page.
Why this matters
Section titled “Why this matters”Interactive web pages respond to actions such as submitting a form, activating a button, changing a field, or selecting an option. The browser reports each action as an event. JavaScript can listen for that event, read the current page data, and update the document.
What you will practice
- Explain the relationship between the HTML source and the Document Object Model (DOM).
- Select required page elements with document.querySelector and check that each result exists.
- Register named callback functions with addEventListener.
- Use the submit event and preventDefault to handle a form without a page reload.
- Read and validate an input value, then update text with textContent.
- Verify submit and reset behavior with a mouse and a keyboard.
What is new and what is reused
Section titled “What is new and what is reused”- New: The DOM,
document, CSS selector strings, element objects,querySelector, events, event listeners, callback functions, the event object,preventDefault, input values,textContent, focus management,aria-invalid, and status live regions. - Reused: The project-status calculation, functions, conditions, template literals, strict mode, semantic HTML, form controls, browser developer tools, and Console error messages.
Starting point
Before you start
- The completed web-interaction-lessons folder from the JavaScript foundations lesson.
- index.html loads app.js with defer.
- app.js calculates the required 60% status without a Console error.
- VS Code and a current version of Edge, Chrome, or Firefox.
- Current state
- The project status is written only to the Console. The page cannot receive a project name or show a calculated result in the document.
- First action
- Open web-interaction-lessons in VS Code and replace the main element in index.html with the status form from the next section.
- First checkpoint
- The browser shows one labeled text field, a Show status button, a Reset button, and the initial status text in that order.
- Help trigger
- Use the recovery note or ask for help if a label does not focus its input, submitting reloads the page, the Console names null in a TypeError, or a keyboard cannot reach and operate both buttons.
Required result
Section titled “Required result”You have completed the lesson when:
index.htmlcontains a form with a connected label, text input, submit button, reset button, and status output;- the output uses
role="status"so updates can be announced without moving focus; app.jsselects the form, input, and output withdocument.querySelector;- the script stops with a specific error if a required element is missing;
- a named submit handler prevents the form’s default page submission;
- submitting an empty or whitespace-only name shows Enter a project name., applies
aria-invalid="true", and moves focus to the input; - submitting Community event page shows Community event page: 60% complete. Continue with required tasks. and removes
aria-invalid; - resetting the form clears the input and restores the initial status text;
- the submit and reset behavior works with keyboard controls; and
- the final version runs without a JavaScript error in the Console.
Add an interface for the status program
Section titled “Add an interface for the status program”Replace the existing main element in index.html with this markup. Keep the document structure and deferred script element from the previous lesson.
<main> <h1>Project status</h1>
<form id="status-form"> <div> <label for="project-name">Project name</label> <input id="project-name" name="projectName" type="text" aria-describedby="project-name-help" > <p id="project-name-help">Enter the name used in the status message.</p> </div>
<div> <button type="submit">Show status</button> <button type="reset">Reset</button> </div> </form>
<p id="status-message" role="status"> Enter a project name and submit the form. </p></main>Each part has a specific role:
formgroups the input and actions that produce one status result.for="project-name"connects the visible label to the input withid="project-name".name="projectName"gives the control a name for normal form data.aria-describedbyconnects the input to its visible help text.type="submit"gives the first button the form’s submit behavior.type="reset"gives the second button the form’s reset behavior.role="status"makes text updates available as a polite live status in supporting assistive technologies.
Do not replace the buttons with styled div or span elements. Native buttons provide keyboard activation, focus behavior, and button semantics without recreating those features in JavaScript.
Check the static HTML first
Section titled “Check the static HTML first”- Save
index.htmland reload the page. - Select the Project name label and confirm that the text field receives focus.
- Press Tab from the text field and confirm that focus moves to Show status, then Reset.
- Select Reset and confirm that the text field clears. The status text does not reset yet because the JavaScript listener does not exist.
If these checks fail, correct the HTML before you change app.js. JavaScript cannot repair a missing form structure without adding avoidable complexity.
Connect HTML to JavaScript through the DOM
Section titled “Connect HTML to JavaScript through the DOM”When the browser parses HTML, it creates a Document Object Model, or DOM. The DOM represents the document as objects that JavaScript can inspect and change. The HTML file remains the source. The DOM is the browser’s current in-memory representation of that source and any later changes.
The browser supplies the document object as the entry point to the current DOM. document.querySelector(...) searches the document with a CSS selector string and returns the first matching element. It returns null when no element matches.
In app.js, keep the task data, calculateCompletion function, percentage calculation, and conditional status logic from the previous lesson. Remove the two final console.log calls. Add these selections after the status logic:
const statusForm = document.querySelector("#status-form");const projectNameInput = document.querySelector("#project-name");const statusOutput = document.querySelector("#status-message");Each selector starts with # because it selects an element by its id value. The identifiers on the left describe how JavaScript will use the selected elements.
Stop when a required element is missing
Section titled “Stop when a required element is missing”Add this guard after the three selections:
if (!statusForm || !projectNameInput || !statusOutput) { throw new Error("Required status form elements are missing.");}The logical NOT operator ! converts each selection result to a boolean and reverses it. An existing element becomes false after !. null becomes true.
The logical OR operator || makes the complete condition true when any required selection is missing. throw new Error(...) stops the script and places the specific message in the Console.
This guard turns a later error such as Cannot read properties of null into a message that identifies the missing interface contract.
Test the guard deliberately
Section titled “Test the guard deliberately”Change "#status-form" to "#status-form-test", save, and reload. The Console must show Required status form elements are missing. Restore "#status-form", save, reload, and confirm that the error disappears.
Checkpoint: The script finds the required elements
- What now works
- All three querySelector calls return elements, and the deliberate selector error produces the specific missing-elements message.
- Files changed
index.html, app.js- What remains
- Listen for form submission, read the input, and update the status output.
- Next action
- Keep all three selectors restored and add the named submit handler after the missing-elements guard.
- If it does not work
- Compare each selector string with the matching HTML id. Include the # in the selector, but not in the HTML id value.
Listen for the submit event
Section titled “Listen for the submit event”An event is an object that describes something that happened. An event listener connects one event type on one event target to a function.
Add this named function after the missing-elements guard:
function handleStatusSubmit(event) { event.preventDefault(); console.log("Status form submitted.");}Then register the function as the form’s submit listener:
statusForm.addEventListener("submit", handleStatusSubmit);addEventListener receives two arguments:
"submit"names the event type.handleStatusSubmitsupplies the callback function that the browser must call after that event occurs.
Pass the function name without parentheses. handleStatusSubmit() would call the function immediately while the script loads and pass its returned value instead of the callback.
When the event occurs, the browser calls the function and supplies the event object as the event argument. event.preventDefault() prevents the form’s default submission, so this local page does not reload or navigate.
Test mouse and keyboard submission
Section titled “Test mouse and keyboard submission”- Save
app.jsand reload the page with the Console open. - Select Show status. The Console shows Status form submitted. and the page does not reload.
- Focus the Project name input and press Enter. The same submit event runs.
- Confirm that the Console adds one message for each submission.
Listening to the form’s submit event covers both a submit-button activation and Enter-based form submission. A click listener on the button would describe only one route to the form action.
Read, validate, and show the input
Section titled “Read, validate, and show the input”Replace the temporary handleStatusSubmit function with this complete version:
function handleStatusSubmit(event) { event.preventDefault();
const projectName = projectNameInput.value.trim();
if (projectName === "") { projectNameInput.setAttribute("aria-invalid", "true"); statusOutput.textContent = "Enter a project name."; projectNameInput.focus(); return; }
projectNameInput.removeAttribute("aria-invalid"); statusOutput.textContent = `${projectName}: ${statusMessage}`;}The handler follows one input-to-output path:
projectNameInput.valuereads the text field’s current string value..trim()removes whitespace from the start and end of the string.projectName === ""detects an empty or whitespace-only result.setAttributeexposes the invalid state to the DOM and supporting assistive technologies.textContentreplaces the output element’s text.focus()returns keyboard focus to the field that needs correction.returnends the handler before the success code runs.- The success path removes the invalid state and combines the project name with the calculated status.
Use textContent for this user-provided string. It inserts the value as text instead of parsing it as HTML. The input <strong>Test</strong>, for example, appears as those literal characters instead of creating an element.
Test both submit paths
Section titled “Test both submit paths”| Test | Expected output | Expected input state |
|---|---|---|
| Submit an empty field | Enter a project name. |
Focus returns to the input and aria-invalid="true" is present |
| Submit spaces only | Enter a project name. |
Same invalid result after trim() |
Submit Community event page |
Community event page: 60% complete. Continue with required tasks. |
aria-invalid is absent |
Submit <strong>Test</strong> |
The literal tag text followed by the status | No strong element is created |
Use the browser Elements or Inspector panel to confirm the aria-invalid attribute. Restore Community event page as the final successful test value.
Restore the interface on reset
Section titled “Restore the interface on reset”The browser’s default reset behavior clears the input because the HTML does not define an initial value. Add a reset callback that also restores the JavaScript-managed output and invalid state:
function handleStatusReset() { projectNameInput.removeAttribute("aria-invalid"); statusOutput.textContent = "Enter a project name and submit the form.";}Register the reset listener after the submit listener:
statusForm.addEventListener("submit", handleStatusSubmit);statusForm.addEventListener("reset", handleStatusReset);The reset handler does not call preventDefault. The form’s native reset action must still clear the input. The listener adds the two updates that the browser cannot infer from the form controls.
Test this sequence:
- Submit an empty value and confirm the invalid state.
- Enter Community event page and submit it.
- Activate Reset with the keyboard.
- Confirm that the input clears,
aria-invalidis absent, and the initial status text returns.
Checkpoint: The form responds to submit and reset events
- What now works
- Mouse and keyboard submission select the correct validation path without a reload, and reset restores the input, invalid state, and initial output.
- Files changed
index.html, app.js- What remains
- Review the complete event flow and run the final self-check.
- Next action
- Compare app.js with the complete program, then run every self-check without editing the event code between tests.
- If it does not work
- Fix the first Console error, confirm that each listener is registered after its function exists, and test one event at a time.
Review the complete JavaScript
Section titled “Review the complete JavaScript”Your final app.js must contain this program:
"use strict";
// Stable project dataconst completedTasks = 3;const totalTasks = 5;const hasBlockingIssue = false;
function calculateCompletion(completed, total) { if (total === 0) { return 0; }
return Math.round((completed / total) * 100);}
const completionPercentage = calculateCompletion( completedTasks, totalTasks,);
let statusMessage = `${completionPercentage}% complete. Continue with required tasks.`;
if (hasBlockingIssue) { statusMessage = "Blocked: resolve the current issue.";} else if (completionPercentage === 100) { statusMessage = "Ready for review.";}
const statusForm = document.querySelector("#status-form");const projectNameInput = document.querySelector("#project-name");const statusOutput = document.querySelector("#status-message");
if (!statusForm || !projectNameInput || !statusOutput) { throw new Error("Required status form elements are missing.");}
function handleStatusSubmit(event) { event.preventDefault();
const projectName = projectNameInput.value.trim();
if (projectName === "") { projectNameInput.setAttribute("aria-invalid", "true"); statusOutput.textContent = "Enter a project name."; projectNameInput.focus(); return; }
projectNameInput.removeAttribute("aria-invalid"); statusOutput.textContent = `${projectName}: ${statusMessage}`;}
function handleStatusReset() { projectNameInput.removeAttribute("aria-invalid"); statusOutput.textContent = "Enter a project name and submit the form.";}
statusForm.addEventListener("submit", handleStatusSubmit);statusForm.addEventListener("reset", handleStatusReset);The complete event flow is:
HTML source -> browser creates DOM objects -> querySelector returns required elements -> user submits the form -> browser creates a submit event -> handleStatusSubmit receives the event -> handler reads and validates input.value -> handler assigns statusOutput.textContent -> browser renders the changed DOM textThe JavaScript changes the DOM object. It does not rewrite the saved index.html file. Reloading creates a new DOM from the saved source and runs app.js again.
Self-check
Complete these checks against the required result.
- Select the Project name label and confirm that focus moves to its connected input.
- Explain how the HTML source, DOM, document object, and selected element objects relate to each other.
- Confirm that each querySelector string matches one id in index.html and that the missing-elements guard is restored.
- Submit an empty value and confirm the visible error, input focus, and aria-invalid="true" state.
- Submit Community event page with the button and with Enter, then confirm the exact 60% output without a reload.
- Submit <strong>Test</strong> and confirm that textContent displays literal text instead of creating a strong element.
- Activate Reset with the keyboard and confirm that the input, status text, and invalid state return to their starting values.
- Confirm that both addEventListener calls pass a function reference without parentheses.
- Reload the final page and confirm that no red JavaScript error remains in the Console.
Reference patterns for choice and action controls
Section titled “Reference patterns for choice and action controls”The required task-tracker result is complete before this section. Read the relevant pattern when a later project asks you to read one choice from a form or connect a separate button to one action.
Read one selected option
Section titled “Read one selected option”A native select element lets the user choose one value from a list. Connect its visible label in the same way as a text field:
<label for="project-area">Project area</label><select id="project-area" name="projectArea"> <option value="">Choose an area</option> <option value="content">Content</option> <option value="development">Development</option> <option value="quality">Quality</option></select>The visible option text tells the user what they can choose. The value is the stable string that JavaScript reads. The first option uses an empty value so the program can distinguish the instruction from a completed choice.
Select the element and include it in the missing-elements guard:
const projectAreaSelect = document.querySelector("#project-area");
if (!projectAreaSelect) { throw new Error("The project-area choice is missing.");}Read projectAreaSelect.value inside the form’s submit handler. A form-control value is a string. Validate the empty string before using a selected value:
const projectAreaId = projectAreaSelect.value;
if (projectAreaId === "") { statusOutput.textContent = "Choose a project area."; projectAreaSelect.focus(); return;}The selected string can identify one record after you learn arrays and objects. Read Find one record by a stable identity before you connect a choice to a record.
Check: Use only the keyboard to open the choices, select each real option, and submit the form. Confirm that the empty option follows the invalid path and each real option produces its documented value.
Listen for one independent button action
Section titled “Listen for one independent button action”Use a native button with type="button" for an action that is not form submission or form reset:
<button id="pause-project" type="button">Pause project</button>Select the button, check the selection, define one named callback, and register the listener once:
const pauseButton = document.querySelector("#pause-project");
if (!pauseButton) { throw new Error("The pause control is missing.");}
function handlePauseClick() { statusOutput.textContent = "The project is paused.";}
pauseButton.addEventListener("click", handlePauseClick);The native button produces a click event for pointer activation and keyboard activation with Enter or Space. You do not need a separate keyboard event for the same button action. type="button" prevents this control from submitting a form when it appears inside one.
Register a listener once when the selected button remains in the DOM. Do not register the same listener inside a render function that runs after every state change. If rendering replaces a control, the new element needs its own listener and a deliberate focus result.
For several independent actions, make one action work and pass its mouse and keyboard checks before you connect the next action. Give each action one event handler or one documented routing rule.
Check: Activate the button once with a pointer, Enter, and Space. Each activation must produce exactly one result. Reload the page and confirm that one activation still produces one result.
Keep an independent button unavailable until ready
Section titled “Keep an independent button unavailable until ready”If an action needs state that does not exist yet, save its button with disabled in the HTML:
<button id="pause-project" type="button" disabled>Pause project</button>disabled is a Boolean attribute. Its presence makes the native button unavailable to pointer and keyboard activation and removes it from the Tab order. The attribute does not need a value. Its absence makes the button available again.
After the program has selected and checked pauseButton, choose one of these operations when application state changes:
pauseButton.setAttribute("disabled", ""); // Make the action unavailable.pauseButton.removeAttribute("disabled"); // Make the action available.These lines show two alternative results. Do not run both for the same state. In a render function, use a condition based on current state to choose one. Keep the button element and its click listener stable while its availability changes. If disabling a focused button is possible, move focus to another available control with a useful next action.
Check if you try this reference pattern: Start with the disabled pause-project button and its listener from the previous section. Reload: Tab skips the button and a pointer click has no effect. In the Console, run pauseButton.removeAttribute("disabled"). Activate it once with a pointer, Enter, and Space in separate tests; each activation produces one result. Run pauseButton.setAttribute("disabled", "") to restore the unavailable state.
Move focus to a newly shown section
Section titled “Move focus to a newly shown section”When an action hides the focused control and shows a different section, move focus to the new section’s heading after rendering. Give the heading tabindex="-1" so JavaScript can focus it without adding it to the normal Tab order:
<h2 id="result-heading" tabindex="-1">Result</h2>resultSection.hidden = false;resultHeading.focus();Keep the heading’s focus outline visible. The heading tells a keyboard or screen-reader user where the new content begins. Tab then continues to the next available control in source order. Do not call focus() while the heading is still hidden.
Common event and DOM failures
Section titled “Common event and DOM failures”| Symptom | Likely cause | Focused check |
|---|---|---|
Cannot read properties of null |
A selector did not match an element, or the guard is missing | Compare the selector with the HTML id and restore the guard |
| The page reloads on submit | The handler does not call event.preventDefault() |
Confirm the call is the first statement in the submit handler |
| The handler runs during page load | The listener calls the function instead of passing it | Remove () from handleStatusSubmit in addEventListener |
| The output never changes | The code updates the wrong element or property | Confirm statusOutput selects #status-message and assigns textContent |
| Enter does nothing in the input | The listener uses a button click instead of form submit | Register submit on statusForm |
| Reset clears only the input | The reset listener is missing or misspelled | Confirm the reset event name and handleStatusReset reference |
| A choice always uses the instruction option | The handler does not read the selected element’s current value | Log the selected value inside the submit handler and test one real option |
| An independent button submits the form | The button has the default submit type | Add type="button" to the independent action |
| One activation runs the same action several times | The listener is registered during repeated rendering | Register the stable control’s listener once outside the render function |
Use one symptom to select one check. Do not rewrite the complete program before the first error has a known cause.
Next step or safe stopping point
Section titled “Next step or safe stopping point”The required lesson is complete when the form works with mouse and keyboard input, both validation paths work, reset restores the interface, and the Console remains free of JavaScript errors.
Continue to Manage and render interface state to store several project records in JavaScript and render the complete interface from one state value.
If you stop here, leave yourself this resume note: The form now connects DOM events to direct text updates. Next, open the interface-state lesson and separate stored data from the function that renders it.