# VANILLA TODO
A [TeuxDeux](https://teuxdeux.com) clone in plain HTML, CSS and JavaScript (no
build steps). It's fully animated and runs smoothly at 60 FPS with a total
transfer size of **55 KB** (unminified).
**[Try it online →](https://morris.github.io/vanilla-todo/)**
More importantly, it's a case study showing that **vanilla web development** is
viable in terms of [maintainability](#521-the-good), and worthwhile in terms of
[user experience](#51-user-experience) (**50%** less time to load and **95%**
less bandwidth in this case).
**There's no custom framework invented here.** Instead, the case study was
[designed](#22-rules) to discover minimum viable
[patterns](#321-mount-functions) that are truly vanilla. The result is
maintainable, albeit [verbose](#522-the-verbose) and with considerable
duplication.
If anything, the case study validates the value of build steps and frameworks,
but also demonstrates that standard web technologies can be used effectively and
there are only a few [critical areas](#523-the-bad) where a vanilla approach is
clearly inferior.
_While the first version of the case study has been published in 2020, it has
received significant [updates](#9-changelog) over time._
_Intermediate understanding of the web platform is required to follow through._
## Table of Contents
- [1. Motivation](#1-motivation)
- [2. Method](#2-method)
- [2.1. Subject](#21-subject)
- [2.2. Rules](#22-rules)
- [2.3. Goals](#23-goals)
- [2.3.1. User Experience](#231-user-experience)
- [2.3.2. Code Quality](#232-code-quality)
- [2.3.3. Generality of Patterns](#233-generality-of-patterns)
- [3. Implementation](#3-implementation)
- [3.1. Basic Structure](#31-basic-structure)
- [3.2. JavaScript Architecture](#32-javascript-architecture)
- [3.2.1. Mount Functions](#321-mount-functions)
- [3.2.2. Data Flow](#322-data-flow)
- [3.2.3. Rendering](#323-rendering)
- [3.2.4. Reconciliation](#324-reconciliation)
- [3.3. Drag & Drop](#33-drag--drop)
- [3.4. Animations](#34-animations)
- [4. Tooling](#4-tooling)
- [4.1. Local Development Server](#41-local-development-server)
- [4.2. Formatting and Linting](#42-formatting-and-linting)
- [4.3. Testing](#43-testing)
- [4.3.1. Code Coverage](#431-code-coverage)
- [4.4. Pipeline](#44-pipeline)
- [4.5. Debugging](#45-debugging)
- [5. Assessment](#5-assessment)
- [5.1. User Experience](#51-user-experience)
- [5.2. Code Quality](#52-code-quality)
- [5.2.1. The Good](#521-the-good)
- [5.2.2. The Verbose](#522-the-verbose)
- [5.2.3. The Bad](#523-the-bad)
- [5.3. Generality of Patterns](#53-generality-of-patterns)
- [6. Conclusion](#6-conclusion)
- [7. Beyond Vanilla](#7-beyond-vanilla)
- [8. Appendix](#8-appendix)
- [8.1. Links](#81-links)
- [8.2. Response](#82-response)
- [9. Changelog](#9-changelog)
## 1. Motivation
I believe too little has been invested in researching practical, scalable
methods for building web applications without third party dependencies.
It's not enough to describe how to create DOM nodes or how to toggle a class
without a framework. It's also rather harmful to write an article saying you
don't need library X, and then proceed in describing how to roll your own
untested, inferior version of X.
What's missing are thorough examples of complex web applications built only with
standard web technologies, covering as many aspects of the development process
as possible.
This case study is an attempt to fill this gap, at least a little bit, and
inspire further research in the area.
## 2. Method
The method for this case study is as follows:
- Pick an interesting subject.
- Implement it using only standard web technologies.
- Document techniques and patterns found during the process.
- Assess the results by common quality standards.
This section describes the method in more detail.
### 2.1. Subject
I've chosen to build a (functionally reduced) clone of
[TeuxDeux](https://teuxdeux.com) for this study. The user interface has
interesting challenges, in particular performant drag & drop when combined with
animations.
_The original TeuxDeux app deserves praise here. In my opinion it has the best
over-all concept and UX of all the to-do apps out there.
[Thank you!](https://fictivekin.com/)_
The user interface is arguably small (which is good for a case study) but large
enough to require thought on its architecture.
However, it is lacking in some key areas:
- Routing
- Asynchronous resource requests
- Complex forms
- Server-side rendering
### 2.2. Rules
To produce valid vanilla solutions, and because constraints spark creativity, I
came up with a set of rules to follow throughout the process:
- Only use standard web technologies.
- Only use widely supported JS features unless they can be polyfilled (1).
- No runtime JS dependencies (except polyfills).
- No build steps.
- No general-purpose utility functions related to the DOM/UI (2).
(1) This is a moving target; the current version is using ES2020.
(2) These usually end up becoming a custom micro-framework, thereby questioning
why you didn't use one of the established and tested libraries/frameworks in the
first place.
### 2.3. Goals
The results are going to be assessed by three major concerns:
#### 2.3.1. User Experience
The product should be comparable to or better than the original regarding
functionality, performance and design.
This includes testing major browsers and devices.
#### 2.3.2. Code Quality
The implementation should be _maintainable_ and follow established code quality
standards.
This will be difficult to assess objectively, as we will see later.
#### 2.3.3. Generality of Patterns
The discovered techniques and patterns should be applicable in a wide range of
scenarios.
## 3. Implementation
This section walks through the implementation, highlighting techniques and
problems found during the process. You're encouraged to inspect the
[source code](./public) alongside this section.
### 3.1. Basic Structure
Since build steps are ruled out, the codebase consists of plain HTML, CSS and JS
files. The HTML and CSS follows [rscss](https://ricostacruz.com/rscss/) (devised
by [Rico Sta. Cruz](https://ricostacruz.com)) resulting in an intuitive,
component-oriented structure.
The stylesheets are slightly verbose.
[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)
did help but I missed [SCSS](https://sass-lang.com/) here; I think it's a
must-have for bigger projects. Additionally, the global CSS namespace problem is
unaddressed (see e.g.
[CSS Modules](https://github.com/css-modules/css-modules)).
All JavaScript files are ES modules (`import`/`export`). I added a few
[JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html)
comments to functions to get additional code completion in VSCode. This helps,
but using TypeScript would be much safer and less verbose.
Note that I've opted out of web components completely. My attempts to refactor
the implementation using web components either added more complexity, or did not
show significant value over the initial, more basic approach.
---
The basic structure comes with some boilerplate, e.g. referencing all the
individual stylesheets and scripts from the HTML; probably enough to justify a
simple build step.
It is otherwise straight-forward—literally a bunch of HTML, CSS and JS
files.
### 3.2. JavaScript Architecture
Naturally, the JavaScript architecture is the most interesting part of this
study.
I found that using a combination of functions, query selectors and DOM events is
sufficient to build a scalable, maintainable codebase, albeit with some
trade-offs as we will see later.
Conceptually, the proposed architecture loosely maps CSS selectors to JS
functions which are _mounted_ (i.e. called) once per matching element. This
simple mental model aligns well with the DOM and styles:
```
TodoList -> .todo-list
scripts/TodoList.js
styles/todo-list.css
AppCollapsible -> .app-collapsible
scripts/AppCollapsible.js
styles/app-collapsible.css
...
```
This proved to be a useful, repeatable pattern throughout all of the
implementation process.
#### 3.2.1. Mount Functions
_Mount functions_ take a DOM element as their first argument. Their
responsibility is to set up initial state, event listeners, and provide behavior
and rendering for the target element.
For example, this mount function implements a simple counter:
```js
// Define mount function.
// Loosely mapped to ".my-counter".
export function MyCounter(el) {
// Define initial state.
let value = 0;
// Set rigid base HTML.
el.innerHTML = `
`;
// Attach event listeners.
el.querySelector('.increment').addEventListener('click', () => {
// Dispatch a custom event, using .detail to transport data.
// Parent components can listen to this event to receive the counter's value.
el.dispatchEvent(
new CustomEvent('counter', {
detail: value + 1,
bubbles: true,
}),
);
});
el.querySelector('.decrement').addEventListener('click', () => {
el.dispatchEvent(
new CustomEvent('counter', {
detail: value - 1,
bubbles: true,
}),
);
});
// This event handler supports the increment/decrement actions above,
// as well as resetting the counter from the outside.
el.addEventListener('counter', (e) => {
// Update state and re-render.
value = e.detail;
update();
});
// Define idempotent update function.
function update() {
el.querySelector('.value').innerText = value;
}
// Initial update.
update();
}
// Mount MyCounter component(s).
// Any
in the document will be mounted.
document.querySelectorAll('.my-counter').forEach(MyCounter);
```
This comes with quite some boilerplate but has useful properties, as we will see
in the following sections.
Note that a mount function does not have to set any base HTML, and may instead
only set event listeners to enable some behavior. Also note that an element can
be mounted with multiple mount functions. For example, to-do items are mounted
with `TodoItem` and `AppDraggable`.
Compared to React components, mount functions provide interesting flexibility as
components and behaviors can be implemented using the same idiom and combined
arbitrarily.
Reference:
- [AppIcon.js](./public/scripts/AppIcon.js)
- [TodoItem.js](./public/scripts/TodoItem.js)
- [TodoItemInput.js](./public/scripts/TodoItemInput.js)
#### 3.2.2. Data Flow
I found it effective to implement one-way data flow similar to React's approach,
however exclusively using custom DOM events.
- **Data flows downwards** from parent components to child components through
custom DOM events. Data events are in noun-form.
- **Actions flow upwards** through custom DOM events (bubbling up), usually
resulting in some parent component state change which is in turn propagated
downwards through data events. Action events are in verb-form.
The business logic is factored into a pure functional core
([TodoLogic.js](./public/scripts/TodoLogic.js)). This is a sensible approach in
most UI architectures as it encapsulates state transitions in portable, testable
units.
The controller is factored into a separate behavior
([TodoController.js](./public/scripts/TodoController.js)). It only receives and
dispatches events, calling the business logic to apply changes and emit state.
It also handles persistence in Local Storage.
Listening to and dispatching events is slightly verbose with standard APIs and
certainly justifies introducing helpers. I didn't need event delegation à la
jQuery for this study but I believe it's a useful concept that is difficult to
do concisely with standard APIs.
Reference:
- [TodoDay.js](./public/scripts/TodoDay.js)
- [TodoController.js](./public/scripts/TodoController.js)
- [TodoLogic.js](./public/scripts/TodoLogic.js)
#### 3.2.3. Rendering
Naively re-rendering a whole component using `.innerHTML` should be avoided as
this may hurt performance and will likely break important functionality like:
- ``, `