[WIP] Building a JS Framework from scratch
2023/05/16
In this article we will build a small reactive js framework from scratch

Building a JS Framework from Scratch

Warning: This article is still unfinished and will keep changing for a while

I want to get better at writing and explaining technical things so in this article I will try to describe how to build a small JS framework from scratch based on a small project I made.

The Problem

Let’s first see what’s the problem and what we are trying to build. The browser gives us the Document Object Model (DOM) that is a tree that represents the tree of elements we describe in plain HTML.

So if we want to interact with HTML from JS we have to use the DOM. Every HTML element get many methods to add, remove and update elements.

For example to change the text content of an element we can use (Node).textContent. We may want to create new elements in JS, to do so we can create them using document.createElement(tagName) or if we want to set the HTML directly we can use (Element).innerHTML like myDiv.innerHTML = <p>Hello!</p>.

Postulate: Interacting with the DOM is slow

Given this postulate the main job of a UI framework should be to try and minimize the interactions with the DOM to keep the state of our application in sync with the state of the DOM while not wasting too much time doing this exact job.

Vanilla JS

Now let’s consider we are making our beloved “TODO list application”, we want

For now let’s just hack this together just trying to make something that works.

<label>
    New Todo 
    <input type="text" class="new-todo-field">
    <button class="new-todo-btn">Add</button>
</label>
<br>
<label>
    Hide Done
    <input type="checkbox" class="toggle-done-todos">
</label> 
<div class="todo-list"></div>
.todo-list {
    display: flex;
    flex-direction: column;
    padding: 0.5rem;
    gap: 0.25rem;
}

.todo-item {
    display: flex;
    gap: 0.5rem;
}

/* Styles to hide completed todo items when the checkbox is checked */
.todo-list.hide-done .todo-item.done {
    display: none;
}

/* Styles to mark completed todo items */
.todo-item.done .text {
    text-decoration: line-through;
}

/* Styles to show/hide elements only needed while editing */
.todo-item.editing .hide-on-edit {
    display: none;
}
.todo-item:not(.editing) .edit-field {
    display: none;
}

As a convention I name HTML elements with a leading $ to not go mad. Let’s first define a couple of variables to define all the root elements we are going to use.

const $newTodoField = document.querySelector('.new-todo-field')
const $newTodoBtn = document.querySelector('.new-todo-btn')
const $toggleDoneTodos = document.querySelector('.toggle-done-todos')
const $todoList = document.querySelector('.todo-list')

Now let’s define a function to create a new todo item given its text. We will use this later.

function createTodoItem(text) {
    const $todoItem = document.createElement("div");
    $todoItem.classList.add("todo-item");

    const $todoItemDone = document.createElement("input");
    $todoItemDone.type = "checkbox";
    $todoItemDone.classList.add("hide-on-edit");
    $todoItemDone.addEventListener("input", (e) => {
        $todoItem.classList.toggle("done", $todoItemDone.checked);
    });

    const $todoItemText = document.createElement("div");
    $todoItemText.classList.add("text");
    $todoItemText.classList.add("hide-on-edit");
    $todoItemText.textContent = text;

    const $todoItemEditField = document.createElement("input");
    $todoItemEditField.classList.add("edit-field");
    $todoItemEditField.type = "text";
    $todoItemEditField.value = text;

    const $todoItemRemove = document.createElement("button");
    $todoItemRemove.textContent = "Delete";
    $todoItemRemove.classList.add("hide-on-edit");
    $todoItemRemove.addEventListener("click", () => {
        $todoItem.remove();
    });

    const $todoItemEditBtn = document.createElement("button");
    $todoItemEditBtn.textContent = "Edit";
    $todoItemEditBtn.addEventListener("click", () => {
        if (!$todoItem.classList.contains("editing")) {
            $todoItem.classList.add("editing");
            $todoItemEditBtn.textContent = "Done";
        } else {
            $todoItemText.textContent = $todoItemEditField.value;
            $todoItem.classList.remove("editing");
            $todoItemEditBtn.textContent = "Edit";
        }
    });

    $todoItem.appendChild($todoItemDone);
    $todoItem.appendChild($todoItemText);
    $todoItem.appendChild($todoItemEditField);
    $todoItem.appendChild($todoItemEditBtn);
    $todoItem.appendChild($todoItemRemove);

    return $todoItem;
}

This function is just factored out because it gets called when clicking the “Add” button or when pressing Enter from the keyboard while typing in the input field.

function addTodo() {
    const newTodoText = $newTodoField.value;
    if (newTodoText.trim().length === 0) return;

    const $newTodoItem = createTodoItem(newTodoText);

    $todoList.appendChild($newTodoItem);

    $newTodoField.value = "";
}

$newTodoField.addEventListener("keydown", (e) => {
    if (e.key === "Enter") addTodo();
});

$newTodoBtn.addEventListener("click", addTodo);

As a last thing we add a listener for the checkbox that hides all items. Instead of removing items from the DOM we just add a class to the enclosing container.

$toggleDoneTodos.addEventListener("input", () => {
    $todoList.classList.toggle("hide-done", $toggleDoneTodos.checked);
});

And here it is in action

This code has many problems, first its long. We are just creating a couple of items and yet the largest part of the code handles just creating the following elements that in HTML can be represented in just 7 lines.

<div class="todo-item">
    <input type="checkbox" class="hide-on-edit">
    <div class="text hide-on-edit">[Todo item text]</div>
    <input class="edit-field" type="text">
    <button>Edit</button>
    <button class="hide-on-edit">Delete</button>
</div>

Second its all imperative code and we are mutating elements in place so we have to keep track of everything. This might work for very small JS widgets but its unreasonable to keep modifying classes and element attributes directly like this.

Another problem is what if we want to send/receive data from a server. We could send HTML directly over the wire and just use .innerHTML to update elements. On the other hand most just use JSON as its really easy to work with in JS.

[…]


There are mostly three ways people “solved” UIs in the last decades