DEV Community

Usman Suleiman
Usman Suleiman

Posted on

To-do list with Observer Pattern

In this post, we'll be learning about the Observer Pattern by creating a simple To-do application.

In a nutshell, the Observer Pattern is similar to Twitter's Followers feature. When you post a tweet, all your followers get notified, and they decide whether to read your tweet or not. We can say our Followers are observing our tweets.

The Observer Pattern has only two components. The Subject and the Observers. The Observers only want to know when we update the Subject. They don't care when it happens.

Going back to our Twitter analogy, our Tweet is the Subject, while our Followers are the Observers.

So, how does it relate to our Todo list application? We'll uncover the answer while we build the app, but first, we need to know the features of our app.

  • We want to be able to add a unique to-do to our list
  • We want to be able to remove a to-do from our list
  • We want to persist our list on page reload

Let's create the HTML of our Todo app.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Observer Pattern</title>
    </head>
    <body>
        <ul></ul>
        <form>
            <input required type="text" />
            <button type="submit">Add</button>
        </form>
    </body>
    <script>
    // We'll add all our code here
    </script>
</html>
Enter fullscreen mode Exit fullscreen mode

In this HTML, we have an unordered list element which will hold our todo items, a form element to add a todo item to our list, and finally a script element to hold our JavaScript code.

The Subject will be our todo items. So we create an array list to store our todos.

<script>
    let todos = []; // Subject
</script>
Enter fullscreen mode Exit fullscreen mode

Next we create a list of Observers. (Functions that will make use of the list).

<script>
    let todos = []; // Subject
    let observers = [];
</script>
Enter fullscreen mode Exit fullscreen mode

Then, we implement the add todo functionality. Each todo needs to be uniquely identified, so assign each item with an ID.

const form = document.querySelector("form");
form.addEventListener('submit', (event) => {
    event.preventDefault();
    const input = form.elements[0];
    const item = {
        id: Date.now(),
        description: input.value,
    };
    addTodo(item);
    input.value = ''; // Clear text input
});

function addTodo(item) {
    todos.push(item);
}
Enter fullscreen mode Exit fullscreen mode

Introducing our first observer

When you try running the app, you'll notice nothing is being displayed on screen. That's because we haven't hooked up our todos array to our HTML unordered list element.

Our HTML ul element is interested in our todos array. It wants to observe our array list so that it can display it on the screen. So it wants to be an Observer. Let's implement a function that will display our list.

function displayTodos() {
    const ul = document.querySelector('ul');
    todos.forEach((todo) => {
        const li = document.createElement('li');
        li.innerText = todo.description;
        ul.appendChild(li);
    });
}
Enter fullscreen mode Exit fullscreen mode

Now, we register this function as an Observer by adding it to our list of observers. To do that we create a helper function to register new observers.

function registerObserver(observer) {
    // The observers array is basically an array of functions
    observers.push(observer);
}

registerObserver(displayTodos);
Enter fullscreen mode Exit fullscreen mode

Despite registering as an observer, nothing is being displayed. That's because our todos array hasn't notified the observers.
We create a notifyObservers function that will loop through our observers array and call each observer function to know an update has happened.

function notifyObservers() {
    observers.forEach((observer) => observer());
}
Enter fullscreen mode Exit fullscreen mode

Then, we call the notifyObservers function whenever we change the Subject.

function addTodo(item) {
    todos.push(item);
    notifyObservers(); // Add this line
}
Enter fullscreen mode Exit fullscreen mode

Now, run the app in your browser and see your todos being added to the list.

Congratulations on your first bug 🥳

You've probably noticed that our list doubles every time we add a new item. We can fix that by clearing it first.

// Inside the displayTodos function

function displayTodos() {
    const ul = document.querySelector('ul');
    ul.innerHTML = ''; // Add this line
Enter fullscreen mode Exit fullscreen mode

Now that we have "add" functionality working, it's time to remove todos. First, we add a remove button to every li element.

function displayTodos() {
    const ul = document.querySelector('ul');
    ul.innerHTML = '';
    todos.forEach((todo) => {
        const li = document.createElement('li');
        li.innerText = todo.description;

        // Add these lines
        const button = document.createElement('button');
        button.innerText = 'Remove';
        li.appendChild(button);

        ul.appendChild(li);
    });
}
Enter fullscreen mode Exit fullscreen mode

Then, we create a removeTodo function that will handle removing to-dos by their ID.

function removeTodo(id) {
    todos = todos.filter((todo) => todo.id !== id);
    notifyObservers();
}
Enter fullscreen mode Exit fullscreen mode

Then we attach a click event listener to the remove button, that will call the removeTodo function.

// Inside the displayTodos function

const button = document.createElement('button');
button.innerText = 'Remove';
// Attach an event listener here
button.addEventListener('click', () => {
   removeTodo(todo.id);
});
li.appendChild(button)
Enter fullscreen mode Exit fullscreen mode

Introducing the second observer

The final step is to save our list in local storage and load it when we reload the page. We want our local storage to be an observer, and save the list whenever it is notified.

function persistData() {
    localStorage.setItem("saved-todos", JSON.stringify(todos));
}

registerObserver(persistData);
Enter fullscreen mode Exit fullscreen mode

Then, we load the saved todos on page load.

function loadTodos(todoList) {
    todos = todoList;
    notifyObservers();
}

window.addEventListener("load", () => {
    const savedTodos = localStorage.getItem("saved-todos");
    if (savedTodos) {
        loadTodos(JSON.parse(savedTodos));
    }
});
Enter fullscreen mode Exit fullscreen mode

The Clean Code

Our code is working. It meets the minimum requirements, but it's not elegant. If you follow closely, you'll notice there are 2 kinds of code. Those that manipulate the unordered list element and those that manipulate the todos array list. We are mixing UI logic and State logic, which is an attribute of a messy code.

Let us start by wrapping our state logic in a function and exposing the register, add, remove, and load functions as methods to an object. This is called Abstraction.
Our todos array is no longer visible to the UI logic code. So we create the getTodos method for accessing the todos. This is called Encapsulation. The art of hiding internal state and exposing it via a method.

function createSubject() {
    let todos = [];
    let observers = [];

    function registerObserver(observer) {
      observers.push(observer);
    }

    function notifyObservers() {
      observers.forEach((observer) => observer());
    }

    function addTodo(item) {
        todos.push(item);
        notifyObservers();
    }

    function removeTodo(id) {
      todos = todos.filter((todo) => todo.id !== id);
      notifyObservers();
    }

    function loadTodos(todoList) {
        todos = todoList;
        notifyObservers();
    }

    function getState() {
        return todos;
    }

    return {
        registerObserver,
        addTodo,
        removeTodo,
        loadTodos,
        getState,
    }
}
Enter fullscreen mode Exit fullscreen mode

Next we use the createSubject to create a todos subject.

const subject = createSubject();

function displayTodos() {
    const ul = document.querySelector("ul");
    ul.innerHTML = "";
    todos.forEach((todo) => {
        const li = document.createElement("li");
        li.innerText = todo.description;

        const button = document.createElement("button");
        button.innerText = "Remove";
        button.addEventListener("click", () => {
            subject.removeTodo(todo.id);
        });
        li.appendChild(button);

        ul.appendChild(li);
    });
}

subject.registerObserver(displayTodos)

subject.registerObserver(() => {
    localStorage.setItem("saved-todos", JSON.stringify(todos));
});

window.addEventListener("load", () => {
    const savedTodos = localStorage.getItem("saved-todos");
    if (savedTodos) {
        subject.loadTodos(JSON.parse(savedTodos));
    }

    const form = document.querySelector("form");
    form.addEventListener("submit", (event) => {
        event.preventDefault();
        const input = form.elements[0];
        const item = {
            id: Date.now(),
            description: input.value,
        };
        subject.addTodo(item);
        input.value = "";
    });
});
Enter fullscreen mode Exit fullscreen mode

The createSubject function adheres to the Observer Pattern design. We subscribe to the todos by registering as observers. What about if we no longer want to be notified?
It's quite simple. We can return a function in the registerObserver method.

function registerObserver(observer) {
    observers.push(observer);

    return function () {
        observers = observers.filter((currentObserver) => !== observer);
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, we can save the return value after registering and call it later to unregister.

const unregisterDisplayTodos = subject.registerObserver(displayTodos)

// later when we want to unregister
unregisterDisplayTodos(); // displayTodos will no longer be notified
Enter fullscreen mode Exit fullscreen mode

FIN

Redux is a popular JavaScript library that uses the Observer Pattern. In the next post, we'll demystify redux by creating our own small redux library.

Happy Coding!

Top comments (4)

Collapse
 
nhan1303 profile image
nhan1303

Nicely done, really really love to reading posts that make me understand the mechanism (under the hook) like this one. Thank you again!

Collapse
 
donubwise profile image
U.M.NDOH

Nicely Done, Thanks for sharing and also i"m waiting for the next.

Collapse
 
devusman profile image
Usman Suleiman

Thank you! I'm glad you liked it.

Collapse
 
nuralam profile image
nuralam

Thanks for sharing 😊, waiting for next post about "demystify redux by creating our own small redux library"