When I promote vanilla JS, one of the places I often get pushback is around building complex UIs.
Sure, vanilla JS is great for simple scripts and web apps. But as soon as things get complex, you need React.
Over the next few days, I want to take explore the edges where vanilla JS starts to fall apart, and highlight the vibrant middle ground between "100% hand-rolled vanilla JS" and "just use React."
Let's dig in!
A todo app
For this series, we're going to look at the most cliche example of all examples: a todo app.
It's a cliche example in large part because it seems deceiving simple, and can be built with varying levels of complexity. It's a great teaching project!
For our purposes, let's imagine we have a form
element and an unordered list (ul
).
<form id="add-todo"> <label>What do you want to do?</label> <input type="text" id="todo"> <button>Add Todo</button> </form> <ul id="app"></ul>
Whenever the user submits a todo using the #add-todo
form, we want to create a new list item (li
) and add it to the #app
list.
Using traditional DOM manipulation
The old-school vanilla JS version of this app takes 28 lines of code, including comments and white space.
First, let's get the app
and form
elements using the document.querySelector()
method, and assign them to variables.
// DOM elements let app = document.querySelector('#app'); let form = document.querySelector('#add-todo');
Next, lets listen for submit
events on our form
, and addTodo
items whenever that happens.
// Add todos when form is submited form.addEventListener('submit', addTodo);
Inside the addTodo()
function, we'll first use the event.preventDefault()
method to stop the form from reloading the page.
Then, we'll make sure the #todo
field inside the form
has a value
. If not, we'll end our function early.
Because the field has an ID, we can access it directly as a property of the form
element.
/** * Add todo to the list * @param {Event} event The event object */ function addTodo (event) { // Stop the form from reloading the page event.preventDefault(); // If there's no field value, ignore the submission if (!form.todo.value) return; }
Otherwise, we'll use the document.createElement()
method to create a new list item (li
), and set the form.todo.value
as its textContent
.
Then, we can use the Element.append()
method to inject it into the app
list. We'll also clear the form field.
/** * Add todo to the list * @param {Event} event The event object */ function addTodo (event) { // Stop the form from reloading the page event.preventDefault(); // If there's no field value, ignore the submission if (!form.todo.value) return; // Otherwise, add a todo let li = document.createElement('li'); li.textContent = form.todo.value; app.append(li); // Clear the form field form.todo.value = ''; }
Now, we've got a basic todo app stood up with just a few lines of code. Try it yourself on CodePen.
Adding features (and a bit of complexity)
Now we've got an app where we can add todo items. But, every time the user reloads the page, they get wiped out.
We can fix that pretty easily with localStorage
.
Whenever we add a todo item, we'll use the localStorage.setItem()
method to save the innerHTML
of the app
element.
// Clear the form field form.todo.value = ''; // Save list localStorage.setItem('todos', app.innerHTML);
And whenever the page is opened, we'll look for saved todos and load them into the UI.
// Add todos when form is submited form.addEventListener('submit', addTodo); // Load saved todos app.innerHTML = localStorage.getItem('todos') || '';
But if you're going to do this, you also need to provide a way for users to remove todo items.
After creating our list item, let's create a button
with the [data-delete]
attribute on it, and append()
it inside the li
element.
// Otherwise, create a todo let li = document.createElement('li'); li.textContent = form.todo.value; // Add a remove button let btn = document.createElement('button'); btn.textContent = 'Delete'; btn.setAttribute('data-delete', ''); li.append(btn); // Append to the UI app.append(li);
Since we're dynamically adding buttons, we can listen for clicks on our button using event delegation. We'll listen for all clicks in the document
, and run the removeTodo()
function in response.
// Remove todos when delete button is clicked document.addEventListener('click', removeTodo);
Inside the removeTodo()
function, we'll ignore any clicks that weren't triggered by a [data-delete]
button.
We can check that by running the Element.matches()
method on the event.target
, the element that triggered the event.
/** * Remove todo items * @param {Event} event The event object */ function removeTodo (event) { // Only run on [data-delete] items if (!event.target.matches('[data-delete]')) return; }
If it was a delete button, we'll use the Element.closest()
method to find the parent list item (li
), and the Element.remove()
method to remove it from the DOM.
Then, we'll run the localStorage.setItem()
method again to update our saved list.
/** * Remove todo items * @param {Event} event The event object */ function removeTodo (event) { // Only run on [data-delete] items if (!event.target.matches('[data-delete]')) return; // Otherwise, remove the todo let li = event.target.closest('li'); if (!li) return; li.remove(); // Save the list localStorage.setItem('todos', app.innerHTML); }
Things get unmanageably complex pretty fast
At this point, we're still in the world of "not too bad" with traditional DOM manipulation (in my opinion).
But that changes pretty fast!
For example, we probably want to display a message when there are no todo items yet. With traditional DOM manipulation, the easiest way to do that is to add an element in the DOM that we selectively show and hide.
<p id="no-todos" hidden><em>You don't have any todos yet.</em></p>
First, we'll create another variable for the noTodos
element.
// DOM elements let app = document.querySelector('#app'); let form = document.querySelector('#add-todo'); let noTodos = document.querySelector('#no-todos');
Then, we'll create a loadSavedTodos()
function to help us out.
If there's saved todos, we'll show them. Otherwise, we'll use the Element.removeAttribute()
method to remove the [hidden]
attribute and show our message.
/** * Load saved todo items into the UI */ function loadSavedTodos () { let saved = localStorage.getItem('todos'); if (saved) { app.innerHTML = saved; } else { noTodos.removeAttribute('hidden'); } }
Then, we'll run this method instead of directly loading saved todos.
// Load saved todos loadSavedTodos();
Whenever we append a list item to the app
, we'll add the [hidden]
attribute attribute back to hide the message if it's current visible.
// Append to the UI app.append(li); // Hide the no-todos message noTodos.setAttribute('hidden', '');
And when we remove a todo item, if there are no list items in our app
, we'll remove the attribute again to show the message.
// Save the list localStorage.setItem('todos', app.innerHTML); // If there are no todos, show the no-todos message if (!app.innerHTML.trim().length) { noTodos.removeAttribute('hidden'); }
We've reached the breaking point
We're now up to 85 lines of code (with comments and whitespace), and there's still a ton of features you'd probably want to add to an app like this.
For example, you probably want a button to clear all todos. But you only want to show it if there are todo items to remove.
And you might also want to provide users with a way to edit existing todo items or mark them as complete.
At this point, the cost of adding new features has gotten quite high. It requires keeping track of the current state of the UI, and selectively adding, removing, and updating various pieces.
And this is where a completely different approach to building the app makes sense.
Tomorrow, we're going to dive into what that is.
The Vanilla JS Academy is a project-based online JavaScript workshop for beginners. Click here to learn more.
Cheers,
Chris
Want to share this with others or read it later? View it in a browser.
0 Komentar untuk "[Go Make Things] Building complex apps with vanilla JavaScript (a series)"