Lab
Observer
type Listener = (quantity: number) => void;
type Registration = { id: number; listener: Listener };
// The lesson's cart, keeping its registrations in a list.
class Cart {
private quantity = 0;
private listeners: Registration[] = [];
private nextId = 1;
getQuantity(): number {
return this.quantity;
}
subscribe(listener: Listener): number {
const id = this.nextId++;
this.listeners.push({ id, listener });
return id;
}
unsubscribe(id: number): void {
this.listeners = this.listeners.filter((registration) => registration.id !== id);
}
setQuantity(quantity: number): void {
if (!Number.isInteger(quantity) || quantity < 0 || quantity > 9) return;
if (quantity === this.quantity) return;
this.quantity = quantity;
for (const registration of [...this.listeners]) registration.listener(quantity);
}
registrations(): number {
return this.listeners.length;
}
}
// The cart panel: the shopper opens and closes it.
class CartPanel {
renders: number[] = [];
open(cart: Cart): void {
this.renders.push(cart.getQuantity()); // the cart doesn't replay, so read it
cart.subscribe((quantity) => this.renders.push(quantity));
}
close(cart: Cart): void {
// Closing slides the panel out of view.
}
}
// Checker plumbing: runs one script against a page. You don't need to edit it.
function runCart(script: string[]): string {
const cart = new Cart();
// The header badge: subscribed for as long as the page is open.
const badge = [cart.getQuantity()];
cart.subscribe((quantity) => badge.push(quantity));
const panel = new CartPanel();
for (const step of script) {
switch (step) {
case 'open': panel.open(cart); break;
case 'close': panel.close(cart); break;
default: cart.setQuantity(Number(step));
}
}
const views = `badge [${badge.join(' ')}] · panel [${panel.renders.join(' ')}]`;
return `${views} · registrations ${cart.registrations()}`;
}