Skip to content
aviral gupta

// B6.2 · ~30 min · Beginner

Creating and removing elements

After this lesson you build new elements, put them where they belong, remove them again, and render a whole list from an array of data.

Lesson 2 of 5 in B6 The DOM and events

You will be able to

  • Create elements with createElement, fill them with textContent, and insert them with append and prepend
  • Remove elements with remove(), and empty or replace a list with replaceChildren()
  • Build a list from an array, with a <template> or a DocumentFragment, and re-render it from the data instead of editing nodes one by one
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B6.1. The list is <ul id="list"><li>html</li><li>css</li></ul>. What does this print?

    const names = [...document.querySelectorAll('#list li')].map((li) => li.textContent);
    console.log(names.length, names[1]);
  2. Predict · Activity 2 of 7

    Predict before you read on. The page has an empty <ul id="list">. What does this print?

    const li = document.createElement('li');
    li.textContent = 'tea';
    console.log(document.querySelectorAll('li').length, li.textContent);
  3. Practice · Activity 3 of 7

    Fill in the method that puts li after the last item of the list.

    list.____(li);
    list.(li);
  4. Practice · Activity 4 of 7

    Match each call to what it does to the page.

  5. Practice · Activity 5 of 7

    The list holds html and css. What does this print?

    const list = document.querySelector('#list');
    const li = document.createElement('li');
    li.textContent = 'new';
    list.prepend(li);
    console.log(list.firstElementChild.textContent, list.children.length);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The list starts empty. What does the last line print?

    const list = document.querySelector('#list');
    const li = document.createElement('li');
    for (const name of ['a', 'b', 'c']) {
      li.textContent = name;
      list.append(li);
    }
    console.log(list.children.length, list.textContent);
  7. Apply · Activity 7 of 7

    Mini-task: a film list rendered from data. Write an index.html with an empty <ul id="films">. In main.js keep an array of three films and write render(), which builds one li per film with createElement and textContent and puts them in with replaceChildren. Call render(), push a fourth film and call render() again: the list must show four films, not seven. Then remove the first item with remove() and print the texts left.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A playlist rendered from data

The page starts with a placeholder item and a <template> for one song row. main.js removes the placeholder, then render() builds a row for each song from the template and puts all rows into the list with replaceChildren. After a song is added to the array, render() runs again and the list has three rows, not five. Last, one element made with createElement is put at the start with prepend.

main.js

const songs = [
  { name: 'Blue', seconds: 245 },
  { name: 'Halo', seconds: 198 }
];
const list = document.querySelector('#songs');
const template = document.querySelector('#song-row');

// The placeholder is not needed once the script runs.
document.querySelector('#loading').remove();
console.log('after remove:', list.children.length);

// One row from the template: copy it, then fill the copy.
function row(song) {
  const copy = document.importNode(template.content, true);
  copy.querySelector('.name').textContent = song.name;
  copy.querySelector('.length').textContent = song.seconds + ' s';
  return copy;
}

// Builds the whole list from the data, replacing what was there.
function render() {
  list.replaceChildren(...songs.map(row));
}

render();
console.log('first render:', list.children.length);
songs.push({ name: 'Home', seconds: 130 });
render();
console.log('second render:', list.children.length);

// A single element made by hand, put at the start.
const now = document.createElement('li');
now.textContent = 'Now playing: ' + songs[0].name;
now.classList.add('now');
list.prepend(now);
console.log(list.firstElementChild.textContent, list.children.length);

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Playlist</title>
</head>
<body>
  <h1>Playlist</h1>
  <ul id="songs"><li id="loading">Loading...</li></ul>
  <template id="song-row"><li class="song"><span class="name"></span> <span class="length"></span></li></template>
  <script type="module" src="main.js"></script>
</body>
</html>

To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).

Output

after remove: 0
first render: 2
second render: 3
Now playing: Blue 4
<!-- #songs -->
<ul id="songs"><li class="now">Now playing: Blue</li><li class="song"><span class="name">Blue</span> <span class="length">245 s</span></li><li class="song"><span class="name">Halo</span> <span class="length">198 s</span></li><li class="song"><span class="name">Home</span> <span class="length">130 s</span></li></ul>
  • remove() took the placeholder out, so the list was empty before the first render.
  • The second render replaced the two rows with three: replaceChildren never adds to what is there.
  • Each row is a filled copy of the template; the template itself stays empty and unseen.
  • prepend put the hand-made li before the first row.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

Render a shopping list from data

render() should build the list from the array items: one li per item, with the item as its text, and the paragraph #count set to "4 items" after the fourth is added. The program calls render(), adds tea, and calls render() again. The starter appends on every call, so the second call duplicates the list, and it never sets the count. Make render() replace the list's children instead.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Run the tests: the list shows bread, milk and eggs twice, because the second render() appends to the first.

  2. Hint 2

    Build all the li elements first, for example with items.map, and return each one.

  3. Hint 3

    Put them in with list.replaceChildren(...lis), then set document.querySelector('#count').textContent.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

const items = ['bread', 'milk', 'eggs'];
const list = document.querySelector('#list');

// Builds the list from items. It runs again whenever items change.
function render() {
  const lis = items.map((item) => {
    const li = document.createElement('li');
    li.textContent = item;
    return li;
  });
  list.replaceChildren(...lis);
  document.querySelector('#count').textContent = items.length + ' items';
}

render();
items.push('tea');
render();
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

const items = ['bread', 'milk', 'eggs'];
const list = document.querySelector('#list');

// Builds the list from items. It runs again whenever items change.
function render() {
  for (const item of items) {
    const li = document.createElement('li');
    li.textContent = item;
    list.append(li);
  }
  // TODO: the count, such as "4 items".
}

render();
items.push('tea');
render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const shown = (page) => [...page.document.querySelectorAll('#list li')].map((li) => li.textContent);

test('The list shows every item once, in order', async () => {
  const page = await loadPage();
  assert.deepEqual(shown(page), ['bread', 'milk', 'eggs', 'tea'], 'the list shows ' + JSON.stringify(shown(page)));
});

test('The list has exactly four items', async () => {
  const page = await loadPage();
  const n = page.document.querySelector('#list').children.length;
  assert.equal(n, 4, 'the list has ' + n + ' children');
});

test('The count says 4 items', async () => {
  const page = await loadPage();
  const text = page.document.querySelector('#count').textContent;
  assert.equal(text, '4 items', 'the count says ' + JSON.stringify(text));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Shopping</title>
</head>
<body>
  <h1>Shopping</h1>
  <ul id="list"></ul>
  <p id="count"></p>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).

Run the checks (needs learnrun.js in the same folder):

npm install jsdom@28.1.0
node --test
Download learnrun.js

Exercise 2 of 2

A menu from a template

The page shows a placeholder, "Loading the menu...", and has a <template id="dish"> for one row. Remove the placeholder. Then, for every dish that is not sold out, copy the template, fill .name with the name and .price with "€" plus the price with two decimals (€4.00), and add the row to #menu. The template itself must stay empty. The starter keeps the placeholder, ignores soldOut and the template, and builds plain li elements.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Run the tests: the placeholder is still there, Pasta is shown, and the rows have no .name or .price.

  2. Hint 2

    Remove the placeholder with document.querySelector('#placeholder').remove(), and skip a sold-out dish with continue.

  3. Hint 3

    For each dish: const row = document.importNode(template.content, true); fill row.querySelector('.name') and '.price', then menu.append(row). dish.price.toFixed(2) gives "4.00".

Show a solution

One way to solve it. Yours can look different and still pass the checks.

const dishes = [
  { name: 'Soup', price: 4, soldOut: false },
  { name: 'Pasta', price: 9, soldOut: true },
  { name: 'Salad', price: 6.5, soldOut: false }
];
const menu = document.querySelector('#menu');
const template = document.querySelector('#dish');

document.querySelector('#placeholder').remove();
for (const dish of dishes) {
  if (dish.soldOut) continue;
  const row = document.importNode(template.content, true);
  row.querySelector('.name').textContent = dish.name;
  row.querySelector('.price').textContent = '€' + dish.price.toFixed(2);
  menu.append(row);
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

const dishes = [
  { name: 'Soup', price: 4, soldOut: false },
  { name: 'Pasta', price: 9, soldOut: true },
  { name: 'Salad', price: 6.5, soldOut: false }
];
const menu = document.querySelector('#menu');
const template = document.querySelector('#dish');

// TODO: remove the placeholder; one row from the template per dish that is not sold out.
for (const dish of dishes) {
  const li = document.createElement('li');
  li.textContent = dish.name;
  menu.append(li);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const texts = (page, selector) => [...page.document.querySelectorAll(selector)].map((el) => el.textContent);

test('The placeholder is gone', async () => {
  const page = await loadPage();
  assert.equal(page.document.querySelector('#placeholder'), null, 'the placeholder is still on the page');
});

test('Only the dishes that are not sold out are shown', async () => {
  const page = await loadPage();
  const names = texts(page, '#menu .name');
  assert.deepEqual(names, ['Soup', 'Salad'], 'the menu shows ' + JSON.stringify(names));
});

test('Each row shows its price with two decimals', async () => {
  const page = await loadPage();
  const prices = texts(page, '#menu .price');
  assert.deepEqual(prices, ['€4.00', '€6.50'], 'the prices are ' + JSON.stringify(prices));
});

test('Every row in the menu is a copy of the template', async () => {
  const page = await loadPage();
  const classes = [...page.document.querySelectorAll('#menu li')].map((li) => li.className);
  assert.deepEqual(classes, ['dish', 'dish'], 'the rows have the classes ' + JSON.stringify(classes));
});

test('The template itself stays empty', async () => {
  const page = await loadPage();
  const content = page.document.querySelector('#dish').content;
  assert.equal(content.querySelector('.name').textContent, '', 'the template was filled in: copy it, then fill the copy');
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Menu</title>
</head>
<body>
  <h1>Today's menu</h1>
  <ul id="menu"><li id="placeholder">Loading the menu...</li></ul>
  <template id="dish"><li class="dish"><span class="name"></span> <span class="price"></span></li></template>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).

Run the checks (needs learnrun.js in the same folder):

npm install jsdom@28.1.0
node --test
Download learnrun.js

Common mistakes

Filling the template copy after inserting it

const template = document.querySelector('#row');
const copy = document.importNode(template.content, true);
document.querySelector('#list').append(copy);
copy.querySelector('.name').textContent = 'eggs';

What Node.js prints

TypeError: Cannot set properties of null (setting 'textContent')

Why, and the fix

The copy of template.content is a DocumentFragment. Appending it moves its nodes into the list and leaves the fragment empty, so copy.querySelector('.name') finds nothing and returns null. Fill the copy first, then append it. Firefox and Safari word this message differently.

Passing text to appendChild

const list = document.querySelector('#list');
list.appendChild('eggs');

What Node.js prints

TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.

Why, and the fix

appendChild, the older method in many tutorials, accepts only a node. A string has to become an element first: create an li, set its textContent to "eggs", and insert that. append accepts strings, but list.append("eggs") would put bare text into the ul, not a new item.

Looping over children with forEach

const list = document.querySelector('#list');
list.children.forEach((li) => li.remove());

What Node.js prints

TypeError: list.children.forEach is not a function

Why, and the fix

element.children is an HTMLCollection, which has length and indexes but no forEach or map. To empty a list, call list.replaceChildren() with no arguments. To loop over the children, spread them first: [...list.children].forEach(…), or use for...of.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Create, fill, insert

document.createElement("li") makes a new element. It is not on the page yet: you fill it first, with textContent, classList and setAttribute, then insert it. parent.append(child) puts it after the last child, parent.prepend(child) before the first. Both take several arguments at once, and strings too, which become text. One element can be in one place only: appending an element that is already on the page moves it there, and a loop that appends the same element three times leaves one item. So create a new element for each item. The older appendChild, used in MDN's tutorial, takes one node and no strings.

Remove and replace

el.remove() takes an element out of the page. The variable still holds it, with its text, so you could insert it again. parent.replaceChildren(a, b) removes all the children of parent and puts a and b in their place; with no arguments it empties parent. That is the clean way to clear a list: no loop, no innerHTML = "". Be careful with the target: list.remove() removes the whole list, while list.replaceChildren() keeps the list and removes its items. element.children lists the child elements; like a NodeList it has no forEach or map, so spread it first: [...list.children].

Render from data

Keep the data in an array and write one function, render(), that builds every item from the array and puts them in with list.replaceChildren(...items). When the data changes, change the array and call render() again; the page then matches the data, with nothing duplicated. Editing single nodes by hand soon lets page and data disagree. For rows with more markup, write them once in a <template>, which the page does not show, and copy it with document.importNode(template.content, true). MDN recommends importNode so that custom elements inside the copy use the current document’s definitions. Fill the copy before you insert it. A DocumentFragment collects nodes off the page and inserts them in one step.

Sources

Last reviewed October 4, 2026