Going Old School And Being Fine With It

Working With Constraints In Modern Web Development

You might not need the latest framework, that you just read about on Twitter, for your latest todo app. But how can we render anything to the screen then, you might be asking? I could write in full length about why we might not need any fancy build tools or why it’s better to gradually build the app and add things along the way as soon as we see benefit.

This post is not about “don’t use frameworks or tools”, rather the contrary. It’s about knowing when to use the appropriate solution for a given problem. Definitely use frameworks, libraries and tools when they fit a given situation and avoid reinventing existing solutions. Sometimes you can start with constraints and focus on the problem and figure out what framework or library you really need to add, along the way. To be very clear, “going old school” doesn’t imply neglecting modern development practices and tools, it should imply that we can start bare bones and advance to a working solution step by step.

What make’s great website and apps are not the tools alone, it’s understanding the context they operate in and also knowing about the trade-offs said tools and decisions come with, when building modern applications.

Adding frameworks, tools and libraries will not magically make your application scale nor do they benefit your users just by being added to the mix. Having options is a great thing, this can’t be emphasized enough, but it also means discipline and making wise choices. The last aspect is the one that get’s people into trouble not the sheer mass of different solutions.

Constraints are a great thing sometimes. Just imagine how standout producers can magically create amazing compositions by virtually leveraging the most limited instruments you pass to them. If you ever heard of something like the SP-1200, which is a cult drum machine/sampler, you will know how much limitations you face when trying to program drum sounds with said machine. Very limited in sample time, no interface, but an endless number of classics have been programmed on these machines.

Technology doesn’t stand still, but that doesn’t mean that you can’t use an olds school approach sometimes. There is still music out there being created with old school drum machines and vintage mixers. Anything goes, it has to fit the current task and situation. Same goes for selecting the right tools for the right task when developing applications.

Let’s choose the most pragmatic approach and build yet another todo app. Todo apps are the de facto standard to present a new framework or library and have been for the longest, even when they might not be suited to portray the complexity that a framework tackles in the first place. But this is a different topic for itself, and might be addressed by the community at some point.

So let’s begin with building yet another todo app by only using one HTML and a single JavaScript file. No ES6, no build tools. This one is all about constraints. We can always improve on this. See it as a training session and sometimes using an HTML file and adding some JavaScript is exactly what is needed to achieve a desired result.

We will do something crazy and actually write some HTML.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Old School Todo App</title>
</head>
<body>
<!-- what goes here? -->
</body>
</html>

After clarifying what we need, we decide to add a couple of buttons and an input field.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Oldschool Todo App</title>
</head>
<body>
<input type="text" name="todo" id="addTodo" />
<button id="showAll">All</button>
<button id="showActive">Active</button>
<button id="showCompleted">Completed</button>
<div id="root"></div>
<script src="index.js"></script>
</body>
</html>

We also added an index.js file, so we can get along and start writing some mechanism that ensures our todo tasks are inserted, updated and deleted again. Along the line, we would like to ensure that our users can filter any completed tasks or see all, as they choose.

var root = document.getElementById('root');
var input = document.getElementById('addTodo');
var allBtn = document.getElementById('showAll');
var activeBtn = document.getElementById('showActive');
var completedBtn = document.getElementById('showCompleted');

So we defined all the buttons we need and are ready to go to work. One thing that we need to define upfront is the initial App state as well as a way to enable updating that state.

var id = 0;
var state = { todos: [], filter: showAllTodos };

function updateState(key, val) {
state[key] = val;
render(state);
}

You might be asking yourself, what is this render function doing here and why are we passing in the state? Hold on for a minute, we will get to this very shortly. Before we continue, let’s think about what we actually want to do. Add todos and remove todos? Check.

function createTodo(text) {
return { id: id++, text: text, completed: false };
}
function removeTodo(id) {
return state.todos.filter(function(todo) {
return todo.id !== id;
});
}

The filtering is only a couple of functions that operate on a given set of todos. Nothing too complicated.

function showAllTodos(todos) {
return todos;
}
function showActiveTodos(todos) {
return todos.filter(function(todo) {
return !todo.completed;
});
}

function showCompletedTodos(todos) {
return todos.filter(function(todo) {
return todo.completed;
});
}

Now let’s ensure that our filter buttons actually do something. Again, all we need to do is attach event listeners that update the filter set.

allBtn.addEventListener('click', function() {
updateState('filter', showAllTodos);
});

activeBtn.addEventListener('click', function() {
updateState('filter', showActiveTodos);
});

completedBtn.addEventListener('click', function() {
updateState('filter', showCompletedTodos);
});

How do we add any todos? We need to add an event listener to our input.

input.addEventListener('keyup', function(e) {
var text = e.target.value;
if (e.keyCode !== 13 || !text) return;
e.target.value = '';
state.todos.push(createTodo(text));
updateState('todos', state.todos);
});

Nothing too spectacular, but it gets the job done. We check to see if the enter key has been pressed and an actual value has been submitted and proceed to updating our todos and calling updateState with the newly updated todo array. All in all this is very clear, but we can see that we’re accessing the state directly and reassigning it back again. This will leave some space for refactoring later on.

All that is left to implement now is the aforementioned render function and figuring out how to create the needed DOM elements that should be rendered. Let’s get the latter figured out first. Actually, this means creating a div element and a button element for every todo task. The button is needed as we also want to be able to remove the todo. If you remember we already have the removeTodo function in place.

function createRemoveButton(id) {
var button = document.createElement('button');
button.innerHTML = 'Remove';
button.addEventListener('click', function() {
updateState('todos', removeTodo(id));
});
return button;
}

This looks very “low level”, we don’t have any fancy abstractions that hide away the messy parts involved with creating the DOM element, but it get’s the job done and we have a very fine grained control over the process plus we can improve on this during a refactoring session.

Lastly we need to create the row that displays the todo as well as the remove button. By adding createTodoElement we can now pass in a todo object and get a DOM element in return.

function createTodoElement(todo) {
var element = document.createElement('div');
element.innerHTML = todo.text;
element.id = todo.id;
element.appendChild(createRemoveButton(todo.id));

if (todo.completed) element.setAttribute('class', 'completed');

element.addEventListener('click', function() {
updateState('todos', state.todos.map(function(t) {
if (t.id === todo.id) t.completed = !t.completed;
return t;
}));
});

return element;
}

Not really anything special happening here, except maybe for the fact that we also add a class completed once a todo has been completed. We’re also attaching an event handler onto the element, so that we can toggle the todos completed state. The toggle function could have been extracted into a standalone function, but we’ll leave it as it is for now.

Finally we need to render something to the screen, right?

The last function we need to add is the render function. render expects the state and re-renders the complete todo list as soon as a state update occurred inside our application.

function render(state) {
root.innerHTML = '';
state.filter(state.todos).map(function(todo) {
root.appendChild(createTodoElement(todo));
});
}

We filter the todos according to the currently selected filter and create the corresponding DOM elements for every todo item and append it to our root DOM node.

The screen is still blank, because we need to trigger an initial rendering, so all that is left to do is call render with the initial state.

render(state);

This was an old school approach, very basic, but it gave us time to focus on the problem.

Refactoring And Improving

From here on, it’s clear that we need to do some improvements on the code. The improvements can be anything from swapping the low level DOM manipulation with a virtual DOM library or with React or Preact for example. We can also improve on that state handling by using something like lenses or by building a store that we can subscribe to and update via an API. We have a clear outlook on what needs improving from here on.

Summary

What’s the point for all of this?

There is a desire for focusing on the new shiny stuff, which is great and understandable, and if we’re building throw away apps or experimenting around we don’t have to really think about the long run . Everything else is a business decision. Literally, everything else is about making the right decisions. What does your application need, to reach a certain business goal, and what are the tradeoffs that have to be considered?

Somebodies framework or boilerplate can’t anticipate what you’re trying to actually solve. You’re shifting decisions onto somebody else, which means you’re only delaying the point to where you have to understand the problem you’re trying to solve. This is only a matter of time.

Constraints can be a great thing, they let you think harder about the problem, as you can’t just simply install your way out of a problem. You might not be actually solving the problem with a barebones initial approach, but it might help you to get a clearer understanding of what tools you actually need to solve the problem in the end.

Sometimes writing HTML and some plain old JavaScript gets the job done. It’s all about choosing the right tools for the right job at the end of the day.

Any questions or Feedback? Connect via Twitter

Thanks to Patrick Stapfer and Mustafa Alic for the feedback and Nik Graf for additional input and ideas.