TypeScript Survival Guide

Favorite typescript videos

017 — Rendering an HTML Template (ListTemplate class)

Full companion narrative for TypeScript Tutorial #17 - Rendering an HTML Template: the ListTemplate class, interface-typed render, literal union position, and wiring it into the submit handler

Video: 017 · TypeScript Tutorial #17 - Rendering an HTML Template · watch · ~10:09 · Net Ninja TypeScript tutorial series

Source: this is a written companion to the video. Part of the Favorite TypeScript Videos curated path, module 05 (objects and interfaces). Closes the loop from 016: the form builds Invoice/Payment objects — now they render to the page instead of the console.

the one idea: a class that turns objects into list items

Up to now the Finance Logger form built Invoice and Payment objects and only logged them to the console. This video closes the loop: a brand-new ListTemplate class takes a formatted object and renders it as an li inside the ul on the page.

The class does exactly two things:

  1. register a list container (a ul element) in the constructor
  2. expose a render method that builds an li template and inserts it at the start or end of that container

plan as code comments: register the container, create render, build the li/h4/p template

reading the transcript: every “URL” is the ul

The auto-transcript mangles several terms. Corrections used throughout this narrative:

transcript saysactually means
“URL” / “this URL in the index.html”ul — the unordered-list element in index.html
“has formata”HasFormatter — the shared interface
“it could be nulled”querySelector can return null
“propend”prepend (insert at start)

Nothing about network URLs appears in this video. Every “URL” is a mishearing of “UL”.

the file: classes/ListTemplate.ts

import { HasFormatter } from "../interfaces/HasFormatter.js";

export class ListTemplate {
	constructor(private container: HTMLUListElement) {}

	render(item: HasFormatter, heading: string, pos: "start" | "end") {
		const li = document.createElement("li");

		const h4 = document.createElement("h4");
		h4.innerText = heading;
		li.append(h4);

		const p = document.createElement("p");
		p.innerText = item.format();
		li.append(p);

		if (pos === "start") {
			this.container.prepend(li);
		} else {
			this.container.append(li);
		}
	}
}

It is exported because app.ts imports it later.

finished ListTemplate class in the editor

the constructor registers the container

constructor(private container: HTMLUListElement) {}

The parameter property (private in front of the constructor arg) automatically assigns the passed-in ul to this.container — no separate field declaration and no assignment body needed (pattern taught in 013). The type is HTMLUListElement, the DOM type for a ul, so the constructor only accepts an actual unordered-list element, not any element.

Finance Logger form whose submissions will land in the ul

render takes an interface, not a concrete class

render(item: HasFormatter, heading: string, pos: 'start' | 'end')

Three arguments:

  1. item: HasFormatter — either an Invoice or a Payment. Typing it as the concrete classes would force a choice; typing it as the HasFormatter interface accepts anything that implements format(). Both classes already implement HasFormatter, so both qualify. The editor auto-imports the interface (with the .js extension the project uses).
  2. heading: string — the text for the h4 at the top of the list item (e.g. “Invoice” or “Payment”).
  3. pos: 'start' | 'end' — a literal union type: only those two strings are legal, nothing else. The type system enumerates the allowed options instead of a comment saying so.

Because item is guaranteed to implement HasFormatter, the body can call item.format() with confidence and put the returned string in the p tag.

building DOM nodes with inferred types

const li = document.createElement("li");
const h4 = document.createElement("h4");
h4.innerText = heading;
li.append(h4);

const p = document.createElement("p");
p.innerText = item.format();
li.append(p);

Plain DOM API, no framework: createElement, innerText, append. TypeScript infers each node’s type from the tag string — hovering li shows HTMLLIElement. No annotations needed. Structure per item: li > h4 (heading) + p (formatted string). Placement branch: pos === 'start' uses this.container.prepend(li), otherwise this.container.append(li).

console still logging raw Invoice/Payment objects — the before state

wiring it up in app.ts: instance + submit handler

// list template instance
const ul = document.querySelector("ul")!;
const list = new ListTemplate(ul);

form.addEventListener("submit", (e: Event) => {
	e.preventDefault();

	let doc: HasFormatter;
	if (type.value === "invoice") {
		doc = new Invoice(toFrom.value, details.value, amount.valueAsNumber);
	} else {
		doc = new Payment(toFrom.value, details.value, amount.valueAsNumber);
	}

	list.render(doc, type.value, "end");
});

app.ts wiring: querySelector, ListTemplate instance, submit handler

Points to notice:

  • There is only one ul on the page, so querySelector('ul') with no class is enough to grab it.
  • querySelector can return null, so TypeScript errors on passing it straight into ListTemplate. The ! non-null assertion tells the compiler the element definitely exists. The ListTemplate import at the top of app.ts is added automatically by the editor.
  • The form inputs were already grabbed with as assertions in an earlier video (HTMLFormElement, HTMLSelectElement, HTMLInputElement).
  • The old console.log lines from previous lessons are deleted. The submit handler now builds a doc typed as HasFormatter (either branch assigns compatibly), then calls list.render(...) with the doc, the type string as heading, and the position.

rendered output on the page

cleared form ready for input

Submitting an invoice for Mario appends a real list item — the format() string moves from console to page:

<ul>
	<li>
		<h4>Invoice</h4>
		<p>mario owes £250 for website work</p>
	</li>
</ul>

rendered list item on the Finance Logger page

Each further submission renders another li through the same render call.

With the Finance Logger rendering to the page, module 05 closes — classes, modifiers, modules, interfaces, rendering — and the series continues into patterns and best practices; see the course map for what’s next.

Prev: 016 — Interfaces with Classes · Next: 018 — Generics

Transcript notes: ul (not “URL” — no network URLs appear anywhere in this video), HasFormatter (not “has formata”), prepend (not “propend”). One frame caution: a mid-edit capture (archived as frame_0007.jpg, not embedded) shows li.append(item.format()); the finished code above creates the p element and sets its innerText, matching the final frame.