Skip to content

APPLICATION DEVELOPMENT · LESSON 18 OF 24

DOM elements, forms and events

Connect typed JavaScript to a working browser form.

  • DOM
  • events
  • instanceof
  • textContent

Your runnable checkpoint

Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.

1. Understand the concepts

Browser APIs return nullable results because selectors may match nothing. A generic querySelector annotation can describe an expected element, but it does not inspect the actual HTML tag. instanceof performs that runtime check.

Event types depend on the event and element. The submit listener receives the event, prevents navigation, reads the checked input element and updates the page. Use known references or narrow event.target before accessing element-specific properties.

Insert user text with textContent. Assigning untrusted text to innerHTML would interpret markup and may create injection vulnerabilities. TypeScript does not automatically make DOM operations safe.

After npm install and npm run build, run npm run serve and open http://127.0.0.1:4173. The included server is a local practice tool bound to your computer; stop it with Ctrl+C. The HTML loads emitted JavaScript, not the TypeScript source.

This first browser example stores only DOM elements. Refreshing loses the list. The capstone later separates typed data from rendering and adds validated local persistence.

2. Build and run

These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.

src/main.ts

src/main.ts
const form = document.querySelector("#task-form");
const input = document.querySelector("#title");
const list = document.querySelector("#tasks");
const message = document.querySelector("#message");
if (!(form instanceof HTMLFormElement) || !(input instanceof HTMLInputElement)
    || !(list instanceof HTMLUListElement) || !(message instanceof HTMLParagraphElement)) {
  throw new Error("Required page elements are missing");
}
form.addEventListener("submit", event => {
  event.preventDefault();
  const title = input.value.trim();
  if (!title) { message.textContent = "Enter a title"; return; }
  const item = document.createElement("li");
  item.textContent = title;
  list.append(item);
  message.textContent = "Task added";
  form.reset();
  input.focus();
});

index.html

index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>EduPoly TypeScript Practice</title>
<style>body{font:18px system-ui;max-width:700px;margin:40px auto;padding:20px;background:#f6f8ff;color:#17263d}input,button{font:inherit;padding:10px}li{margin:12px 0}button{cursor:pointer}label{display:block;margin-bottom:8px}</style></head>
<body><h1>TypeScript task practice</h1><form id="task-form"><label for="title">Task title</label><input id="title" maxlength="120" required><button>Add task</button></form><p id="message" role="status"></p><ul id="tasks"></ul><script type="module" src="./dist/main.js"></script></body></html>

server.mjs

server.mjs
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { resolve, sep, extname } from "node:path";
const root = resolve(".");
const types = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".map": "application/json" };
createServer(async (request, response) => {
  try {
    const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
    const file = resolve(root, "." + (pathname === "/" ? "/index.html" : pathname));
    if (!file.startsWith(root + sep)) { response.writeHead(403).end(); return; }
    const content = await readFile(file);
    response.writeHead(200, { "Content-Type": types[extname(file)] ?? "application/octet-stream" });
    response.end(content);
  } catch { response.writeHead(404).end("Not found"); }
}).listen(4173, "127.0.0.1", () => console.log("Open http://127.0.0.1:4173"));

package.json

package.json
{
  "name": "edupoly-typescript-dom-forms-and-events",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "check": "tsc --noEmit",
    "build": "tsc",
    "serve": "node server.mjs"
  },
  "devDependencies": {
    "typescript": "5.9.3"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true,
    "verbatimModuleSyntax": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "types": [],
    "moduleDetection": "force"
  },
  "include": [
    "src/**/*"
  ]
}

3. Trace the code and check the result

  1. Runtime element checks protect against an edited or incomplete HTML page.
  2. preventDefault keeps the form from navigating away.
  3. textContent displays markup-looking input as literal text.

Expected result

In the browser: submit Learn TypeScript → a list item appears and the status reads Task added.

4. Practice independently

Add a delete button to each item using a click listener.

Show the explanation

Create a button with type="button", set its textContent to "Delete", attach a listener that calls item.remove(), and append it to the li.

When something goes wrong

If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.

Read the related official TypeScript documentation ↗