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 w…


This content originally appeared on DEV Community and was authored by Usman Suleiman

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>

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>

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

<script>
    let todos = []; // Subject
    let observers = [];
</script>

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);
}

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);
    });
}

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);

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());
}

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

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

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

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);
    });
}

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();
}

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)

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);

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));
    }
});

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,
    }
}

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 = "";
    });
});

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);
    }
}

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

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!


This content originally appeared on DEV Community and was authored by Usman Suleiman


Print Share Comment Cite Upload Translate Updates
APA

Usman Suleiman | Sciencx (2021-07-19T17:06:25+00:00) To-do list with Observer Pattern. Retrieved from https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/

MLA
" » To-do list with Observer Pattern." Usman Suleiman | Sciencx - Monday July 19, 2021, https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/
HARVARD
Usman Suleiman | Sciencx Monday July 19, 2021 » To-do list with Observer Pattern., viewed ,<https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/>
VANCOUVER
Usman Suleiman | Sciencx - » To-do list with Observer Pattern. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/
CHICAGO
" » To-do list with Observer Pattern." Usman Suleiman | Sciencx - Accessed . https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/
IEEE
" » To-do list with Observer Pattern." Usman Suleiman | Sciencx [Online]. Available: https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/. [Accessed: ]
rf:citation
» To-do list with Observer Pattern | Usman Suleiman | Sciencx | https://www.scien.cx/2021/07/19/to-do-list-with-observer-pattern/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.