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.
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).
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:
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.
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 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):
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:
// 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 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:
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);
});
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.
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.