Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Sat, 09 Mar 2024 00:23:53 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.2 225069128 Building a TODO App from Scratch — Step 5 — Extra Functionality https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-5-extra-functionality/ https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-5-extra-functionality/#respond Sat, 09 Mar 2024 00:23:53 +0000 https://frontendmasters.com/blog/?p=1169 We left off in a decent place, but there was some missing functionality that we wanted all along.

  1. We need to be able to view completed to-dos (and be able to delete them entirely)
  2. We need to be able edit to-dos.

Article Series

Editing To-Dos

The interaction we decided on for editing is to double-click the todo. This will turn the to-do, in place, into an editable input. Then you hit enter (e.g. submit) or leave the input (e.g. blur) and it will save the changes.

We can use a bit of event delegation to set up this event:

list.addEventListener("dblclick", (event) => {
  const listItem = event.target.closest("li");

  // If already editing, let it be.
  if (listItem.classList.contains("editing")) return;

  listItem.classList.add("editing");

  // Do editing.
});

Now anywhere you double-click on the list will set the relevant list item into “editing mode”, here indicated by just adding a class we can style against, like hide the existing text.

More importantly, we need to insert some new HTML turning the text into an editable input. We can use a template literal of a <form> to inject as needed, like so:

list.addEventListener("dblclick", (event) => {
  const listItem = event.target.closest("li");

  // If already editing, let it be.
  if (listItem.classList.contains("editing")) return;

  listItem.classList.add("editing");
  const textItem = listItem.querySelector(".text");
  listItem.insertAdjacentHTML(
    "beforeend",
    `<form onsubmit="updateTodo(event);" class="form-edit">
       <input onblur="updateTodo(event);" type="text" class="input-edit" value="${textItem.textContent}">
     </form>`
  );
});

That calls an updateTodo() event we’ll have to write. But first, let’s make sure we focus the input and put the cursor at the end. Just a bit of nice UX right?

list.addEventListener("dblclick", (event) => {
  const listItem = event.target.closest("li");

  // If already editing, let it be.
  if (listItem.classList.contains("editing")) return;

  listItem.classList.add("editing");
  const textItem = listItem.querySelector(".text");
  listItem.insertAdjacentHTML(
    "beforeend",
    `<form onsubmit="updateTodo(event);" class="form-edit"><input onblur="updateTodo(event);" type="text" class="input-edit" value="${textItem.textContent}"></form>`
  );

  const input = listItem.querySelector(".input-edit");
  input.focus();

  // put cursor at end of input
  input.setSelectionRange(input.value.length, input.value.length);
});

Updating the to-do is pretty straightforward. We get our hands on the new text, update it in the DOM, toggle the editing class, and write the data back to localStorage. It looks like a lot of lines, but a lot of it is just getting our hands on DOM elements and basic manipulation.

function updateTodo(event) {
  event.preventDefault();
  const listItem = event.target.closest("li");
  const textItem = listItem.querySelector(".text");
  const inputItem = listItem.querySelector(".input-edit");
  const form = listItem.querySelector(".form-edit");
  textItem.textContent = inputItem.value;
  listItem.classList.remove("editing");
  form.remove();
  TODOs = TODOs.map((todo) => {
    if (todo.id === listItem.id) {
      todo.title = inputItem.value;
    }
    return todo;
  });
  localStorage["data"] = JSON.stringify(TODOs);
}

And it works!

Viewing Completed To-Dos

Before this, we could delete a to-do, but that was it. Even though our data structure was set up to have a complete attribute that could change, all we did was filter out the completed ones entirely from the data.

Here’s what that data structure is like:

{
  title: `text of to-do`,
  complete: false,
  id: self.crypto.randomUUID()
}

Now, when we check that checkbox in the UI to complete a to-do, we need to:

  1. Set complete to true if the list item is in the active list
  2. Remove the list item entirely if the to-do is already in the completed list

We’ll update our function to be called toggleTodo and do it like this:

function toggleTodo(event) {
  const listItem = event.target.parentElement;
  // Trigger complete animation
  listItem.classList.toggle("complete");
  setTimeout(() => {
    // list item is already complete, remove it
    if (listItem.dataset.complete === "true") {
      TODOs = TODOs.filter((todo) => !todo.complete);
    } else {
      // list item is just being set to complete now
      TODOs.forEach((todo) => {
        if (todo.id === listItem.id) {
          todo.complete = !todo.complete;
        }
      });
    }

    localStorage["data"] = JSON.stringify(TODOs);

    if (!document.startViewTransition) {
      buildUI();
    } else {
      document.startViewTransition(() => {
        buildUI();
      });
    }
  }, 1000);
}

This isn’t a big change from last time, just one little fork in the logic that either removes it or updates the data.

Now we need a control though to decide if we’re looking at the active to-dos or the completed ones. Let’s make that control in HTML:

<div class="todo-type-toggles">
  <button aria-pressed="true">Active</button>
  <button>Completed</button>
</div>

Now when you press the buttons, we’ll swap the state and re-build the UI accordingly:

const toggles = document.querySelectorAll(".todo-type-toggles > button");

toggles.forEach((toggle) => {
  toggle.addEventListener("click", (event) => {
    toggles.forEach((toggle) => {
      toggle.setAttribute("aria-pressed", false);
    });
    toggle.setAttribute("aria-pressed", true);

    if (toggle.textContent === states.ACTIVE) {
      buildUI(states.ACTIVE);
    } else {
      buildUI(states.COMPLETED);
    }
  });
});

Now I’m calling buildUI() with a param to declare which type I want to see. I like using little ENUM type variables for this just to make sure we don’t do typos.

const states = {
  ACTIVE: "Active",
  COMPLETED: "Completed"
};

Then we update the function to display one or the other…

function buildUI(state) {
  let HTML = ``;
  let viewTODOs = [];

  if (state === states.COMPLETED) {
    viewTODOs = TODOs.filter((todo) => todo.complete);
  } else {
    viewTODOs = TODOs.filter((todo) => !todo.complete);
  }

  if (viewTODOs.length === 0) {
    HTML = `<li class="empty">Nothing to do!</li>`;
  }

  // Loop over the viewTODOs and build HTML to insert, exactly as before.
}

This gives us an empty state as well.

And we’ve done it!

Perhaps more back-endy readers will be like “is this dude not sanitizing data before it goes to data storage?” and that would be a smart observation. We should probably be sanitizing the HTML. But for now, the only person you can pwn with this is yourself, so not a massive deal.

Here’s where we’ve gotten now:

Article Series

]]>
https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-5-extra-functionality/feed/ 0 1169
Building a TODO App from Scratch — Step 4 — Styling & Interactive Choices https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-4-styling-interactive-choices/ https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-4-styling-interactive-choices/#respond Wed, 28 Feb 2024 21:17:43 +0000 https://frontendmasters.com/blog/?p=1020 I’m already a little over all the green. What was I thinking? I’m much more of a nice gray color palette kinda guy with some fun poppy colors. That’s what we need to get to here. When we left off, we just had wireframes going. It’s time to get some style on this thing!

Article Series

Custom Properties

We can get rid of that wireframe look by removing all those 1px borders and applying some backgrounds. Let’s give ourselves some Custom Properties to work with, so we can apply colors in such a way they are easy to change later.

html {
  --gray800: oklch(10% 0% 0);
  --gray600: oklch(40% 0% 0);
  --gray100: oklch(92% 0% 0);
  --brand: oklch(85% 0.3 145);

  background: var(--gray100);
}

You can see we’re using the lightest gray right away in colorizing the background.

I’m using OKLCH color function here because I find it the generally-nicest of all the color models. One very immediate thing it does for us here is that it unlocks P3 colors. That --brand color you see there is actually a bit more bright and vibrant than is even possible in the sRGB color space (like a hex code or any of the more common color functions you may be familiar with, like rgb()).

Applying those colors to the relevant sections gets us a start:

Fonts

We don’t need to go too far down the typography rabbit hole here, as all our design calls for is the header, one input, and a list of to-do items. Keeping it simple and fast (not loading fonts is… fast), is ideal, so:

html {
  font-family: system-ui, sans-serif;
}

You’d think system-ui would be boring, but as a little fun and progressive enhancement, we can use some font-variation-settings on it in the case that the font that loads supports them. It’s not the only one, but Apple’s San Francisco font ships as a variable font, so we can do fun things like stretch it out.

header {
  background: var(--gray800);
  color: white;
  h1 {
    font-variation-settings: "wght" 900, "wdth" 700;
  }
}

I love this look, myself:

SVGs

We need SVG’s for the “check” of the logo and that we’ll use when a user checks off a to-do. I’m kinda thinking we just re-use the same one for both as that just makes good sense. Plus we’ll make a “+” SVG too. It might be tempting to find unicode characters for these, but I’m telling you, just use SVG. The predictable bounding box of SVG will be your friend (no fighting against weird space).

There are loads of resources for finding SVGs to use. I’m usually a Noun Project guy, but if we needed, say, 10 or more icons I’d probably try to find a more cohesive set somewhere. These are also so simple that could (should) draw them ourselves (even directly in code), but we’ll save that for another day.

Ultimately, we’ll just get our hands on a “check” icon and use it in the logo…

<header>
  <svg ... class="svg-check site-logo">
    <polyline ... />
  </svg>
  <h1>TODOs</h1>
</header>

… in our template literal for each to-do …

HTML += `<li id="${todo.id}">
  ${todo.title}
  <button aria-label="Complete" class="button-complete">
    <svg ... class="svg-check">
      <polyline ... />
    </svg>
  </button>
</li>`;

… and then our “+” in the form:

<form id="todo-form" class="todo-form">
  <label>
    <span class="screen-reader-text">New TODO</span>
    <input type="text" name="todo">
  </label>
  <button id="button-add-todo" class="button-add-todo">
    <svg ... class="svg-plus">
      <g>
        <line ... />
        <line ... />
      </g>
    </svg>
  </button>
</form>

There is a lot to know about SVG so I’m really just dropping the basic usage in here. I’ve chosen to drop the SVG code right “inline” with the HTML because I generally think that’s best because it’s fast and offers the most styling control. If you wanna dig more into SVG, I did write a book about it one time you could check out.

With the SVG’s I chose in there:

Adding To-Do Interactions

When we add a new to-do, I want to give the adding button a temporary class so we can do something fun with it. So we’ll use a saved reference to that button and do that:

buttonAddTodo.classList.add("added");

But this is an opportunity! We could add a different class upon a failure. What failure? Not entering any text, for one. We could make the call not to allow entering blank to-dos, which at this point I think is a decent idea.

So the form event handler then becomes like:

form.addEventListener("submit", (event) => {
  event.preventDefault();
  // Don't allow empty todo
  if (!form[0].value) {
    buttonAddTodo.classList.add("shake");
    return;
  }
  addTodo(event);
  form.reset();
});

Now check out these little @keyframe animations I can do with those classes:

.button-add-todo {
  ...

  &.shake {
    rotate: 0deg;
    transform-origin: bottom right;
    animation: shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  }
  &.added {
    transform-origin: center center;
    animation: added 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  }
}
@keyframes shake {
  50% {
    rotate: -12deg;
  }
}
@keyframes added {
  50% {
    rotate: 1turn;
    translate: 0 50px;
  }
}

A little fun, maybe?

Let’s be careful to remove those classes after the animation happens, so that another animation can be triggered freshly when needed:

buttonAddTodo.addEventListener("animationend", () => {
  buttonAddTodo.classList.remove("shake", "added");
});

Completing To-Do Interactions

We’ve got a big ol’ box sitting there waiting for to-do items to be checked. And now that we’re using SVG, my temptation is to do a little fun “line drawing” with that. That is, see the checkbox be “drawn” as we complete the to-do, before removing it.

Here’s the trick.

First we go find the SVG itself in our template literal, and add this special pathLength attribute to the actual shape:

<svg width="20" height="20" viewBox="0 0 241.44 259.83" class="svg-check">
  <polyline pathLength="1" points="16.17 148.63 72.17 225.63 225.17 11.63" />
</svg>

This just makes animating it much easier.

Then we set and animate some specific SVG properties to do the drawing:

.todo-list {
  ...
  li {
    ...
    .svg-check {
      opacity: 0;
    }
    &.complete {
      .svg-check {
        opacity: 1;
        stroke-dasharray: 1;
        stroke-dashoffset: 1;
        animation: do-check 1s infinite alternate;
      }
    }
  }
}

@keyframes do-check {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}

Check out the drawing effect at work:

The trick to getting the animation to run before being removed is just to … wait.

function removeTodo(event) {
  const listItem = event.target.parentElement;
  listItem.classList.toggle("complete");
  setTimeout(() => {
    TODOs = TODOs.filter((todo) => todo.id !== event.target.parentElement.id);
    localStorage["data"] = JSON.stringify(TODOs);
    buildUI();
  }, 1000);
}

You can see I’m just waiting for a second there, which is about the same, a little longer, than the animation itself. We could tie it to the animationend event here also, but for some reason it felt better to me to leave this bit of business logic in the code here rather than tying it to a CSS thing.

Really smoooooth completions

What if we remove a to-do from the middle of the list. It’ll be jerky won’t it? It certainly could be. This is one of those classic situations in CSS where being able to “animate to/from auto” would be nice. In this case, we want to animate from “however tall a list item is” to zero. That way the bottom items in the list would slide up, making a much more comfortable animation. But honestly, even animating the height here feels like slight of hand, what I actually want is just not to think very hard about it and for the list items to just slide up into place, if at all possible.

Turns out this is a lovely use-case for View Transitions. Without getting very deep at all into it, essentially all we need to do is:

  1. Give all our to-dos a unique view-transition-name
  2. When we update the DOM, e.g. buildUI(), wrap that in a startViewTransition() function.

So our template literal is updated to be like…

  TODOs.forEach((todo) => {
    HTML += `<li id="${todo.id}" style="view-transition-name: list-item-${todo.id};">

We already have a unique ID so we’ll steal that for the dual purpose here.

Then wrap our DOM updating function like:

if (!document.startViewTransition) {
  buildUI();
} else {
  document.startViewTransition(() => {
    buildUI();
  });
}

That’s the same and progressive-enhancement friendly way to do it. It’s kind of required here, as that startViewTransition will throw in browsers that don’t support it and it could break our whole site.

Look at us know:

This is friggin’ cool if you ask me.

So far

Just a little bit more functionality to go!

Article Series

]]>
https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-4-styling-interactive-choices/feed/ 0 1020
Building a TODO App from Scratch — Step 3 — Basic JavaScript Functionality https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-3-basic-javascript-functionality/ https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-3-basic-javascript-functionality/#respond Thu, 22 Feb 2024 16:39:19 +0000 https://frontendmasters.com/blog/?p=903 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 */ }
]

(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! 
  }
]

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

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 = [];

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

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

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

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

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

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 */
}

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

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

]]>
https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-3-basic-javascript-functionality/feed/ 0 903
Building a TODO App from Scratch — Step 2 — HTML https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-2-html/ https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-2-html/#respond Thu, 15 Feb 2024 22:28:39 +0000 https://frontendmasters.com/blog/?p=823 OK it’s HTML time! I love HTML time. We’re laying down a foundation that makes everything feel very real.

Article Series

Is HTML jumping the gun?

Nah.

The reason that we can jump to HTML right away is that it is common to any other future choice we make. As I mentioned, we might just stay in entirely vanilla land, but let’s say we chose a JavaScript framework instead. Those frameworks will likely all have their own slightly special takes on HTML.

  • If we pick Vue it might have funny attributes like @input, :value, or v-for
  • If we pick React we might be using JSX and have funny attributes like className or onClick and we might see .map() a lot.
  • If we pick Svelte we might see stuff like on:click and {#if} blocks.

Even if we go with native Web Components, we’ll see attributes you might not see every day like part and elements like <slot />.

But… it doesn’t really matter. All of them really need to start with solid semantic and accessible HTML, and no matter what other choices we make, we can acquiesce to their specific syntax.

The Parts

We can look at our very simple design and get a sense for the main parts of the HTML:

The Header

The header is just a decorative thing. Maybe something like:

<header>
  <svg ... />
  <h1>TODO</h1>
</header>

The <h1> is perhaps a little controversial. I often prefer “saving” that for whatever the main focus of the page is. But in this case since the name of the app is doing that for us, it kinda works. Otherwise a <div> or whatever is fine here.

The Form

The <form> feels vitally important to me. Already in poking around at other web-based to-do apps I’ve seen some inaccessible forms which feels sad to me for something so simple.

The trick is:

  1. Actually using a <form>.
  2. Making sure the <input> is properly <label>ed.
  3. Offering a submit <button>.
<form id="todo-form" class="todo-form">
  <label>
    <span class="screen-reader-text">New TODO</span>
    <input type="text" name="todo">
  </label>
  <button>Add</button>
</form>

I like having the id on the form for getting our hands on it in JavaScript like we’re likely going to want to. We can make the interactive event of adding a new to-do happen on the submit event. Plus that makes us ready for potentially adding more information into the form later if we wanted, like, say, a date picker for a deadline. The submit event will still work great for us, instead of something more specific like watching for the enter key being pressed while any specific input is focused.

I added the class for styling. That’s just a habit of mine, preferring to style with mostly classes for fairly flat specificity. If you have your own methods or use Tailwind or whatever, you likely already know what you’re doing and godspeed.

Notice my <label> doesn’t have a for attribute like you normally see, and that’s because the <input> is nested within it, which is a valid pattern. I like the pattern as it’s one less thing to screw up.

The actual text of the label we’re hiding, as-per the design. That’s also a little controversial, but I think is generally fine. It’s fairly clear what this form does, and if you were using a screen reader, the actual label text is still there even if we do hide it visually with CSS.

Last, the <button> is a lone wolf within the form, and that’ll make it be a submit button automatically. We could use an <input type="submit" /> as well, which is fine, but I like buttons. You can put more stuff in them and their type is implied in simple situations like this.

The List

The interesting thing about the list is that it’s going to be dynamically generated. We’ll have as many to-do list items as our data tells us. We can start with just the wrapper:

<ol id="todo-list" class="todo-list"></ol>

Again using the ol’ id/class one two punch there. I feel like an ordered list is appropriate somehow. Especially if we eventually add sorting of the list, then the order is very intentional and deserves that treatment. And the fact that we’re using a list at all feels right. Lists announce themselves as such in screen readers, including how many items are in the list, which may be a useful bit of information. That’s more than a bunch of <div>s would do!

The each list item becomes a <li>, but the extra stuff in there is going to beef it up. Let’s pause on the re-ordering and edit-ability for now and just focus on the item and the ability to complete it.

<li id="something-unique">
  <button aria-label="Complete">
    <svg ... />
  </button>
  To-do text
</li>

I figure each list item will have a unique ID on it. Actions taken on it will need to be unique to that item so that’ll be the main identifier. Maybe we don’t need it yet but we probably will. No class name here as I feel like selecting .todo-list > li is probably fine. We could always add it.

Our design makes the interactive element to complete a to-do look like a checkbox, and we could use an <input type="checkbox" />, but I’m kinda torn. This is where visual design and interactive design collide. We didn’t think out the interaction of completing a to-do all the way. What happens when you click that thing? Does it instantly get marked as done? Then disappear? Maybe it instantly gets marked as done but doesn’t disappear from the list. Or maybe there is like a 5-second countdown before it gets removed, so you have a chance to undo it in case it was a mistake.

I kinda like the delay idea, which to me leads me more toward the path of using an <input type="checkbox"> for that with the very obvious two states. We could really do it either way. If we keep the <button>, we’d just need to make sure to use an aria-pressed role for the two states accordingly.

Lastly, the position of that interactive element I have listed first here, before the text of the to-do, because that’s how it looks in the design. I could see the case being made though that in the HTML it should come after, just so that it reads nicer. Like as you tab through the list, you’d potentially hear the list item and then the button text offering to complete it. I’m not 100% sure what the right answer is there, so I think we’ll leave it first, because that’s the visual order, and I’ve heard it’s often best to match the source order and visual order.

Let’s stop there

With some very light wireframe-y CSS applied to those classes, we get this:

That’s a nice little foundation to layer on some styles and then get into the actual functionality. But aren’t you glad we have this strong foundation first? We could go anywhere from here.

Article Series

]]>
https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-2-html/feed/ 0 823
Building a TODO App from Scratch — Step 1 — Planning & Design https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-1-planning-design/ https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-1-planning-design/#respond Tue, 13 Feb 2024 00:19:10 +0000 https://frontendmasters.com/blog/?p=794 I find it fascinating how websites come to be. Especially websites with user interactivity and functionality, there are so many things to plan, build, and maintain, that it’s no wonder it easily makes for a whole career.

Perhaps one of the most cliché introductory things to build on the web (or otherwise), is a to-do app. But we’re going to do it again, as a website, and for good reason: it’s the perfect scope. A to-do app is fairly simple, adding and removing short bits of text forming a list. But it’s not too simple. It requires us to think about all sorts of things. While we’re ultimately going to care about coxre web development technologies like HTML & CSS, we’re going to choose to layer in JavaScript to help with user experience and data handling, and then see what other technology we might find helpful along the way, if any.

We’ll call our project TODO. Don’t sue me. It’s just for learning.

We’re not going to get fancy right off the bat.

And we’re going to do this thing as a series, so this list of posts will be updated as they are completed.

Article Series

Prior Art

Marc has already broached this subject on this very blog. Go check out the GitHub repo, where there is a lot of code to explore. Marc went deeper on the JavaScript aspect than we likely will. He tells me:

I added branches for TypeScript support, used lit-html for performant rendering, and an app-architecture branch for how to architect larger apps

There is also an extremely cool project called TodoMVC that was hot’n’heavy around 2012-2016. The purpose of it was comparing different approaches of a to-do app created using different JavaScript frameworks and vanilla approaches.

TodoMVC is useful for comparing syntax and solutions, is officially used in cross-browser benchmarks.

It was/is useful in helping people decide things like “Do I like the Angular approach or the React approach?”, for example. People could decide by looking at the actual code as well as seeing/feeling the output. We might fiddle with those ideas a little, but we’ll be focusing more on the thinking and experience of building and leaning toward vanilla code.

There are no wrong answers

We’re going to take an intentionally wordy trip through this project. We’re going to think about lots of different aspects of product development and web design and development. We’re going to do it in a certain order and land on certain choices.

If you were going to build a to-do app of your own, you might think about things differently. You might make different choices. You might do these things in a different order than I’m doing them. I think there is a rhyme and reason to the path I’ll write about, but that doesn’t make yours wrong. In fact, I think finding your own way is a great way to learn, and probably what you’d do anyway regardless of anything you read.

So unless what you build is broken or inaccessible, there is no wrong way.

Design: Thinking First

I like putting design first, design-driven development as it were, and here’s why. Design forces you to think about your app more holistically than you might otherwise.

When I started thinking about this idea, admittedly, I started poking around at JavaScript right away, putting some basic HTML together the JavaScript could work with. I was having fun reminding myself how powerful native web tech has become and how much we can do without reaching for anything else at all. But then things started to feel sloppy as I realized I was just randomly poking at things rather than have any actual plan. Sure, I could add new to-do items to a list, then remove them. But oh yeah I need to display the list from saved data. And then what else? Can you re-order them? Can you edit them? Can you delete multiple at once? That’s just scratching the surface. Design allows you to take a beat, stop thinking about implementations, and think of a plan first.

A Look Around

There is no shortage of to-do apps. It’s not like we wouldn’t be able to piece it together with our own thoughts of what should be in an app like this, but don’t let that confidence fool you! It’s always worth a look around to see how other apps are doing things, particularly successful ones because those in particular help establish the genre and set user expectations.

Here’s a quick walk through some to-do apps I happen to know about. Seriously, quick. These are very surface-level observations, nothing deep.

Things

Things is the to-do app that I’m most familiar with. It’s a native app, as opposed to the web like we’re doing, but that doesn’t stop us from looking at the UI and UX.

Notice they don’t go very heavy at all on UI for adding new items. A tiny + on the desktop app and a blue (+) on the mobile apps. The emphasis is more on knowing that Command-N will do the trick. I do enjoy the little feature that you can drag the mobile (+) where you want it before adding a new item. Notably, you add a new item before you add any text or information to the item. Metadata is pretty limited, preferring that you group things in various ways (areas, projects, headings), although there is things like tags if you want them. If any metadata is emphasized, it is due dates.

Todoist

Todoist actually is web-based, so that’s nice to see (hey, you get multi-platform for free!).

More metadata is exposed here. Tasks have a description which you automatically some of is interesting. Plus tags, plus due dates, plus collapsible groups.

Notice that adding a new task is also relatively chill. Unlike Things, pressing the + Add task button doesn’t automatically create a new task, but instead expands into a fleshed out task adder:

The task name is the only required thing, which makes sense. It would be obnoxious if anything else was. It feels like a reasonable choice to allow you to add all this metadata immediately, but certainly not the only path. They could do it such that you add the task first, then optionally add more to it if desired as an optional next step if you wanted.

TeuxDeux

TeuxDeux is big on having you put to-do items on individual days, although you can customize it if you want to. Having this kind of initially opinionated structure will have a huge influence on how people think about your app.

Notice there is almost no UI for adding new items. The way it works is that the next item in each column is automatically a live input, waiting for you get into it and add text. The extremely narrow item widths will also encourage you to get to the point darn quickly. You can see the markdown come through with the editing toolbar (Things also does Markdown, but only in the body of the description, which is hidden until opening an individual item.)

TodoMVC

This is the standard design as part of that TodoMVC project I mentioned (example). This is fun, as it’s quite intentionally minimal.

Note there is no submit/add button for new items, which weirds me out a little bit on the web. I’d say this then entirely relies on the submit even from the <form>, but in this case there is no <form> (?!), which then not only requires JavaScript but more JavaScript as they can’t just use that native event. It’s likely our app will require JavaScript as well, at least at first, as adding a backend language might be a trip too far for this series (but we’ll see, if y’all love it, lemme know).

Here we’re seeing implied categories based on completion (and a way to delete the cleared items as well). Plus a count. Plus the ability to double-click to edit. That editablity feels like a significant thing to think about. Does that double-click feel right or should you just be able to click in there? Native operating systems have a click-and-linger approach to name editing sometimes. You could certainly offer an “Edit” button (Pencil icon?) as well, but anything you add to every item is going is a risk of visual clutter. Reveal it on focus? Maybe there will be enough items to warrant a sub-menu? We’ll have to see.

Initial Features

There is no way we’re going to be as robust as some of these apps who have been working on their feature set for years and years. Let’s call it like this:

  1. Let’s emphasize the simple text input. Let’s see how easy we can make it to add a new item to one big single list.
  2. Let’s also emphasize the Add/+ button. Not only does it lean into HTML and accessibility correctness, but we can also make it part of our branding. Maybe we can borrow the Things approach of draggability someday.
  3. Just one big list for now, but let’s think extensibility when we store our data. It shouldn’t be a stretch to add multiple lists, tags, descriptions, or anything else.
  4. Add and Complete are the primary actions. For now, let’s borrow TodoMVC’s approach of double-click to edit unless we find something we like better while building.
  5. Let’s try and make drag-to-reorder happen.

Very basic.

Of course, we’d love to add user accounts, Markdown support, due dates, tagging, an API, bulk actions, recurring items, and who knows?? AI helpers? Integrations? If you absolutely knew that these features needed to be there for day one, then they should be part of the design process from day one. For us, not so much, so we’ll keep things simple and trim and build that first.

It’s fun to think about though! Heck, a to-do app, as simple as it seems at first, might be just as complicated or more than an app in just about any other genre.

Design

Knowing what a trim feature set we have, I’ll Figma something up real quick. Half an hour later, here we are.

I’d say we should also build a small screen version at this point, but I think it’s fairly obvious this simple design will shrink horizontally just fine.

This artifact is a little useful. We can reference colors. We can be relatively sure we’ve represented what we wanted there for day one. But mostly, this forced us to actually think about all that before our greedy little fingers started coding.

We’ll code better if we more clearly know where we’re going.

Working design-first also frees up your mind, I find, to think more creatively. Personally, I’m happy to drag things around weird places. Pick weird colors. Make things humungous or tiny. Explore totally different icons. All sorts of stuff like that that I just know I’m much less apt to do once I’m actually coding. For lack of a better metaphore, a left brain / right brain thing. As you get to know how you work best and most creatively, you’ll have your own tricks.

See ya next time!

Article Series

]]>
https://frontendmasters.com/blog/building-a-todo-app-from-scratch-step-1-planning-design/feed/ 0 794
Writing a TodoMVC App With Modern Vanilla JavaScript https://frontendmasters.com/blog/vanilla-javascript-todomvc/ https://frontendmasters.com/blog/vanilla-javascript-todomvc/#respond Thu, 08 Sep 2022 18:25:00 +0000 http://fem.flywheelsites.com/?p=7 I took a shot at coding TodoMVC with modern (ES6+), vanilla JavaScript, and it only took ~170 lines of code and just over an hour! Compare this to the old/official TodoMVC vanilla JS solution, which has over 900 lines of code. An 80%+ reduction in code! I ❤️ the new state of JavaScript.

The code has received over 🤩 600 stars on GitHub:

In general, the responses were very positive. But as with all popular things, eventually, they spark debate.

React / Frameworks vs. Vanilla JS: Top Four Arguments for Frameworks

#1: “Frameworks Enable Declarative UI”

Modern frameworks like React and Vue don’t exist to fill in the gap left by native JS, they exist so that you write your application in a declarative way where the view is rendered as a function of state.

IMO this is simply a design pattern. Patterns apply in any language.

You can accomplish roughly the same thing in vanilla JavaScript. In my code, when the model changes, it fires a save event, and then I wire App.render() to it, which renders the App using the Todos model.

Todos.addEventListener('save', App.render);

Template strings end up pretty easy to work with when you want to re-render parts of the App from scratch as a framework would:

`
  <div class="view">
    <input class="toggle" type="checkbox" ${todo.completed ? 'checked' : ''}>
    <label></label>
    <button class="destroy"></button>
  </div>
  <input class="edit">
`

The entire App render method is only eleven lines, and it re-renders everything the App needs to based on the state of the App:

render() {
  const count = Todos.all().length;
  App.$.setActiveFilter(App.filter);
  App.$.list.replaceChildren(
    ...this.Todos.all(this.filter).map((todo) => this.renderTodo(todo))
  );
  App.$.showMain(count);
  App.$.showFooter(count);
  App.$.showClear(Todos.hasCompleted());
  App.$.toggleAll.checked = Todos.isAllCompleted();
  App.$.displayCount(Todos.all('active').length);
}

Here I could have chosen to rebuild the entire UI as a template string as a function of state, but instead, it is ultimately more performant to create these DOM helper methods and modify what I want.

#2: “Frameworks Provide Input Sanitization”

The best way to sanitize user input is to use node.textContent.

insertHTML(li, `
  <div class="view">
    <input class="toggle" type="checkbox" ${todo.completed ? 'checked' : ''}>
    <label></label>
    <button class="destroy"></button>
  </div>
  <input class="edit">
`);
li.querySelector('label').textContent = todo.title;

Any user input must be set to the DOM using textContent. If you do that, then you’re fine.

Beyond this, there is a new Trusted Types API for sanitizing generated HTML. I would use this new API if I were generating nested markup with dynamic, user-input data. (Note that this new API isn’t available yet in Safari, but hopefully, it will be soon)

Trusted Types not being everywhere is fine. You can use them where they’re supported and get early warning of issues. Security improves as browsers improve, and usage turns into an incentive for lagging engines (source)

Suppose you want a library to build your app template strings without using textContent manually. In that case, you can use a library like DOMPurify, which uses Trusted Types API under the hood.

#3: “Frameworks Provide DOM Diffing and DOM Diffing is Necessary”

The most common criticism was the lack of DOM Diffing in vanilla JavaScript.

A reactive UI/diff engine is non-negotiable for me.

Diffing is exactly what you need to do (barring newer methods like svelte) to figure out what to tell the browser to change. The vdom tree is much faster to manipulate than DOM nodes.

However, I think this is a much more balanced take:

Diffing seems necessary when your UI gets complicated to the level that a small change requires a full page re-render. However, I don’t think this is necessary for at least 95% of the websites on the internet.

I agree most websites and web apps don’t suffer from this issue, even when re-rendering the needed components based on vanilla’s application state like a framework.

Lastly, I’ll note that DOM diffing is inefficient for getting reactive updates because it doubles up data structures. Lit, Svelte, Stencil, Solid, and many others don’t need it and are way more performant as a result. These approaches win on performance and memory use, which matters because garbage collection hurts the UX.

Modern frameworks necessitate that you render the entire App client-side which makes your apps slow by default.

My issue with modern frameworks forcing declarative UI (see #1) and DOM diffing (see #2) approach is that they necessitate unnecessary rendering and slow startup times. Remix is trying to avoid this by rendering server-side then “hydrating,” and new approaches like Quik are trying not to have hydration altogether. It’s an industry-wide problem, and people are trying to address it.

In my vanilla JavaScript projects, I only re-render the most minimal parts of the page necessary. Template strings everywhere, and especially adding DOM diffing, is inefficient. It forces you to render all of your App client-side increasing startup time and the amount the client has to do overall each time data changes.

That said, if you do need DOM diffing in parts of a vanilla app, libraries like morphdom do just that. There is also a fantastic templating library called Lit-html that solves this problem of making your App more declarative in a tiny package (~3KB), and you can continue using template strings with that.

#4: “Frameworks Scale, Vanilla JavaScript Will Never Scale”

I have built many large vanilla JavaScript projects and scaled them across developers, making the companies I worked for tons of money, and these apps still exist today. 🕺✨

Conventions and idioms are always needed, no matter if you build on top of a framework or not.

At the end of the day, your codebase will only be only as good as your team, not the framework.

The way vanilla JS scales is the same way any framework scales. You have to have intelligent people talk about the needs of the codebase and project.

App Architecture Branch:

That said, here’s an example of adding ~20 lines of structure to the code in the app architecture branch. It splits the code into a TodoList and App component. Each component implements a render method that optionally renders a filtered view of the data.

Overall I’d argue these solutions are more performant, less code (~200 lines only), and more straightforward than most, if not all, the TodoMVC implementations on the internet without a framework.

Here are Eight Vanilla JavaScript Tips from the Code

#1. Sanitization

User input must be sanitized before being displayed in the HTML to prevent XSS (Cross-Site Scripting). Therefore new todo titles are added to the template string using textContent:

li.querySelector('label').textContent = todo.title;

#2. Event Delegation

Since we render the todos frequently, it doesn’t make sense to bind event listeners and clean them up every time. Instead, we bind our events to the parent list that always exists in the DOM and infer which todo was clicked or edited by setting the data attribute of the item $li.dataset.id = todo.id;

Event delegation uses the matches selector:

export const delegate = (el, selector, event, handler) => {
  el.addEventListener(event, e => {
    if (e.target.matches(selector)) handler(e, el);
  });
}

When something inside the list is clicked, we read that data attribute id from the inner list item and use it to grab the todo from the model:

delegate(App.$.list, selector, event, e => {
  let $el = e.target.closest('[data-id]');
  handler(Todos.get($el.dataset.id), $el, e);
});

#3. insertAdjacentHTML

insertAdjacentHTML is much faster than innerHTML because it doesn’t have to destroy the DOM first before inserting.

export const insertHTML = (el, html) => {
  el.insertAdjacentHTML("afterbegin", html);
}

Bonus tip: Jonathan Neal taught me through a PR that you can empty elements and replace the contents with el.replaceChildren() — thanks Jonathan!

#4. Grouping DOM Selectors & Methods

DOM selectors and modifications are scoped to the App.$.* namespace. In a way, it makes it self-documenting what our App could potentially modify in the document.

$: {
  input: document.querySelector('[data-todo="new"]'),
  toggleAll: document.querySelector('[data-todo="toggle-all"]'),
  clear: document.querySelector('[data-todo="clear-completed"]'),
  list: document.querySelector('[data-todo="list"]'),
  count: document.querySelector('[data-todo="count"]'),
  showMain(show) {
    document.querySelector('[data-todo="main"]').style.display = show ? 'block': 'none';
  },
  showFooter(show) {
    document.querySelector('[data-todo="main"]').style.display = show ? 'block': 'none';
  },
  showClear(show) {
    App.$.clear.style.display = show ? 'block': 'none';
  },
  setActiveFilter(filter) {
    document.querySelectorAll('[data-todo="filters"] a').forEach(el => el.classList.remove('selected')),
    document.querySelector(`[data-todo="filters"] [href="#/${filter}"]`).classList.add('selected');
  },
  displayCount(count) {
    replaceHTML(App.$.count, `
      <strong>${count}</strong>
      ${count === 1 ? 'item' : 'items'} left
    `);
  }
},

#5. Send Events on a Class Instance with Subclassing EventTarget

We can subclass EventTarget to send out events on a class instance for our App to bind to:

export const TodoStore = class extends EventTarget {

In this case, when the store updates, it sends an event:

this.dispatchEvent(new CustomEvent('save'));

The App listens to that event and re-renders itself based on the new store data:

Todos.addEventListener('save', App.render);

#6. Group Setting Up Event Listeners

It is essential to know exactly where the global event listeners are set. An excellent place to do that is in the App init method:

init() {
  Todos.addEventListener('save', App.render);
  App.filter = getURLHash();
  window.addEventListener('hashchange', () => {
    App.filter = getURLHash();
    App.render();
  });
  App.$.input.addEventListener('keyup', e => {
    if (e.key === 'Enter' && e.target.value.length) {
      Todos.add({ title: e.target.value, completed: false, id: "id_" + Date.now() })
      App.$.input.value = '';
    }
  });
  App.$.toggleAll.addEventListener('click', e => {
    Todos.toggleAll();
  });
  App.$.clear.addEventListener('click', e => {
    Todos.clearCompleted();
  });
  App.bindTodoEvents();
  App.render();
},

Here we set up all the global event listeners, subscribe to the store mentioned above, and then initially render the App.

Similarly, when you create new DOM elements and insert them into the page, group the event listeners associated with the new elements near where they are made.

#7. Use Data Attributes in Markup & Selectors

One issue with JavaScript is your selectors get tightly coupled to the generated DOM.

To fix this, classes should be used for CSS rules, and data atributes for JavaScript behavior.

<div data-jsmodule="behavior"></div>
document.querySelector('[data-jsmodule="behavior"]')

#8. Render the State of the World Based on Data (Data Flowing Down)

Lastly, to reiterate what I said above, render everything based on the state in the render() method. This is a pattern lifted from modern frameworks.

Make sure you update the DOM based on your App state, not the other way around.

It’s even better if you avoid reading DOM to derive any part of your app state aside from finding your target for event delegation.

Side note: I like to rely on the server to generate the markup for faster boot times, then take control of the bits we show. Have the CSS initially hide things you don’t need, and then have the JavaScript show the elements based on the state. Let the server do most of the work where you can, rather than wait for the entire App to render client-side.

In Conclusion

Vanilla JS is Viable Today for Building Web Apps

JavaScript is better today than it has ever been.

The fact that I could shave off 80% of the code over the previous TodoMVC years ago at the drop of a hat feels terrific. Plus, we now have established design patterns that we can lift from modern frameworks to apply to vanilla JavaScript projects to make our UIs as declarative as we like.

As an industry we should consider pure JavaScript as an option for more projects.

Finally, as Web Components get more ergonomic, we will even have a way to share our code in an interoperable and framework-agnostic way.

I hope you enjoyed the post. Please send your feedback to me @1marc on Twitter. Cheers!


Bonus: Performant Rendering of Large Lists

The code for rendering the entire list contents on model change is clean because data flows down, but it potentially will not be as performant for rendering large lists.

More Performant & Granular DOM Updates with Vanilla

Here’s a branch sending specific events with context from the model so we can make DOM updates more selectively as we need them: (see granular DOM updates diff).

performant-rendering branch

More Performant DOM Updates with lit-html (plus animations!)

We can acheieve the same performant DOM updates with far less code by adopting lit-html using the repeat directive: (see adding lit-html diff).

animation-lithtml branch

]]>
https://frontendmasters.com/blog/vanilla-javascript-todomvc/feed/ 0 7