web · Level 3

JavaScript: the browser, events and application state

Make the event page respond to people with plain JavaScript and no build step: events, one state object, a checked form and a saved shortlist, tested in a real browser.

By Mickarle Wagstaff-Irons - Micky Irons

  • Level 3Building
  • 140 min
  • 6 chapters
  • Online course and PDF workbook
The Pageweb / 03

Read, practise and review

Your next step

Start this course online

Register your start, then work through the lessons and exercises at your own pace. You can download the PDF separately. Where a scored exam is available, you will find it after the lessons.

Register or resume your course

Mickai privately records this registration and sends its team a course-start notification. It records your intention to start, not proof of attendance or completion. Our course-start privacy notice explains the details and the 30-day resume cookie.

Follow the Mickai company page on LinkedIn

You can start learning without subscribing or following. Newsletter email confirmation and provider enrolment are recorded separately.

Start with the essentials

The short answer

Load your script with defer, find elements with querySelector and listen for events with addEventListener, often once on a parent. Keep what the page knows in one plain object and redraw the page from it. Put untrusted text in with textContent, never innerHTML. Validate forms with the Constraint Validation API, move focus to the first error, announce results in a status region and never store secrets in localStorage.

What you will learn

  • You will be able to load a script with defer, say why a module needs a web server, and find and create elements in the DOM tree.
  • You will be able to handle clicks and key presses with one delegated listener, using real buttons so the keyboard works without extra code.
  • You will be able to keep state in one plain object, render the page from it without losing focus, and keep untrusted text out of innerHTML.
  • You will be able to check a form with the Constraint Validation API, move focus to the first error and announce results in a status region, naming WCAG 2.2 criteria 3.3.1 and 4.1.3.
  • You will be able to save state in localStorage safely, state its limits, and read errors in the Console.
  • You will be able to review JavaScript that an AI assistant wrote and find its common faults by running it.

Who it is for

People who have hand-written an accessible, styled page, such as the event page from the HTML and CSS workbooks, and now want it to respond to the people using it. You write plain JavaScript in a text editor and test it in a browser, with no framework and no build step.

Before you start

  • The HTML and CSS workbooks, or the finished Marigold Lane event page from them: index.html, style.css and layout.css in a folder called event-page. A plain text editor and a desktop browser with developer tools; the tool names here are Microsoft Edge's. No earlier JavaScript is needed, but you should be happy to type code exactly and read it slowly.

Learn in your browser · Chapter 01 of 06

01

How the page runs your script

Add one script to the event page, see when it runs, and meet the tree it works on.

This workbook adds JavaScript, the language browsers run, to the Marigold Lane event page from the HTML and CSS workbooks. Every example was run in Microsoft Edge 153.

Create an empty app.js in event-page and add this line to the head of index.html, under the stylesheet links.

html · 1 line
<script src="app.js" defer></script>

When scripts run. A plain script runs as soon as it is fetched, and the browser stops reading the page meanwhile (HTML standard). With defer it runs, in order, once the page has been parsed (MDN), so it can find every element.

A module (type="module") is deferred automatically and keeps its top-level names private, but opened from disk Edge blocked it: Access to script at '.../app.js' from origin 'null' has been blocked by CORS policy. MDN says to test modules through a server, so this workbook uses a classic deferred script.

The DOM. The browser turns your HTML into the Document Object Model, a tree of objects that scripts can read and change (MDN). Part of the event page's tree:

text · 8 lines
main
`-- section#stalls
    |-- h2  "Stalls"
    `-- ul.cards
        `-- li  (one of four)
            `-- article.stall
                |-- h3  "Vegetable seeds"
                `-- p   "Beans and peas."

querySelector returns the first element matching a CSS selector, or null; querySelectorAll returns every match (MDN). Try these in the Console (Ctrl+Shift+J). The first time you paste there, Edge asks you to type allow pasting: do so only for code you understand (Microsoft).

js · 3 lines
document.querySelector('#stalls h2').textContent
document.querySelectorAll('.stall').length
document.querySelector('.stall').parentElement.tagName

Put this in app.js: it gives each stall card a button named from its heading.

js · 11 lines
// app.js: behaviour for the Marigold Lane page
const stalls = document.querySelectorAll('.stall');
console.log(stalls.length + ' stalls found');

for (const stall of stalls) {
  const button = document.createElement('button');
  button.type = 'button';
  button.dataset.stall = stall.querySelector('h3').textContent;
  button.textContent = 'Shortlist ' + button.dataset.stall;
  stall.append(button);
}

const names a value that cannot be reassigned, though an object's contents can change (ECMAScript, MDN). createElement makes an element, dataset.stall writes a data-stall attribute (MDN), textContent sets its text and append adds it to the card.

Try it yourself · Activity 01

15 min

Load it, then break the load order

Check what the script did, then remove defer.

  1. Reload, read the Console and run the three query lines.
  2. Click the page background near the top and press Tab until focus leaves the page, noting each stop.
  3. Delete defer, reload and look again. Then put it back.

Why is a script that quietly finds nothing harder to fix than one that crashes?

Worked answer

The Console shows 4 stalls found, and the queries return 'Stalls', 4 and 'LI'. Tab visits the three menu links, the four Shortlist buttons, then the form's four controls. Without defer: 0 stalls found, no buttons and no error, because the script ran before the cards existed. A silent failure leaves nothing to search for. If the Console shows Failed to load resource: net::ERR_FILE_NOT_FOUND instead of 4 stalls found, app.js is not beside index.html or is misnamed.

Next chapter

Learn in your browser · Chapter 02 of 06

02

Events and delegation

Make the buttons do something, with one listener that hears every card.

An event is something that happens, such as a click or key press. addEventListener(type, listener) asks the browser to call your function each time, passing an event object; (event) => { ... } is an arrow function, a compact way to write one (MDN).

Delegation. The DOM Standard says an event on an element also reaches listeners on its ancestors, so one listener on the list can serve every button (event delegation, MDN). event.target is where the event started, event.currentTarget the element whose listener is running, and closest(selector) returns the nearest match, the element itself or a parent, or null (MDN). Add this to the end of app.js.

js · 7 lines
const cards = document.querySelector('.cards');

cards.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-stall]');
  if (!button) return;
  console.log('clicked', button.dataset.stall, 'on', event.currentTarget.tagName);
});

Keyboard. The WAI-ARIA Authoring Practices say Space and Enter both activate a button, and a real button does it for you: Edge fired click for both keys. WCAG 2.2 criterion 2.1.1 Keyboard (level A) asks that everything works from a keyboard; real buttons give you that free, and a clickable div does not.

Try it yourself · Activity 02

10 min

Follow the clicks

Add the listener, then predict each Console line before you act.

  1. Click Shortlist Vegetable seeds, then the words Mint, in damp paper. on the Cuttings card.
  2. Click near the top of the page, press Tab five times, then press Enter and then Space.

What would change if each button had a listener of its own?

Worked answer

Step 1 logs clicked Vegetable seeds on UL (the listener is on the ul), then nothing, because closest found no button. Step 2 reaches Shortlist Flower seeds and logs clicked Flower seeds on UL twice, once per key. A listener per button means four listeners and one more per new card, while the list's one listener also hears cards added later.

Next chapter

Learn in your browser · Chapter 03 of 06

03

State in, HTML out, safely

Keep what the page knows in one object, draw the page from it, and never let text become code.

State is what your page knows: here, which stalls are shortlisted. Keep it in one object, change it in one function, and let a render function draw the page from it, never reading state back from the page. Add a status line to index.html after the cards' closing </ul>, and two rules to layout.css.

html · 1 line
<p id="shortlist-status" role="status"></p>

css · 2 lines
.stall button { grid-column: 1 / -1; justify-self: start; }
[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

Add this to the end of app.js, and change the listener's console.log line to toggle(button.dataset.stall);.

js · 23 lines
const state = { shortlist: [] };
const shortlistStatus = document.querySelector('#shortlist-status');

function toggle(name) {
  if (state.shortlist.includes(name)) {
    state.shortlist = state.shortlist.filter((n) => n !== name);
  } else {
    state.shortlist.push(name);
  }
  render();
}

function render() {
  for (const button of cards.querySelectorAll('button[data-stall]')) {
    const on = state.shortlist.includes(button.dataset.stall);
    button.setAttribute('aria-pressed', String(on));
  }
  shortlistStatus.textContent = state.shortlist.length
    ? 'Shortlisted: ' + state.shortlist.join(', ') + '.'
    : 'Your shortlist is empty.';
}

render();

aria-pressed makes each button a toggle button, whose label must not change, because screen readers announce the state (Authoring Practices). Styling [aria-pressed="true"] means sight and software share one source. render updates the existing buttons, because rebuilding them destroys the one with focus.

textContent versus innerHTML. textContent inserts a text node, never markup (DOM Standard). innerHTML parses HTML, a common route for cross-site scripting (XSS), where a site runs an attacker's code as its own (MDN). Inserted script elements do not run (HTML standard), but an image with a broken address runs its onerror handler (MDN). Test that harmlessly in the Console:

js · 4 lines
const test = `<img src="missing.png" onerror="console.log('This text ran as code')">`;
shortlistStatus.textContent = test;
shortlistStatus.innerHTML = test;
shortlistStatus.innerHTML = `<script>console.log('script ran')</script>`;

Try it yourself · Activity 03

25 min

State, focus and safe text

Build the shortlist, compare two renders, then test both ways of inserting text.

  1. Click Shortlist Vegetable seeds, then Shortlist Cuttings. Read the status line, run JSON.stringify(state) and inspect each button's aria-pressed.
  2. Reload, click near the top, press Tab four times and press Space. Where is the focus ring?
  3. In render's loop, after the setAttribute line, add button.replaceWith(button.cloneNode(true));. Reload, repeat step 2, then press Space again. Delete the line.
  4. Run the four Console lines one at a time.

Where could text from a stranger reach your pages?

Worked answer

Step 1: Shortlisted: Vegetable seeds, Cuttings. The state is {"shortlist":["Vegetable seeds","Cuttings"]}, aria-pressed reads true, false, true, false, and pressed buttons turn dark (light in the dark theme). Step 2: Shortlisted: Vegetable seeds., ring on the button. Step 3: the same status, but focus falls to the body, the ring vanishes and the second Space only scrolls the page: a keyboard user has lost their place. Step 4: textContent shows the string itself; innerHTML shows no text, only a 16-pixel image box, and logs Failed to load resource: net::ERR_FILE_NOT_FOUND, then This text ran as code; the script line logs nothing. Typed text, addresses, stored data and other sites' data are all untrusted.

Next chapter

Learn in your browser · Chapter 04 of 06

04

Forms: validation, focus and status messages

Check the sign-up form in the browser, send nothing, and tell every user what happened.

The Constraint Validation API is the browser's form checking, readable from scripts. required needs a value and type="email" an address-shaped one. Each field's validity has flags such as valueMissing and typeMismatch, and checkValidity() returns true or false (MDN). Change the form in index.html (dots are unchanged lines).

html · 10 lines
<form novalidate>
  ...
    <input id="name" name="name" type="text" required aria-describedby="name-error">
    <span id="name-error"></span>
  ...
    <input id="email" name="email" type="email" required aria-describedby="email-hint email-error">
    <span id="email-error"></span>
  ...
</form>
<p id="form-status" role="status"></p>

novalidate turns off the browser's own error bubbles, not the API (MDN). WCAG's guidance on 3.3.1 notes such bubbles may vanish, or stay small when zoomed, so this page writes its own. Add this to the end of app.js.

js · 27 lines
const form = document.querySelector('#signup form');
const fields = [document.querySelector('#name'), document.querySelector('#email')];
const formStatus = document.querySelector('#form-status');
form.querySelector('button').type = 'submit';

function problem(field) {
  if (field.validity.valueMissing) return field.id === 'name' ? 'Enter your name.' : 'Enter your email address.';
  if (field.validity.typeMismatch) return 'Enter an email address like sam@example.com.';
  return '';
}

form.addEventListener('submit', (event) => {
  event.preventDefault(); // this demonstration never sends anything
  for (const field of fields) {
    const message = problem(field);
    document.querySelector('#' + field.id + '-error').textContent = message;
    field.setAttribute('aria-invalid', String(message !== ''));
  }
  const firstBad = fields.find((field) => !field.checkValidity());
  if (firstBad) {
    formStatus.textContent = '';
    firstBad.focus();
    return;
  }
  formStatus.textContent = 'Thank you, ' + fields[0].value + '. Nothing was sent: this is a demonstration.';
  form.reset();
});

The button becomes a submit button only while this script runs, and preventDefault() cancels the sending every time (MDN). Each message goes in a span tied to its field by aria-describedby, and aria-invalid marks the field (technique ARIA21). Focus moves to the first problem, where Edge's accessibility tree showed the name field as invalid, named Your name and described by Enter your name. WCAG 3.3.1 Error Identification (A) asks for errors described in text, and 3.3.3 Error Suggestion (AA) for fixes, as the email message gives.

Status messages. WCAG 2.2 criterion 4.1.3 Status Messages (AA) asks that results, progress or errors which do not take focus can still be announced, like a shopping cart's new count. role="status" does this (technique ARIA22): MDN says it is a polite live region, announced when the user is idle, and Edge exposed both status lines that way. MDN advises putting live regions in the initial HTML, as here.

Try it yourself · Activity 04

20 min

Break the form politely

Try each input, noting the messages and where focus goes.

  1. Press Join the list with both fields empty.
  2. Type Sam Example, press Tab, type sam and press Enter. Then replace sam with a@b and press Enter.
  3. Try three spaces as the name, with sam@example.com, then the chapter 3 test string (between the backticks) as the name, again with sam@example.com.
  4. Fix the spaces bug with setCustomValidity: a field stays invalid until its custom message is empty (MDN).

Which result would a screen reader user have missed without the status line?

Worked answer

Step 1: Enter your name. and Enter your email address., focus on the name field. Step 2: Enter an email address like sam@example.com., focus on the email field; then Thank you, Sam Example. Nothing was sent: this is a demonstration. The address still ends index.html. Step 3: spaces satisfy required, so the thanks has a blank name; the test string shows as plain text and nothing is logged. Step 4, tested: add fields[0].setCustomValidity(fields[0].value.trim() ? '' : 'Enter your name.'); after the preventDefault line and if (field.validity.customError) return field.validationMessage; as the first line of problem.

Next chapter

Learn in your browser · Chapter 05 of 06

05

Saving state, and reading errors

Keep the shortlist after a reload, know what localStorage is not for, and read what the Console tells you.

localStorage keeps text under text keys with no expiry date (MDN). It belongs to an origin (the scheme, host and port of an address), shared by every page there (HTML standard). Add these functions to the end of app.js, then make two edits: the state line becomes const state = { shortlist: load() }; and toggle calls save(); just before render();.

js · 17 lines
function load() {
  try {
    const saved = JSON.parse(localStorage.getItem('marigold-shortlist'));
    return Array.isArray(saved) ? saved : [];
  } catch (error) {
    console.warn('Saved shortlist ignored:', error.message);
    return [];
  }
}

function save() {
  try {
    localStorage.setItem('marigold-shortlist', JSON.stringify(state.shortlist));
  } catch (error) {
    console.warn('Shortlist not saved:', error.message);
  }
}

JSON.stringify turns the list into text and JSON.parse turns it back. A missing key gives null, not a list, hence the check. Non-JSON text makes JSON.parse throw a SyntaxError (ECMAScript), and a full store makes setItem throw QuotaExceededError (HTML standard). Both are caught, so the page keeps working.

Scroll sideways to see every column.

Saving state, and reading errors · Table 1
LimitWhat was found
Text onlyEdge stored an array as a,b, an object as [object Object]: use JSON.
SizeMDN: 5 MiB per origin. Edge refused over 5,242,880 characters, keys and values together.
Files from diskMDN: undefined and may vary, and it describes one store per file. In Edge all such pages shared one store, across folders and restarts: use distinctive keys.
Not privateAny script on the origin, injected code included, can read it (MDN): never store passwords, tokens or personal details.
Not always therePrivate windows clear it; blocked storage throws SecurityError (MDN).

Reading errors. The Console shows each uncaught error with a link to its file and line, which opens that line in the Sources tool (Microsoft). Read the type and message first. From the tested files:

text · 3 lines
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
Uncaught ReferenceError: rendr is not defined
Saved shortlist ignored: Unexpected token 'o', "oops" is not valid JSON

The first came from line 15, cards.addEventListener, with defer removed: querySelector found nothing, so cards was null. The second, line 44, is a misspelt call: a ReferenceError means a name that does not exist (MDN). The third is load's own warning.

Try it yourself · Activity 05

20 min

Save, corrupt and break

Make the shortlist survive a reload, then break things on purpose.

  1. Shortlist Vegetable seeds and Cuttings, reload, and run localStorage.getItem('marigold-shortlist').
  2. Run localStorage.setItem('marigold-shortlist', 'oops') and reload.
  3. Remove defer, reload and click the error's link. Put defer back.
  4. Change the render(); line that stands alone after the render function to rendr();, reload, then undo it.

Which error would have been hardest to find without its line number?

Worked answer

Step 1: both buttons stay pressed, the status reads Shortlisted: Vegetable seeds, Cuttings. and the stored text is '["Vegetable seeds","Cuttings"]'. Step 2: the warning above, and the list starts empty. Step 3: 0 stalls found, the TypeError at line 15, no buttons. Step 4: the ReferenceError at line 44; the buttons exist but the status stays empty, because render never ran. Your line numbers differ if your file does.

Next chapter

Learn in your browser · Chapter 06 of 06

06

Reviewing JavaScript an assistant writes

Run code you did not write before you trust it, and know the faults to look for.

An AI assistant writes plausible JavaScript in seconds. Watch for invented APIs, global state in var names, innerHTML with user input, missing cleanup such as listeners re-added on every render (MDN: each new function is added again), and no keyboard handling. Treat its code as a stranger's draft: run it on a copy, and never paste passwords, keys or personal data into a prompt.

Written for this workbook, not by any product, this file shows those faults. Copy event-page to event-page-review, save this there as review.js, and point the copy's script line at it.

js · 17 lines
// review.js: written for this workbook to show common faults
var shortlist = [];

function showShortlist() {
  var visitor = document.getElementById('name').value;
  document.getElementById('shortlist-status').innerHTML =
    visitor + ' shortlisted: ' + shortlist.join(', ');
  document.querySelectorAll('.stall').forEach(function (stall) {
    stall.addEventListener('click', function () {
      shortlist.push(stall.querySelector('h3').innerText);
      showShortlist();
    });
  });
}

showShortlist();
localStorage.setObject('shortlist', shortlist);

Try it yourself · Activity 06

25 min

Review the script

List at least five problems, each with evidence and a fix.

  1. Open the review page, read the Console and press Tab through the page.
  2. Click the Vegetable seeds heading three times, running shortlist.length after each click.
  3. Reload, paste the chapter 3 test string into Your name and click one card once.

Which fault would a confident explanation have hidden best?

Worked answer

Evidence: Uncaught TypeError: localStorage.setObject is not a function; Tab never reaches a card; shortlist.length reads 1, 3, 7; window.shortlist is open to any script; the test string logs This text ran as code. Fixes: (1) invented setObject: setItem and JSON; (2) var globals: one state object or a module; (3) innerHTML with typed text: textContent; (4) listeners added on every render multiply each click: one delegated listener, added once; (5) clickable articles: real buttons with aria-pressed; (6) unchecked duplicates: a toggle function. Any five with evidence will do.

Know your limits. All of this was measured in one browser, Edge 153, under automation and without a screen reader: announcements described are what the browser exposed. None of it makes a page WCAG conformant, so test with other browsers, a keyboard and a screen reader.

Assessment options

Keep learning

The complete workbook

This workbook adds behaviour to the Marigold Lane event page from the HTML and CSS workbooks, with plain JavaScript, no framework and no build step. You will add shortlist buttons, keep their state in one object, check the sign-up form with the Constraint Validation API, announce changes to assistive technology and save the shortlist in localStorage, testing each step in a real browser. You will also learn to read errors, keep untrusted text out of innerHTML and review JavaScript an AI assistant writes.

  1. 01
    How the page runs your script

    Add one script to the event page, see when it runs, and meet the tree it works on.

    Read here · 1 exercise
  2. 02
    Events and delegation

    Make the buttons do something, with one listener that hears every card.

    Read here · 1 exercise
  3. 03
    State in, HTML out, safely

    Keep what the page knows in one object, draw the page from it, and never let text become code.

    Read here · 1 exercise
  4. 04
    Forms: validation, focus and status messages

    Check the sign-up form in the browser, send nothing, and tell every user what happened.

    Read here · 1 exercise
  5. 05
    Saving state, and reading errors

    Keep the shortlist after a reload, know what localStorage is not for, and read what the Console tells you.

    Read here · 1 exercise
  6. 06
    Reviewing JavaScript an assistant writes

    Run code you did not write before you trust it, and know the faults to look for.

    Read here · 1 exercise

Also inside: a 10-point checklist, a glossary of 12 terms and 10 questions and answers to test yourself. 6 hands-on exercises, each with a worked answer at the back where the workbook gives one.

The PDF is free. We ask for your name and email before downloading. Read the privacy notice at the download form. Free to download and use for personal learning, study groups and inside your own team. Please do not resell the workbooks or republish them as your own. Link people to trust-agent.ai instead.

Get your free PDF workbook

Enter your name and email, then download the complete workbook. This form also works without JavaScript.

We privately record your name, email and request IP for this download. Read our privacy notice and retention policy.

Visit Mickai on LinkedIn

Newsletter confirmation does not affect your download. Following Mickai on LinkedIn is optional; this site does not verify followers.

Test yourself

Questions and answers

Where should I put my script tag?

In the head, with defer: <script src="app.js" defer></script>. The file loads while the page is read and runs once the page has been parsed, so every element exists. Without defer, a script in the head runs before the body exists: in this workbook's tests it found no stall cards, at first silently and later with a TypeError. A plain script at the end of body also works, because the elements above it already exist.

Why does my module script not load when I open the page from disk?

MDN says modules loaded from a file opened on disk meet CORS errors, because of module security requirements, and that you should test them through a server. Edge 153 blocked the module in this workbook's test and reported the page's origin as null. Serve the folder from a local web server on your own computer, or use a classic script with defer.

What is the difference between textContent and innerHTML?

textContent sets text: whatever you give it appears as characters, never as markup. innerHTML parses its string as HTML, so a string containing an image with an onerror handler runs that handler. MDN advises against innerHTML for text. Use textContent for anything a person typed and anything from storage, a web address or another site.

What is event delegation, and when should I use it?

Events bubble from the element where they start up through its ancestors, so one listener on a parent can handle clicks on all its children. Use event.target.closest(...) to find the child that matters. It suits lists and grids, keeps the number of listeners down and works for children added later, which listeners attached to each child would miss.

Do I need extra code for keyboard users?

Not if you use real controls. A button takes focus with Tab and fires its click event for Enter and Space, as the Authoring Practices ask of a button. A div with a click listener gets neither. Only custom widgets need tabindex and key handling of your own, and then you should follow the matching Authoring Practices pattern.

Why keep state in one object instead of reading it from the page?

Because the page is output. When one function changes the state and another draws the page from it, they cannot disagree, and you can inspect the state in the Console. Reading state back from button text or colours invites drift, and rebuilding elements to show new state can destroy the element that has keyboard focus.

When should I use role=status or aria-live?

For short messages that report results, progress or errors without moving focus, such as Shortlisted: Cuttings. WCAG 2.2 criterion 4.1.3 asks that such messages can be announced. role="status" is polite, so it waits until the user is idle. Put the empty region in the HTML from the start, and move focus instead when the user must act, as with a form error.

Is checking a form in the browser enough?

No. It helps people fix mistakes quickly, but MDN warns it is easy to bypass, so a real form's server must check everything again. type="email" only checks the shape of an address: the HTML standard accepts a@b. Write your own messages beside each field, and move focus to the first problem.

What should I never keep in localStorage?

Passwords, access tokens, keys and personal details. Any script on the same origin can read it, including code injected through an XSS flaw, and it has no expiry date. Keep it for small, harmless things such as a shortlist, stored as JSON, and wrap reads and writes in try and catch, because both can throw.

Can I trust JavaScript an AI assistant writes?

Treat it as a draft from a stranger. Run it on a copy with the Console open, press Tab through the page and try hostile input. Look for invented APIs, var globals, innerHTML with user text, listeners added again and again, and clickable divs. Never paste passwords, keys or other people's data into a prompt.

Write as you go

My notes

Use this scratchpad for your own notes, working or answers as you read. Notes are saved in this browser on this device. Downloading makes a local copy; it does not send your notes to Mickai.

Consolidate your learning

Review your work

Work through the exercises and questions, then compare your reasoning with the worked answers. A scored exam and verifiable certificate are not yet available for this course.

Review the questions

Learn the language

Key terms

DOM
The Document Object Model: the browser's tree of objects for a page, which scripts read and change.
defer
A script attribute that loads the file while the page is read and runs it after the page has been parsed.
Module
A script loaded with type="module": deferred, in strict mode and with private top-level names. It needs a web server rather than a file opened from disk.
Event
Something that happens on the page, such as a click or a key press, which listeners can react to.
Event delegation
One listener on a parent that handles events bubbling up from its children, including children added later.
State
What the page knows at a given moment, kept in one plain object.

6 of the workbook's 12 terms. The complete glossary is in the workbook.

Follow the evidence

Sources and checks

Facts last checked: .

Examples in this workbook were run on: Microsoft Edge 153.0.4234.48 (system install, headless) driven by Playwright 1.59.0 for Python 3.12.10 on Windows 11 Pro, using real mouse clicks and key presses on pages opened from disk (file: URLs); Console lines evaluated through the Chrome DevTools Protocol in Console (REPL) mode; accessibility properties read through the same protocol; one module test served from 127.0.0.1 by Python's http.server; a persistent browser profile for the file: storage restart test. No screen reader was used. (2026-09-26).

These workbooks use AI assistance. See how the workbooks are made.

  1. The Script element: script (defer, async and module scripts)MDN Web Docs
  2. JavaScript modulesMDN Web Docs
  3. Document Object Model (DOM)MDN Web Docs
  4. Document: querySelector() methodMDN Web Docs
  5. Document: querySelectorAll() methodMDN Web Docs
  6. HTMLElement: dataset propertyMDN Web Docs
  7. constMDN Web Docs
  8. Arrow function expressionsMDN Web Docs
  9. EventTarget: addEventListener() methodMDN Web Docs
  10. Event bubbling (including event delegation)MDN Web Docs
  11. Event: currentTarget propertyMDN Web Docs
  12. Element: closest() methodMDN Web Docs
  13. Node: cloneNode() methodMDN Web Docs
  14. Event: preventDefault() methodMDN Web Docs
  15. HTMLFormElement: submit eventMDN Web Docs
  16. Keyboard-navigable JavaScript widgetsMDN Web Docs
  17. Node: textContent propertyMDN Web Docs
  18. Element: innerHTML property (security considerations)MDN Web Docs
  19. Cross-site scripting (XSS)MDN Web Docs
  20. Using HTML form validation and the Constraint Validation APIMDN Web Docs
  21. ValidityStateMDN Web Docs
  22. HTMLInputElement: checkValidity() methodMDN Web Docs
  23. HTMLInputElement: setCustomValidity() methodMDN Web Docs
  24. Client-side form validationMDN Web Docs
  25. HTMLElement: focus() methodMDN Web Docs
  26. ARIA live regionsMDN Web Docs
  27. ARIA: status roleMDN Web Docs
  28. Window: localStorage propertyMDN Web Docs
  29. Storage quotas and eviction criteriaMDN Web Docs
  30. JSON.parse()MDN Web Docs
  31. TypeError: "x" is (not) "y"MDN Web Docs
  32. ReferenceError: "x" is not definedMDN Web Docs
  33. HTML Standard: Scripting (the script element)WHATWG
  34. HTML Standard: Web storageWHATWG
  35. HTML Standard: Form control infrastructure (constraints and novalidate)WHATWG
  36. HTML Standard: The input element (valid email address)WHATWG
  37. DOM Standard (events and textContent)WHATWG
  38. ECMAScript Language Specification, editor's draft: The JSON Object (JSON.parse)Ecma International, TC39
  39. ECMAScript Language Specification, editor's draft: Let and Const DeclarationsEcma International, TC39
  40. Web Content Accessibility Guidelines (WCAG) 2.2W3C
  41. Understanding Success Criterion 2.1.1: KeyboardW3C Web Accessibility Initiative
  42. Understanding Success Criterion 3.3.1: Error IdentificationW3C Web Accessibility Initiative
  43. Understanding Success Criterion 3.3.3: Error SuggestionW3C Web Accessibility Initiative
  44. Understanding Success Criterion 4.1.3: Status MessagesW3C Web Accessibility Initiative
  45. ARIA21: Using aria-invalid to Indicate An Error FieldW3C Web Accessibility Initiative
  46. ARIA22: Using role=status to present status messagesW3C Web Accessibility Initiative
  47. Button Pattern (WAI-ARIA Authoring Practices Guide)W3C Web Accessibility Initiative
  48. Fix JavaScript errors that are reported in the ConsoleMicrosoft Learn, Microsoft Edge developer documentation
  49. Console overviewMicrosoft Learn, Microsoft Edge developer documentation
  50. Run JavaScript in the Console (Allow pasting into the Console)Microsoft Learn, Microsoft Edge developer documentation

Created by Mickarle Wagstaff-Irons - Micky Irons with the Mickai team. Published by Mickai LTD. Last updated 26 September 2026.

NextKeep going

Where to go next