πŸ₯¦Ndiro
Chapter 7 of 13

JavaScript and the DOM

JavaScript is the only language browsers execute, so it's where all behavior lives. As a language it will feel familiar β€” dynamically typed like Python, C-family syntax like Java. This chapter covers the handful of language features Ndiro's code leans on, then the two ideas that are genuinely new: the event loop and the DOM.

The language, in the dialect this repo speaks

const GOAL = 20;              // binding can't be reassigned (use by default)
let daysShown = 7;            // mutable binding
var icon = …                  // legacy 'let' β€” you'll see it in older-style blocks

const f = (x) => x * 2;       // arrow function; also plain `function name() {}`
const matches = FIBER_GUIDE   // first-class functions everywhere:
    .filter(f => f.name.toLowerCase().includes(q))
    .slice(0, 8);

if (!query) { … }             // falsy: '', 0, null, undefined, NaN
a === b                       // always ===; == does type coercion, avoid it

Objects are untyped bags (meal.description, day.totals[NUTRIENT.key] β€” bracket syntax for computed keys), arrays are lists, and there's no compile step: the browser parses source directly, and errors surface at runtime in the console (F12 β†’ Console β€” check it first whenever a page misbehaves).

One thread and an event loop

Each page runs JavaScript on a single thread driven by an event queue. Your code runs only in short bursts: an event fires (click, keypress, network response), its handler runs to completion, the loop takes the next event. Block that thread β€” a long loop, a synchronous wait β€” and the entire UI freezes: no scrolling, no clicks. So all I/O is asynchronous by construction, and slowness must be pushed to the server (as Ndiro does with the photo pipeline β€” the capstone chapter).

async/await is the ergonomic layer over that. Here is the app's core data fetch, trimmed:

templates/log.html β†—
async function loadMeals(opts) {
    const resp = await fetch('/api/meals?days=' + daysShown +
                             '&anchor=' + currentDateLocal());
    …
    const data = await resp.json();
    renderDays(data);
}

fetch() returns a promise β€” a handle to a value that doesn't exist yet. await suspends this function until it resolves, without blocking the thread: other events keep being handled, and the function resumes later with the value.

🐘 From your world

A promise is a Future/ListenableFuture; await is a non-blocking get() β€” the runtime parks the coroutine instead of the thread. The event loop is a single-threaded executor (Netty's event loop is the exact same architecture). The cardinal sin is also the same: never do blocking work on the event thread.

The DOM: reading and building the tree

The DOM from the HTML chapter is exposed to JavaScript as objects. Ndiro's entire UI-update strategy is three verbs:

const el = document.getElementById('todayFiber');   // find
el.textContent = '12.5';                            // change
container.appendChild(card);                        // add

Watch the pattern that renders one meal card (trimmed from log.html):

templates/log.html β†—
function mealCard(meal) {
    const card = document.createElement('div');
    card.className = 'meal-card card';

    const desc = document.createElement('div');
    desc.className = 'meal-desc';
    desc.textContent = meal.description;      // ← data goes in as TEXT, always
    …
    const editBtn = document.createElement('button');
    editBtn.textContent = '✎';
    editBtn.addEventListener('click', () => startEdit(meal));
    …
    return card;
}

Two things deserve attention. First, textContent versus innerHTML: assigning innerHTML parses the string as HTML, so feeding it user data is an XSS hole β€” the DOM-side version of skipping Jinja's autoescaping. The repo has an explicit law about it, stated where it's bent:

templates/log.html β†—
// innerHTML only ever receives these fixed string literals.
function setBusy(btn, label) {
    btn.innerHTML = '<span class="spin">πŸ₯¦</span> ' + label;

Static literals from the source are fine; anything that ever touched a user or the network goes through textContent. In review, grep the diff for innerHTML and check which side of that line it's on.

Second, the closure: () => startEdit(meal) captures meal, so each card's button remembers its own meal with no registry or lookup table. Closures are the idiomatic way UI code binds data to handlers.

Events: bubbling and the menu

Events fire on an element, then bubble up through its ancestors to document. The corner menu uses the whole mechanism in eight lines β€” open on toggle click, close on any click elsewhere, close on Esc:

templates/base.html β†—
menuToggle.addEventListener('click', function (e) {
    e.stopPropagation();          // don't let this click reach document…
    setOpen(panel.hidden);
});
document.addEventListener('click', function (e) {
    // …because any click that DOES bubble to document, from outside
    // the menu subtree, means "clicked elsewhere": close.
    if (!panel.hidden && !menu.contains(e.target)) setOpen(false);
});
document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') setOpen(false);
});

State: module variables and localStorage

Where does client state live? Mostly in plain top-level variables in the page's script β€” daysShown, editing (null, or the meal being edited), estimating (a re-entrancy latch). This state is per-tab and vanishes on reload, which is fine because the server is the source of truth: after every successful save, the code just refetches the list rather than surgically patching the DOM. Less clever, always consistent.

The one durable piece of client state is the theme: localStorage.setItem('theme', 'light') β€” a tiny per-origin key-value store that survives restarts. Note every access is wrapped in try/catch: in private browsing, storage may throw, and a theme preference is never worth an error page.

πŸ”¬ Try it

On the log page, open the Console and run: document.getElementById('todayFiber').textContent = '999'. Then run FIBER_GUIDE.length and NUTRIENT β€” the constants Jinja injected in the Jinja chapter are just sitting there. Finally paste document.querySelectorAll('.meal-card').length to count rendered cards. The console is a REPL over the live page β€” the fastest way to learn the DOM API there is.

πŸ