Write a function that turns a list of items into a DOM list.
This is the classic DOM exercise because it looks trivial and has a dozen small decisions inside it. The interviewer is watching how you build the tree, not whether you can call appendChild.
// A tiny stand-in for the browser DOM, so this example runs in Node.
const element = (tag, attrs = {}) => ({
tagName: tag.toUpperCase(),
attributes: attrs,
children: [],
textContent: "",
appendChild(child) { this.children.push(child); return child; },
});
const renderList = (items) => {
const list = element("ul", { class: "list" });
for (const item of items) {
const li = element("li", { "data-id": String(item.id) });
li.textContent = item.label;
list.appendChild(li);
}
return list;
};
const rendered = renderList([
{ id: 1, label: "Write a spec" },
{ id: 2, label: "Ship it" },
]);
console.log(rendered.tagName); // "UL"
console.log(rendered.children.length); // 2
console.log(rendered.children.map((li) => li.textContent));
// ["Write a spec", "Ship it"]
console.log(rendered.children[0].attributes["data-id"]); // "1"The decisions that matter. You build the whole tree before you attach it, so the browser does the layout work once rather than once per node. You set the text through the textContent property rather than through innerHTML, which is safe with any content and does not parse markup. And you put the identifier in a data attribute, so a click handler can find out which row was clicked without you having to search the text.
What an interviewer will ask next. What happens with an empty list, and the answer is that you either render an empty state message or leave the list empty, and you decide which. How you would update it when the data changes, and the answer is that you re-render and replace the children, or you diff them if the list is long. And how you would test it, which is where a function that takes data and returns an element rather than one that reaches into the document is much easier to test.