Building a TODO App from Scratch — Step 3 — Basic JavaScript Functionality

We left off with a wireframe-y looking HTML-ized version of our design. Personally, I’m a little tempted to get some CSS on there, but let’s hold off and do the very basic JavaScript functionality first. That will make it, ya know, actually functional, and having some of the interactive stuff in place might inform some of our styling choices anyway.

Article Series

Where is the data going to go?

There are more ways and places to save data on the web than you can shake a stick at. Much like we slowed down at the beginning and did some design thinking at first, we might do well do do a little data thinking and consider the experience we’re trying to build.

For example, we might want to offer our TODO app for multiple users who log in to the website. If that’s the case, we’ll need to store the data in such a way that each user has their own set of to-dos associated with them. That would be fun! In that case we’d have to think about security and ensuring correct auth around data access. Maybe a bit much for a first crack though, so let’s table that for now.

Circling back to the front-end, it would be nice to get our data there ultimately as JSON. JSON is just so browser-friendly, as JavaScript can read it and loop over it and do stuff with it so easily. Not to mention built-in APIs.

That doesn’t mean we have to store data as JSON, but it’s not a terrible idea. We could use a more traditional database like MySQL, but then we’d need some kind of tool to help us return our data as JSON, like any decent ORM should be able to help with. SQLlite might be even better.

Or if we went with Postgres, it supports a better native json field type, and built-in utility functions like ROW_TO_JSON().

Still, just keeping the data as JSON sounds appealing. I’m not a huge data expert, but as I understand it there are JSON-first databases like MongoDB or CouchDB that might make perfect sense here. I’ve used Firebase a number of times before and their data storage is very JSON-like.

There are a million data storage options, and every data storage company wants your data. Just the way it is, kid.

Overwhelmed? Sorry. Let’s make like Liz Lemon and not overthink it. Let’s assume we’re going to use JSON data, and dunk that data as a string in localStorage. While this isn’t exactly a database, we can still nicely format our data, make it extensible, and pretend like we’re interfacing with a fancier database system.

What we’re giving up with localStorage is a user being able to access their data from anywhere. If they so much as open our TODO app in another browser, their data isn’t there. Let alone open the site on their phone or the like. Or do something blasphemous like clear their browser data. So it’s rudimentary, but it’s still “real” enough data storage for now.

What is the data going to look like?

JSON can be an {} Object or [] Array. I’m thinking Array here, because arrays have this natural sense of order. We talked about being able to re-arrange to-do items at some point, and the position in the array could be all we need for that.

As best I know, as of ES2020, I believe Objects maintain order when you iterate over them in all the various ways. But I don’t think there is any reasonable way to change that order easily, without creating a brand new Object and re-inserting things in the newly desired order. Or, we’d have to keep an order value for each item, then update that as needed and always iterate based on that value. Sounds like too much work.

So…

[
  { /* to-do item */ },
  { /* to-do item */ }
]Code language: JSON / JSON with Comments (json)

(Astute readers will note that JSON doesn’t actually have comments, unless we fix it somehow.)

If we were supporting a multi-user system, perhaps all the data would be an object with each user ID being a key and the data being an array like above. One way or another, one use per Array.

Now what does each item look like? Based on our design, we really only need a few things:

  1. Title (text of the to-do)
  2. Complete (whether it is done or not)

We could decide that we don’t need “complete” because we’ll just delete it. But any to-do app worth it’s salt will be able to show you a list of completed items, and you can always delete them from there.

We talked about using the array order for the visual order of the to-dos. I think I’m fine with that for now, knowing we can always add ordering properties if we really feel like it. In fact, we can add whatever. We could add dates like a “created at”, “modified at”, “due date”, or “completed at” if we felt it would be useful. We could add tags. We could add a description. But these kind of things should be design and UX driven, as we well know by now. Don’t just go adding data speculatively, that tends to not end well.

When it comes to updating/deleting existing to-dos, we’re going to need a way to update just that item in the data. That’s why I brought up the array order again, because theoretically we could know which item we’re dealing with by the DOM order, then match that to the array order to find the right item. But something about that feels janky to me. I’d rather have a more solid-feeling one-to-one connection between the UI and the data. Maybe it’s just me, but it feels better. So let’s add a unique identifier to each todo.

So our now-list-of-three will look like this in the data:

[
  {
     title: "Walk the dog", // string
     completed: false,      // boolean
     id: "something-unique" // string
  },
  {
    // more! 
  }
]Code language: JSON / JSON with Comments (json)

Seems workable and extensible to me!

Writing data

There is one way to add a new item on our site: submitting the form. So if we get ahold of that form in JavaScript and watch for the submit event, we’re in business.

const form = document.querySelector("#todo-form");

form.addEventListener("submit", (event) => {
  event.preventDefault();

  // Add to-do to data and render UI

  form.reset();
});Code language: JavaScript (javascript)

Two little tricks there.

  1. The preventDefault is because we’re handling the submission in JavaScript so we’re preventing the browser from trying to perform the default action of going to a new page.
  2. The reset bit is a nice little built-in UI for resetting the fields, so we don’t have to manually clear them ourselves. After we add a to-do, we don’t want that same text just sitting there in the input, it should go back to blank.

We talked about all our data being one big JSON-able Array. So:

let TODOs = [];Code language: JavaScript (javascript)

Then we can push into that array with the new data. We know the complete value will be false (we just added it!) and the title will be from the input.

TODOs.push({
  title: event.target[0].value,
  complete: false,
  id: self.crypto.randomUUID()
});Code language: CSS (css)

That last bit is the browser giving us a unique identifier for free! We could used the package for them, but we just don’t need to anymore. UUID’s are cool. There is a practically-zero chance of ever getting a duplicate ever. Wikipedia:

… only after generating 1 billion UUIDs every second for approximately 100 years would the probability of creating a single duplicate reach 50%.

We’ve decided we’re just going to keep the data in localStorage for now, so after we’ve updated our TODOs Array, let’s dump it there.

localStorage["data"] = JSON.stringify(TODOs);Code language: JavaScript (javascript)

Uh, that was easy.

Now that we’ve added the data, we know we’ll need to re-render the UI. But we’ll make a function for that in the next section, as obviously we’ll need to render the UI when we read the data when the page loads as well.

Reading data

Getting the data out of localStorage is just as easy as writing to it: localStorage["data"]. That’ll have our JSON data in it. Probably. If we’ve written to it before. Just to be sure, let’s check before we parse out the data.

let TODOs = [];

if (localStorage["data"] !== null && localStorage["data"] !== undefined) {
  TODOs = JSON.parse(localStorage["data"]);
}Code language: JavaScript (javascript)

Just doing that once when the page is loaded will ensure our TODOs variable is loaded with what we got.

Now we need to render the UI. We already figured out we need to do this in several situations:

  1. When the page loads
  2. When we add a new to-do
  3. When we complete a to-do

So let’s write it as a function so we can call it in all those situations.

const list = document.querySelector("#todo-list");

function buildUI() {
  let HTML = ``;
  TODOs.forEach((todo) => {
    HTML += `
      <li id="${todo.id}">
       ${todo.title}
       <button aria-label="Complete" class="button-complete">
         <svg class="svg-check"><path d="..." /></svg>
       </button>
      </li>`;
  });
  list.innerHTML = HTML;
}Code language: JavaScript (javascript)

We knows TODOs is an Array, so we loop over it, creating one big string of HTML with all the <li>s we’ll populate the <ol> with. (We’ll monkey with that SVG later.)

I feel like the native JavaScript Template Literal is a good fit here. That’s the string within backticks (`). This allows us to write multi-line strings and interpolate variables inside. This is a place where there is lots of choice though! We could have used a native HTML <template> here, and perhaps we will in the future. We could have used a Handlebars template or the like. If you’re used to using a JavaScript framework, this is essentially a component and essentially the heart of whatever framework it is. Like the JSX of React.

The downsides of a Template Literal is that you don’t get syntax highlighting usually. It’s not going to be linted or checked like your other HTML. Still, I like how simple the Template Literal is here, let’s keep it.

Completing a to-do

In the HTML for each of our to-dos, remember we have a <button> designed for clicking to complete a to-do. But they don’t have click event handlers on them yet. We could put an onclick handler as an attribute right on them. That’s not the world’s worst idea, since they would automatically have interactivity applied to them the second they hit the DOM.

Just go another way though: event delegation. We can just watch for clicks on the whole document, and if the event originated on that kind of button, then we can do our work.

document.documentElement.addEventListener("click", (event) => {
  if (event.target.classList.contains("button-complete")) {
    // Click happened on a Complete button
  }
});Code language: JavaScript (javascript)

There is a little gotcha here though! We have an <svg> in our button, and it’s possible/likely the user clicks directly on that, so event.target will be that and not the <button>. So a smidge of CSS will help us:

.svg-check {
  display: block; /* prevent weird line-height issue */
  pointer-events: none; /* stop click events from being target */
}Code language: CSS (css)

Now we need to do the actual work. We can figure out exactly which to-do this is by the unique ID that we gave the <li> element. That will match the ID in our data. So we look through our data and find that ID, mark it as complete, and put the data back.

document.documentElement.addEventListener("click", (event) => {
  if (event.target.classList.contains("button-complete")) {
    TODOs = TODOs.filter((todo) => todo.id !== event.target.parentElement.id);
    localStorage["data"] = JSON.stringify(TODOs);
    buildUI();
  }
});Code language: JavaScript (javascript)

That filter function is instantly removing the to-do with a matching ID from the data. Ultimately our plan is to update the complete value in our data, so that we can show a list of completed to-dos. But we’ve done a lot today already, so let’s revisit that when we do more with JavaScript. We’ve still got editing to do and such.

This is a little akwardness of the localStorage setup we have. Every time we touch our data, we rewrite the entire set of data each time. When you’re working with a “real” database, don’t download the entire database and replace the entire database when small changes are made, that would just be silly. But our data is so small/light here, even if there were a few hundred to-dos, it’s not that big of a deal. But certainly a real database is a cleaner and more scalable approach. We could have also architected things differently, making each to-do a unique key in localStorage, but that didn’t have the Array characteristic we wanted, and just feels kinda sloppy to me.

See that we’re calling our buildUI() function after making the data change as well, ensuring our UI is in sync with our data.

So Far

Article Series

It's time to take your JavaScript to the next level

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.