Skip to content
wannacode

A list from data

In short: Rendering from data is one render() function that builds the list on the page from an array, so that every place shows the same thing.

Problem

The cart lies

A buyer adds bread and milk. The lines in the cart are right, the total is right, and the top says "Cart (0)". Press both "Add" buttons and look at the badge.

HTML, CSS, JS

The console is empty. Every place on the page is updated by a separate line of code in add, and there is no such line for the badge. The browser does not know that the three places show the same thing. So nobody notices the mistake, except the buyer.

The idea

From an array to a list

The products come as data: an array of objects with a name and a price. The page has to show them as list items. The path is the same for every list:

function productItem(product) {
const li = document.createElement('li');
li.textContent = `${product.name}, ${money(product.price)}`;
return li;
}
list.replaceChildren(...products.map(productItem));
  • document.createElement('li') creates a new element. It is not on the page yet.
  • textContent sets the text. The name of the product is text, not HTML, so no character in it becomes a tag.
  • products.map(productItem) makes one <li> from each product: an array of elements.
  • list.replaceChildren(...) removes the old children of the list and puts in the new ones. ... spreads the array into separate arguments.

This is called rendering a list from data: the data live in JavaScript, and the page is built from them by a function.

See it

The catalogue

HTML, CSS, JS

Change the price of the bread in the array and press "Run". You do not touch the HTML: the list follows the data. Add a fifth product to the array and it shows up as a fifth line.

Try it

Only what is in stock

Show in the list only the products that are in stock (stock is greater than 0). Below the list, in #count, write how many there are: "In stock: 3 of 5".

HTML, CSS, JS

The idea

One set of data, three places

The cart is an array cart. The page shows it three times: the count at the top, the lines and the total. Every time cart changes, the three places have to change together.

By hand this means: every function that changes cart touches every place that shows it. add touches the lines and the total. remove touches the lines, the total and the badge. Tomorrow a fourth place comes, say "Free delivery over €30", and every function has to remember it.

When one place is forgotten, the data and the screen drift apart: cart holds two products and the page says zero. There is no error. There is a page that shows something false.

See it

Watch them drift apart

const cart = [];
function addByHand(product) {
cart.push(product);
cartList.append(cartItem(product));
total.textContent = `Total: ${money(sum())}`;
}
function render() {
badge.textContent = `Cart (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Total: ${money(sum())}`;
}
function add(product) {
cart.push(product);
render();
}
Step by step, in words
  1. On the left are the data: the array cart. On the right are the three places on the page that show it: the badge, the lines and the total.
  2. addByHand(bread). First the data change: the array has one product.
  3. Then the function touches the screen by hand: a new line and a new total. Every place is a separate line of code.
  4. Nobody wrote a line for the badge. The data say 1, the badge says 0. There is no error in the console: the page simply shows something false.
  5. One more product, by hand again. The gap grows: 2 against 0.
  6. Now add(): it only changes the data. For a moment the whole screen is behind, because nothing has been drawn yet.
  7. render() draws all three places again from cart: the badge, every line and the total. It does not care what was on the screen before.
  8. The rule: every change is the data first, then render(). The screen follows from the data, and a forgotten place is impossible.

addByHand touches two places out of three. render touches all three, every time, from the data. It does not ask what was on the screen before, so it cannot be misled by it.

The idea

One render()

The rule is short: the functions that change the data do not touch the screen. They change the data and call render(). Only render() writes to the page, and it writes everything from the data:

function render() {
badge.textContent = `Cart (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Total: ${money(sum())}`;
}

This way every place is described once. A new place is one line in render(), and it works for add, for remove and for every future function. The lines are drawn again in full: replaceChildren removes the old ones and puts new ones from cart.map(cartItem).

Try it

One render()

add and remove touch the screen by hand, and each forgets one place. Write render() so that it draws the badge, the lines and the total from cart, and make add and remove only change cart and call render().

HTML, CSS, JS

Try it

Find the bug

The badge is always one product behind: after the first click it says 0, after the second 1.

Pick the line with the bug.

The idea

The price of "everything again"

render() does not know what changed, so it replaces everything. Usually you cannot see that. Sometimes you can. In the cart below every line has a quantity field, and every change of the field calls render(). The buyer wants 15 milks: deletes the 2, types 1 and 5.

HTML, CSS, JS

After the digit 1, render() removes the old field and puts a new one with the value 1. The focus was on the old field and it is gone, so the 5 goes nowhere. The same happens to the caret in a text field, to the scroll position in a long list and to an open drop-down. And with 1000 lines the browser builds 1000 new elements on every click.

Both ways are bad: by hand you forget places, and "everything again" loses focus and is slow for a big list. The answer is a third way: you write one function that says what the screen looks like for these data, like render(). Something else compares the new result with the old one and changes only the difference on the page: the text of the badge, one line, nothing about the field. This is what React and Vue do, and it is the next module: components.

More precisely, if you are curious

innerHTML instead of createElement. list.innerHTML = products.map((p) => `<li>${p.name}</li>`).join('') is shorter, but the name of the product goes in as HTML. A name with <img onerror=…> becomes code on the page (XSS). textContent and createElement keep text as text.

replaceChildren. With no arguments it empties the element: list.replaceChildren(). Before it, people wrote list.innerHTML = '' and then append.

The listeners. Every new button gets a new listener in productItem. The old buttons disappear together with their listeners. Another way is delegation: one listener on the <ul> and event.target.closest('li'), with the element saying which product it is (data-id).

hidden. element.hidden = true is like the hidden attribute in HTML: the element is not shown. If a CSS rule gives the element a display, the rule wins over hidden and the element shows again.

Keeping the focus by hand. You can remember which field had the focus and where the caret was, and put them back after render(). It works, but it is one more place to forget. The libraries do it for you, because they do not replace the field at all.

Exercise

The cart

Build the catalogue and the cart from the data. Every product is a line with an "Add" button (the name for a screen reader: "Add White bread"). Every line in the cart has a "Remove" button ("Remove White bread"). The badge at the top is "Cart (2)", the total is "Total: €10.00", and "The cart is empty" shows only when the cart is empty (the hidden property).

HTML, CSS, JS

Test

Check yourself

1. The cart shows the number of products in three places. When you update by hand, what is the most common mistake?
2. What does list.replaceChildren(...items) do?
3. Where in add does render() belong?
4. Why does "draw everything again" lose the focus in a field inside the list?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·