1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-01-17 20:58:22 +01:00
vanilla-todo/README.md

972 lines
34 KiB
Markdown
Raw Normal View History

2020-10-20 17:27:16 +02:00
# VANILLA TODO
2020-10-23 12:06:29 +02:00
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
2023-12-02 11:06:09 +01:00
with a total transfer size of **55 KB** (unminified).
2020-10-23 12:06:29 +02:00
2023-12-02 12:33:47 +01:00
**[Try it online →](https://morris.github.io/vanilla-todo/)**
2020-10-23 12:07:27 +02:00
2020-10-23 12:06:29 +02:00
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)
2023-12-02 11:06:09 +01:00
(**50%** less time to load and **95%** less bandwidth in this case).
2020-10-23 12:06:29 +02:00
**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.
2020-10-23 12:06:29 +02:00
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.
2020-10-20 17:27:16 +02:00
2023-12-02 11:06:09 +01:00
_While the first version of the case study has been published in 2020, it has received significant [updates](#9-changelog) over time._
2020-10-23 12:06:29 +02:00
_Intermediate understanding of the web platform is required to follow through._
2020-10-21 11:28:36 +02:00
## 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)
2020-10-21 11:45:02 +02:00
- [3.2.1. Mount Functions](#321-mount-functions)
2020-10-21 11:28:36 +02:00
- [3.2.2. Data Flow](#322-data-flow)
- [3.2.3. Rendering](#323-rendering)
- [3.2.4. Reconciliation](#324-reconciliation)
2020-10-21 11:45:02 +02:00
- [3.3. Drag & Drop](#33-drag--drop)
- [3.4. Animations](#34-animations)
2023-11-30 18:55:58 +01:00
- [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)
2023-12-02 12:33:47 +01:00
- [4.4. Pipeline](#44-pipeline)
2020-10-21 11:28:36 +02:00
- [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)
2020-10-21 11:45:02 +02:00
- [5.2.2. The Verbose](#522-the-verbose)
- [5.2.3. The Bad](#523-the-bad)
2020-10-21 11:28:36 +02:00
- [5.3. Generality of Patterns](#53-generality-of-patterns)
- [6. Conclusion](#6-conclusion)
2023-12-02 11:06:09 +01:00
- [7. Beyond Vanilla](#7-beyond-vanilla)
2020-10-21 17:44:39 +02:00
- [8. Appendix](#8-appendix)
2021-01-02 15:57:47 +01:00
- [8.1. Links](#81-links)
- [8.2. Response](#82-response)
2020-10-28 16:21:12 +01:00
- [9. Changelog](#9-changelog)
2020-10-21 11:28:36 +02:00
2020-10-20 17:27:16 +02:00
## 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.
2020-10-22 16:18:59 +02:00
What's missing are thorough examples of complex web applications
2020-10-20 17:27:16 +02:00
built only with standard web technologies, covering as many aspects of
the development process as possible.
2020-10-22 15:22:13 +02:00
This case study is an attempt to fill this gap, at least a little bit,
and inspire further research in the area.
2020-10-20 17:27:16 +02:00
## 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
2023-11-30 18:55:58 +01:00
I've chosen to build a (functionally reduced) clone of
2020-10-20 17:27:16 +02:00
[TeuxDeux](https://teuxdeux.com) for this study.
The user interface has interesting challenges,
in particular performant drag & drop when combined with animations.
2020-10-28 16:21:12 +01:00
_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/)_
2020-10-20 17:27:16 +02:00
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
- 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.
2020-10-20 17:27:16 +02:00
(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
2020-10-21 11:45:02 +02:00
The results are going to be assessed by three major concerns:
2020-10-20 17:27:16 +02:00
#### 2.3.1. User Experience
The resulting 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 resulting implementation should adhere to
established code quality standards in the industry.
2020-10-21 11:28:36 +02:00
This will be difficult to assess objectively, as we will see later.
2020-10-20 17:27:16 +02:00
#### 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 resulting 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 is organized around
plain HTML, CSS and JS files. The HTML and CSS mostly follows
[rscss](https://ricostacruz.com/rscss/) (devised by [Rico Sta. Cruz](https://ricostacruz.com))
2020-10-21 11:45:02 +02:00
which yields an intuitive, component-oriented structure.
2020-10-20 17:27:16 +02:00
The stylesheets are slightly verbose.
I missed [SCSS](https://sass-lang.com/) here
2020-10-22 17:24:37 +02:00
and I think one of these is 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)).
2020-10-20 17:27:16 +02:00
All JavaScript files are ES modules (`import`/`export`).
2020-10-21 17:44:39 +02:00
2020-10-20 17:27:16 +02:00
Note that I've opted out of web components completely.
I can't clearly articulate what I dislike about them
but I never missed them throughout this study.
---
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 and trivial to understand
(literally just a bunch of HTML, CSS and JS files).
### 3.2. JavaScript Architecture
2020-10-22 16:18:59 +02:00
Naturally, the JavaScript architecture is the most interesting part of this study.
2020-10-20 17:27:16 +02:00
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
2023-11-30 11:44:32 +01:00
per matching element. This simple mental model also works well
2020-10-20 17:27:16 +02:00
with the DOM and styles:
```
.todo-list -> TodoList
2020-10-20 17:27:16 +02:00
scripts/TodoList.js
styles/todo-list.css
.app-collapsible -> AppCollapsible
2020-10-20 17:27:16 +02:00
scripts/AppCollapsible.js
styles/app-collapsible.css
...
```
2020-10-22 13:35:49 +02:00
This proved to be a useful, repeatable pattern throughout all of the
implementation process.
2020-10-20 17:27:16 +02:00
2020-10-21 11:45:02 +02:00
#### 3.2.1. Mount Functions
2020-10-20 17:27:16 +02:00
2023-11-30 11:44:32 +01:00
_Mount functions_ take a DOM element as their first argument.
2020-10-20 17:27:16 +02:00
Their responsibility is to set up initial state, event listeners, and
provide behavior and rendering for the target element.
2020-10-21 11:45:02 +02:00
Here's a "Hello, World!" example of mount functions:
2020-10-20 17:27:16 +02:00
```js
2023-11-19 14:48:31 +01:00
// Define mount function
// Loosely mapped to ".hello-world"
export function HelloWorld(el) {
2023-11-19 14:48:31 +01:00
// Define initial state
2023-11-26 11:54:04 +01:00
let title = 'Hello, World!';
let description = 'An example vanilla component';
let counter = 0;
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Set rigid base HTML
el.innerHTML = `
<h1 class="title"></h1>
<p class="description"></p>
<div class="my-counter"></div>
`;
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Mount sub-components
2022-07-22 16:43:29 +02:00
el.querySelectorAll('.my-counter').forEach(MyCounter);
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Attach event listeners
2023-11-26 11:54:04 +01:00
el.addEventListener('modifyCounter', (e) => {
counter += e.detail;
update();
});
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Initial update
update();
2023-11-19 14:48:31 +01:00
// Define idempotent update function
2023-11-26 11:54:04 +01:00
function update() {
2023-11-19 14:48:31 +01:00
// Update own HTML
2023-11-26 11:54:04 +01:00
el.querySelector('.title').innerText = title;
el.querySelector('.description').innerText = description;
2020-10-20 17:27:16 +02:00
2023-11-26 11:54:04 +01:00
// Pass data to sub-components
el.querySelector('.my-counter').dispatchEvent(
new CustomEvent('updateMyCounter', {
2023-11-26 11:54:04 +01:00
detail: { value: counter },
2023-11-13 20:03:04 +01:00
}),
);
2020-10-20 17:27:16 +02:00
}
}
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Define another component
// Loosely mapped to ".my-counter"
export function MyCounter(el) {
2023-11-19 14:48:31 +01:00
// Define initial state
2023-11-26 11:54:04 +01:00
let value = 0;
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Set rigid base HTML
el.innerHTML = `
<p>
<span class="value"></span>
<button class="increment">Increment</button>
<button class="decrement">Decrement</button>
</p>
`;
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Attach event listeners
2022-07-22 16:43:29 +02:00
el.querySelector('.increment').addEventListener('click', () => {
2023-11-19 14:48:31 +01:00
// Dispatch an action
// Use .detail to transport data
2020-10-20 17:27:16 +02:00
el.dispatchEvent(
new CustomEvent('modifyCounter', {
detail: 1,
bubbles: true,
2023-11-13 20:03:04 +01:00
}),
2020-10-20 17:27:16 +02:00
);
});
2022-07-22 16:43:29 +02:00
el.querySelector('.decrement').addEventListener('click', () => {
2023-11-19 14:48:31 +01:00
// Dispatch an action
// Use .detail to transport data
2020-10-20 17:27:16 +02:00
el.dispatchEvent(
new CustomEvent('modifyCounter', {
detail: -1,
bubbles: true,
2023-11-13 20:03:04 +01:00
}),
2020-10-20 17:27:16 +02:00
);
});
2023-11-26 11:54:04 +01:00
el.addEventListener('updateMyCounter', (e) => {
value = e.detail;
update();
});
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Define idempotent update function
2023-11-26 11:54:04 +01:00
function update() {
el.querySelector('.value').innerText = value;
2020-10-20 17:27:16 +02:00
}
}
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Mount HelloWorld component(s)
// Any <div class="hello-world"></div> in the document will be mounted
document.querySelectorAll('.hello-world').forEach(HelloWorld);
2020-10-20 17:27:16 +02:00
```
This comes with quite some boilerplate but has useful properties,
as we will see in the following sections.
Note that any part of a mount function is entirely optional.
For example, a mount function does not have to set any base HTML,
and may instead only set event listeners to enable some behavior.
2020-10-22 13:35:49 +02:00
Also note that an element can be mounted with multiple mount functions.
For example, to-do items are mounted with `TodoItem` and `AppDraggable`.
2020-10-20 17:27:16 +02:00
2020-10-22 15:22:13 +02:00
Compared to React components, mount functions provide interesting flexibility as
2020-10-22 16:18:59 +02:00
components and behaviors can be implemented using the same idiom and combined
arbitrarily.
2020-10-22 13:35:49 +02:00
Reference:
2020-10-20 17:27:16 +02:00
- [AppIcon.js](./public/scripts/AppIcon.js)
- [TodoItem.js](./public/scripts/TodoItem.js)
- [TodoItemInput.js](./public/scripts/TodoItemInput.js)
2020-10-21 11:28:36 +02:00
#### 3.2.2. Data Flow
2020-10-20 17:27:16 +02:00
I found it effective to implement one-way data flow similar to React's approach,
however exclusively using custom DOM events.
2020-10-20 17:27:16 +02:00
- **Data flows downwards** from parent components to child components
through custom DOM events.
2020-10-20 17:27:16 +02:00
- **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.
2020-10-20 17:27:16 +02:00
The business logic is factored into a pure functional core
([TodoLogic.js](./public/scripts/TodoLogic.js)).
This is a good idea in many UI architectures as it encapsulates
state transitions in a portable, testable unit.
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.
2020-10-20 17:27:16 +02:00
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
2020-10-22 13:35:49 +02:00
but I believe it's a useful concept that is difficult to do
2020-10-22 15:22:13 +02:00
concisely with standard APIs.
2020-10-20 17:27:16 +02:00
2020-10-22 13:35:49 +02:00
Reference:
2020-10-20 17:27:16 +02:00
- [TodoDay.js](./public/scripts/TodoDay.js)
- [TodoController.js](./public/scripts/TodoController.js)
- [TodoLogic.js](./public/scripts/TodoLogic.js)
2020-10-20 17:27:16 +02:00
2020-10-21 11:28:36 +02:00
#### 3.2.3. Rendering
2020-10-20 17:27:16 +02:00
2020-10-22 16:18:59 +02:00
Naively re-rendering a whole component using `.innerHTML` should be avoided
as this may hurt performance and will likely break important functionality
which browsers have already been optimizing for decades:
- `<a>`, `<button>`, `<input>`, etc. may lose focus.
2022-08-06 16:59:51 +02:00
- Form inputs may lose data.
- Text selection may be reset.
- CSS transitions may not work correctly.
- Event listeners may need to be reattached.
2020-10-20 17:27:16 +02:00
2020-10-22 13:35:49 +02:00
As seen in [3.2.1.](#321-mount-functions), rendering is therefore split into
some rigid base HTML and an idempotent, complete update function which only
makes necessary changes.
2020-10-20 17:27:16 +02:00
2023-11-30 11:44:32 +01:00
- **Idempotence** is key here, i.e. update functions may be called at any time
2020-10-20 17:27:16 +02:00
and should always render the component correctly.
- **Completeness** is equally important, i.e. update functions should render
the whole component, regardless of what triggered an update.
In effect, this means almost all DOM manipulation is done in update functions,
which greatly contributes to robustness and readability of the codebase.
2020-10-22 16:18:59 +02:00
As seen above this approach is quite verbose and ugly compared to JSX, for
2020-10-22 13:35:49 +02:00
example. However, it's very performant and can be further optimized
2020-10-20 17:27:16 +02:00
by checking for data changes, caching selectors, etc.
2020-10-22 13:35:49 +02:00
It is also simple to understand.
2020-10-20 17:27:16 +02:00
2020-10-22 13:35:49 +02:00
Reference:
2020-10-20 17:27:16 +02:00
- [TodoItem.js](./public/scripts/TodoItem.js)
- [TodoCustomList.js](./public/scripts/TodoCustomList.js)
#### 3.2.4. Reconciliation
Expectedly, the hardest part of the study was rendering a variable
amount of dynamic components efficiently. Here's a commented example
2020-10-22 13:35:49 +02:00
from the implementation outlining the reconciliation algorithm:
2020-10-20 17:27:16 +02:00
```js
export function TodoList(el) {
2023-11-26 11:54:04 +01:00
let items = [];
2020-10-20 17:27:16 +02:00
el.innerHTML = `<div class="items"></div>`;
2023-11-30 11:44:32 +01:00
el.addEventListener('todoItems', (e) => {
2023-11-26 11:54:04 +01:00
items = e.detail;
update();
});
2020-10-20 17:27:16 +02:00
2023-11-26 11:54:04 +01:00
function update() {
const container = el.querySelector('.items');
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Mark current children for removal
const obsolete = new Set(container.children);
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// Map current children by data-key
const childrenByKey = new Map();
obsolete.forEach((child) =>
2023-11-13 20:03:04 +01:00
childrenByKey.set(child.getAttribute('data-key'), child),
);
2023-11-19 14:48:31 +01:00
// Build new list of child elements from data
2023-11-26 11:54:04 +01:00
const children = items.map((item) => {
2023-11-19 14:48:31 +01:00
// Find existing child by data-key
let child = childrenByKey.get(item.id);
2020-10-20 17:27:16 +02:00
if (child) {
2023-11-19 14:48:31 +01:00
// If child exists, keep it
2020-10-20 17:27:16 +02:00
obsolete.delete(child);
} else {
2023-11-19 14:48:31 +01:00
// Otherwise, create new child
2020-10-20 17:27:16 +02:00
child = document.createElement('div');
child.classList.add('todo-item');
2020-10-22 13:35:49 +02:00
2023-11-19 14:48:31 +01:00
// Set data-key
2020-10-20 17:27:16 +02:00
child.setAttribute('data-key', item.id);
2020-10-22 13:35:49 +02:00
2023-11-19 14:48:31 +01:00
// Mount component
TodoItem(child);
2020-10-20 17:27:16 +02:00
}
2023-11-19 14:48:31 +01:00
// Update child
2023-11-30 11:44:32 +01:00
child.dispatchEvent(new CustomEvent('todoItem', { detail: item }));
2020-10-20 17:27:16 +02:00
return child;
});
2023-11-19 14:48:31 +01:00
// Remove obsolete children
obsolete.forEach((child) => container.removeChild(child));
2020-10-20 17:27:16 +02:00
2023-11-19 14:48:31 +01:00
// (Re-)insert new list of children
children.forEach((child, index) => {
2020-10-20 17:27:16 +02:00
if (child !== container.children[index]) {
container.insertBefore(child, container.children[index]);
}
});
}
}
2020-10-20 17:27:16 +02:00
```
2023-11-19 14:48:31 +01:00
It's very verbose, with lots of opportunity to introduce bugs.
2020-10-22 15:22:13 +02:00
Compared to a simple loop in JSX, this seems insane.
2020-10-21 11:28:36 +02:00
It is quite performant as it does minimal work but is otherwise messy;
2020-10-20 17:27:16 +02:00
definitely a candidate for a utility function or library.
2020-10-21 11:45:02 +02:00
### 3.3. Drag & Drop
2020-10-20 17:27:16 +02:00
Implementing drag & drop from scratch was challenging,
especially regarding browser/device consistency.
Using a library would have been a lot more cost-effective initially.
However, having a customized implementation paid off once I started
2020-10-22 16:18:59 +02:00
introducing animations as both had to be coordinated closely.
2020-10-21 11:28:36 +02:00
I can imagine this would have been a difficult problem
when using third party code for either.
2020-10-20 17:27:16 +02:00
The drag & drop implementation is (again) based on DOM events and integrates
well with the remaining architecture.
2020-10-22 16:18:59 +02:00
It's clearly the most complex part of the study but I was able to implement it
2020-10-20 17:27:16 +02:00
without changing existing code besides mounting behaviors and
adding event handlers.
2020-10-21 18:00:41 +02:00
I suspect the drag & drop implementation to have some subtle problems on
touch devices, as I haven't extensively tested them. Using a library for
identifying the gestures could be more sensible and would reduce costs in
testing browsers and devices.
2020-10-20 17:27:16 +02:00
Reference:
- [AppDraggable.js](./public/scripts/AppDraggable.js)
- [AppSortable.js](./public/scripts/AppSortable.js)
- [TodoList.js](./public/scripts/TodoList.js)
2020-10-21 11:45:02 +02:00
### 3.4. Animations
2020-10-20 17:27:16 +02:00
For the final product I wanted smooth animations for most user interactions.
This is a cross-cutting concern which was implemented using the
[FLIP](https://aerotwist.com/blog/flip-your-animations/) technique as devised
2020-10-21 11:45:02 +02:00
by [Paul Lewis](https://twitter.com/aerotwist).
2020-10-20 17:27:16 +02:00
2020-10-21 11:28:36 +02:00
Implementing FLIP animations without a large refactoring was the biggest
challenge of this case study, especially in combination with drag & drop.
2020-10-20 17:27:16 +02:00
After days of work I was able to implement the algorithm in isolation and
coordinate it with other concerns at the application's root level.
2020-10-21 13:05:29 +02:00
The `useCapture` mode of `addEventListener` proved to be very useful
2020-10-21 11:28:36 +02:00
in this case.
2020-10-20 17:27:16 +02:00
Reference:
- [AppFlip.js](./public/scripts/AppFlip.js)
- [TodoApp.js](./public/scripts/TodoApp.js)
2020-10-20 17:27:16 +02:00
2023-11-30 18:55:58 +01:00
## 4. Tooling
While no runtime dependencies or build steps were allowed,
I did introduce some local tooling to support the development experience.
As a quick start, here are the steps to get a local development server up and running:
- Install [git](https://git-scm.com/)
- Install [Node.js](https://nodejs.org/) (>= 20)
- Install an IDE (I used [VSCode](https://code.visualstudio.com/))
- Clone this repository
- Open a terminal in the repository's directory
- Run `npm install`
- Run `npm run dev`
- Visit [http://localhost:8080](http://localhost:8080)
The following sections describe the tooling in more detail.
### 4.1. Local Development Server
Because ES modules are not allowed under the `file://` protocol
I needed to run a local web server for development.
Initially, I used [serve](https://www.npmjs.com/package/serve)
which was good enough to get going but requires manually reloading
the application on every change.
Tooling for most modern frameworks supports _hot reloading_,
i.e. updating the application in place when changing source files.
Hot reloading provides fast feedback during development;
especially useful when fine-tuning visuals.
Unfortunately, I could not find a local development server
supporting some form of hot reloading
without introducing a framework or build system,
but I was able to implement a minimal local development server (~200 LOC)
with the following behavior:
- Changes to stylesheets or images will hot replace the changed resources.
- Other changes (e.g. JavaScript or HTML) will cause a full page reload.
While it's not proper [hot module replacement](https://webpack.js.org/concepts/hot-module-replacement/)
(which requires immense infrastructure),
it requires zero changes to the application source
and provides a similar experience because page reloads are fast.
Note that the local development server is highly experimental and is likely lacking
some features to be generally usable. See [/dev](./dev) for the implementation.
Feedback is highly appreciated.
### 4.2. Formatting and Linting
Basic code quality is provided by
- [Prettier](https://prettier.io),
- [ESLint](https://eslint.org), and
- [stylelint](https://stylelint.io).
I've set the ESLint parser to ES2020 to ensure only ES2020 code is allowed.
I've also added stylelint rules to check for rscss-compatible CSS.
Run these commands to try it out:
- `npm run format-check` to check formatting
- `npm run format` to apply formatting
- `npm run lint` to lint JavaScript
- `npm run lint-styles` to lint CSS
These tools only required minimal configuration to be effective. They also
2023-12-02 11:06:09 +01:00
integrate well with VSCode so I've rarely had to run these manually.
2023-11-30 18:55:58 +01:00
### 4.3. Testing
2020-10-20 17:27:16 +02:00
2023-11-26 00:43:55 +01:00
I've implemented some end-to-end and unit tests
2023-05-13 18:20:50 +02:00
using [Playwright](https://playwright.dev/).
This was straightforward besides small details like the `*.mjs` extension
and the fact that you cannot use named imports when importing from
`public/scripts`.
2023-11-30 18:55:58 +01:00
While running a local web server (see above), you can run the tests with
- `npm run test` for headless tests, or
- `npm run test-ui` for interactive mode.
These might ask you to install Playwright; just follow the instructions.
2023-05-13 18:20:50 +02:00
There's a lot more to explore here, but it's not much different from
testing other frontend stacks. It's actually simpler as there was zero
configuration and just one dependency.
2023-11-26 00:43:55 +01:00
Reference:
- [addItem.test.mjs](./test/e2e/addItem.test.mjs)
- [util.test.mjs](./test/unit/util.test.mjs)
2023-11-30 18:55:58 +01:00
#### 4.3.1. Code Coverage
2023-11-26 00:43:55 +01:00
I was able to set up code coverage (at least for end-to-end tests) via
[Playwright's code coverage feature](https://playwright.dev/docs/api/class-coverage)
and [c8](https://github.com/bcoe/c8).
This introduced another dependency and was slightly more involved to get right,
e.g. mapping localhost URLs to file URLs.
Use `npm run test-coverage` to run the tests and produce an LCOV test coverage
report in `./coverage`.
Note that the implementation is specific to the project structure,
2023-11-26 00:46:36 +01:00
e.g. `/public` as web root and port `8080` are hard-coded.
2023-11-24 17:44:41 +01:00
2023-05-13 18:20:50 +02:00
Reference:
2023-11-26 00:43:55 +01:00
- [test-coverage.sh](./scripts/test-coverage.sh)
- [coverage.mjs](./test/coverage.mjs)
2020-10-20 17:27:16 +02:00
2023-12-02 12:33:47 +01:00
### 4.4. Pipeline
I've added a simple CI/CD pipeline via GitHub Actions.
It runs linters and tests, and deploys to GitHub pages on success.
This was straight-forward and is orthogonal to the application code and other tooling.
Reference:
- [pipeline.yml](./.github/workflows/pipeline.yml)
2020-10-21 11:28:36 +02:00
## 5. Assessment
2020-10-20 17:27:16 +02:00
### 5.1. User Experience
2020-10-21 14:25:49 +02:00
Most important features from the original TeuxDeux application are implemented
and usable:
- Daily to-do lists
- Add/edit/delete to-do items
- Custom to-do lists
- Add/edit/delete custom to-do lists
- Drag & drop to-do items across lists
- Reorder custom to-do lists via drag & drop
- Local Storage persistence
2020-10-21 14:25:49 +02:00
Additionally, most interactions are smoothly animated at 60 frames per second.
In particular, dragging and dropping gives proper visual feedback
2020-10-21 17:44:39 +02:00
when elements are reordered.
_The latter was an improvement over the original application when I started
working on the case study in 2019. In the meantime, the TeuxDeux
2020-10-22 13:35:49 +02:00
team released an update with a much better drag & drop experience. Great job!_
2020-10-21 14:25:49 +02:00
One notable missing feature is Markdown support. It would be insensible
to implement Markdown from scratch; this is a valid candidate for using
an external library as it is entirely orthogonal to the remaining codebase.
2020-10-21 18:00:41 +02:00
The application has been tested on latest Chrome, Firefox, Safari,
and Safari on iOS.
2020-10-21 14:25:49 +02:00
2020-10-22 16:18:59 +02:00
_TODO Test more browsers and devices._
2020-10-22 13:35:49 +02:00
2023-12-02 11:06:09 +01:00
A fresh load of the original TeuxDeux application transfers around **1.2 MB**
and finishes loading at over **1000 ms**, sometimes up to 2000ms
(measured in 12/2023).
Reloads finish at around **700 ms**.
2020-10-21 17:44:39 +02:00
2023-12-02 11:06:09 +01:00
With a transferred size of around **55 KB**, the vanilla application consistently
2020-10-22 14:44:31 +02:00
loads in **300-500 ms**&mdash;not minified and with each script, stylesheet and icon
2023-12-02 11:06:09 +01:00
served as an individual file. Reloads finish at **100-200 ms**; again, not
2020-10-22 14:44:31 +02:00
optimized at all (with e.g. asset hashing/indefinite caching).
2023-12-02 11:06:09 +01:00
_To be fair, my implementation misses quite a few features from the original. I suspect a fully equivalent clone to be well below 100 KB transfer, though._
2020-10-21 17:44:39 +02:00
_TODO Run more formal performance tests and add figures for the results._
2020-10-20 17:27:16 +02:00
### 5.2. Code Quality
Unfortunately, it is quite hard to find undisputed, objective measurements
for code quality (besides trivialities like code style, linting, etc.).
2020-10-22 13:35:49 +02:00
The only generally accepted assessment seems to be peer reviewal.
2020-10-20 17:27:16 +02:00
To have at least some degree of assessment of the code's quality,
2020-10-21 14:25:49 +02:00
the following sections summarize relevant facts about the codebase
and some opinionated statements based on my experience in the industry.
2020-10-20 17:27:16 +02:00
#### 5.2.1. The Good
- No build steps
2020-10-21 11:28:36 +02:00
- No external dependencies at runtime besides polyfills
2020-10-23 12:06:29 +02:00
- No dependency maintenance
- No breaking changes to monitor
2020-10-20 17:27:16 +02:00
- Used only standard technologies:
- Plain HTML, CSS and JavaScript
2020-10-21 11:28:36 +02:00
- Standard DOM APIs
2020-10-20 17:27:16 +02:00
- Very few concepts introduced:
- Mount functions (loosely mapped by CSS class names)
2020-10-22 15:22:13 +02:00
- State separated from the DOM
2020-10-22 14:44:31 +02:00
- Idempotent updates
- Data flow using custom events
2020-10-20 17:27:16 +02:00
- Compare the proposed architecture to the API/conceptual surface of Angular or React...
- Progressive developer experience
- Markup, style, and behavior are orthogonal and can be developed separately.
- Adding behavior has little impact on the markup besides adding classes.
- Debugging is straight-forward using modern browser developer tools.
- The app can be naturally enhanced from the outside by handling/dispatching
events (just like you can naturally animate some existing HTML).
- Little indirection
- Low coupling
- The result is literally just a bunch of HTML, CSS, and JS files.
2023-05-13 18:20:50 +02:00
- Straight-forward, zero-config testing with Playwright
2023-12-02 11:06:09 +01:00
- Includes code coverage
2020-10-20 17:27:16 +02:00
2023-12-02 11:06:09 +01:00
All source files (HTML, CSS and JS) combine to **under 3000 lines of code**,
2020-10-21 17:44:39 +02:00
including comments and empty lines.
2023-12-02 11:06:09 +01:00
For comparison, prettifying the original TeuxDeux's minified JS assets
yields **81602 LOC** (12/2023).
2020-10-22 14:44:31 +02:00
2023-12-02 11:06:09 +01:00
_To be fair, my implementation misses quite a few features from the original. I suspect a fully equivalent clone to be well below 10000 LOC, though._
2020-10-21 17:44:39 +02:00
2020-10-20 17:27:16 +02:00
#### 5.2.2. The Verbose
2020-10-21 11:28:36 +02:00
- Stylesheets are a bit verbose. SCSS would help here.
2020-10-20 17:27:16 +02:00
- Simple components require quite some boilerplate code.
- `el.querySelectorAll(':scope ...')` is somewhat default/expected and
2020-10-21 11:28:36 +02:00
would justify a helper.
2020-10-20 17:27:16 +02:00
- Listening to and dispatching events is slightly verbose.
2020-10-21 11:28:36 +02:00
- Although not used in this study,
2023-11-19 14:48:31 +01:00
event delegation seems not trivial to implement without code duplication.
2020-10-20 17:27:16 +02:00
2023-11-30 11:44:32 +01:00
Eliminating verbosity through build steps and a minimal set of helpers
2020-10-21 17:44:39 +02:00
would reduce the comparably low code size (see above) even further.
2020-10-20 17:27:16 +02:00
#### 5.2.3. The Bad
- Class names share a global namespace.
- Event names share a global namespace.
- Especially problematic for events that bubble up.
- No code completion in HTML strings.
2020-10-20 17:27:16 +02:00
- The separation between base HTML and dynamic rendering is not ideal
when compared to JSX, for example.
2020-10-22 16:18:59 +02:00
- JSX/virtual DOM techniques provide much better development ergonomics.
2020-10-20 17:27:16 +02:00
- Reconciliation is verbose, brittle and repetitive.
I wouldn't recommend the proposed technique
without a well-tested helper function, at least.
- You have to remember mounting behaviors correctly when
2020-10-21 11:28:36 +02:00
creating new elements. It would be helpful to automate this somehow,
2020-10-20 17:27:16 +02:00
e.g. watch elements of selector X (at all times) and ensure the desired
behaviors are mounted once on them.
- No type safety. I've always been a proponent of dynamic languages
but since TypeScript's type system provides the best of both worlds,
2020-10-20 17:27:16 +02:00
I cannot recommend using it enough.
2020-10-22 15:22:13 +02:00
- We're effectively locked out of using NPM dependencies that don't provide
browser-ready builds (ES modules or UMD).
2020-10-22 13:35:49 +02:00
- Most frameworks handle a lot of browser inconsistencies **for free** and
2020-10-22 15:22:13 +02:00
continuously monitor regressions with extensive test suites.
2020-10-21 17:44:39 +02:00
The cost of browser testing is surely a lot higher
when using a vanilla approach.
2020-10-20 17:27:16 +02:00
2020-10-22 13:35:49 +02:00
---
2020-10-22 14:44:31 +02:00
Besides the issues described above, I believe the codebase is well organized
2020-10-22 13:35:49 +02:00
and there are clear paths for bugfixes and feature development.
2020-10-22 14:44:31 +02:00
Since there's no third party code, bugs are easy to find and fix,
2020-10-22 13:35:49 +02:00
and there are no dependency limitations to work around.
A certain degree of DOM API knowledge is required but I believe this
should be a goal for any web developer.
2020-10-20 17:27:16 +02:00
### 5.3. Generality of Patterns
Assessing the generality of the discovered techniques objectively is
2020-10-22 14:44:31 +02:00
not really possible without production usage. From my experience, however,
I can't imagine any scenario where mount functions, event-based data flow etc.
are not applicable. The underlying principles power the established frameworks,
after all:
2020-10-20 17:27:16 +02:00
- State is separated from the DOM (React, Angular, Vue).
- Rendering is idempotent and complete (React's pure `render` function).
- One-way data flow (React)
An open question is if these patterns hold for library authors.
Although not considered during the study, some observations can be made:
- The JavaScript itself would be fine to share as ES modules.
2023-12-02 11:06:09 +01:00
- Event naming needs great care, as dispatching (bubbling) events
from imported behaviors can trigger parent listeners in consumer code.
- Can be mitigated by providing options to prefix or map event names.
- CSS names share a global namespace and need to be managed as well.
2023-12-02 11:06:09 +01:00
- Can also be mitigated by prefixing, however making the JavaScript
a bit more complex.
2020-10-21 11:28:36 +02:00
## 6. Conclusion
2020-10-20 17:27:16 +02:00
2023-12-02 11:06:09 +01:00
The result of this study is a working to-do application with decent UI/UX and
2020-10-20 17:27:16 +02:00
most of the functionality of the original TeuxDeux app,
2020-10-21 11:28:36 +02:00
built using only standard web technologies.
2020-10-22 15:22:13 +02:00
It comes with better overall performance
at a fraction of the code size and bandwidth.
2020-10-20 17:27:16 +02:00
The codebase seems manageable through a handful of simple concepts,
although it is quite verbose and even messy in some areas.
2020-10-21 11:28:36 +02:00
This could be mitigated by a small number of helper functions and
simple build steps (e.g. SCSS and TypeScript).
2020-10-20 17:27:16 +02:00
The study's method helped discovering patterns and techniques that
are at least on par with a framework-based approach for the given subject,
2020-10-22 16:18:59 +02:00
without diverging into building a custom framework.
A notable exception to the latter is rendering variable numbers of elements
in a concise way. I was unable to eliminate the verbosity involved
in basic but efficient reconciliation.
2020-10-20 17:27:16 +02:00
Further research is needed in this area, but for now this appears to be
a valid candidate for a (possibly external) general-purpose utility.
2020-10-22 16:18:59 +02:00
When looking at the downsides, remember that all of the individual parts are
2020-10-20 17:27:16 +02:00
self-contained, highly decoupled, portable, and congruent to the web platform.
2020-10-23 12:06:29 +02:00
The resulting implementation cannot "rust", by definition, as no dependencies
can become out of date.
2020-10-20 17:27:16 +02:00
2020-10-22 16:18:59 +02:00
Another thought to be taken with a grain of salt: I believe frameworks
make simple tasks even simpler, but hard tasks (e.g. implementing cross-cutting
2020-10-22 15:22:13 +02:00
concerns or performance optimizations) often more difficult.
2020-10-20 17:27:16 +02:00
---
2020-10-21 11:28:36 +02:00
Setting some constraints up-front forced me to challenge
my assumptions and preconceptions about vanilla web development.
It was quite liberating to avoid general-purpose utilities and
get things done with what's readily available.
2023-12-02 11:06:09 +01:00
While I think the study is relatively complete, there's always more to explore.
[Ideas, questions, bug reports](https://github.com/morris/vanilla-todo/issues) and
pull requests are more than welcome!
2020-10-20 17:27:16 +02:00
2023-12-02 11:06:09 +01:00
Finally, this case study does not question using dependencies, libraries or frameworks
in general&mdash;code sharing is an essential part of software engineering.
2020-10-20 17:27:16 +02:00
It was a constrained experiment designed to discover novel methods
for vanilla web development and, hopefully,
inspire innovation and further research in the area.
2023-12-02 11:06:09 +01:00
## 7. Beyond Vanilla
2020-10-20 17:27:16 +02:00
2023-12-02 11:06:09 +01:00
As detailed in the assessment, the result of the case study
could be significantly improved if build steps and helpers were allowed.
Beyond the strict rules I've used in this experiment,
here are a few ideas I'd like to see explored in the future:
2020-10-20 17:27:16 +02:00
- Run another case study with TypeScript, SCSS, and build steps (seems promising).
2023-12-02 11:06:09 +01:00
- Extrapolate deep utility functions (e.g. `reconcile()`) to mitigate some of the discovered downsides.
2020-10-20 17:27:16 +02:00
- Experiment with architectures based on virtual DOM rendering and standard DOM events.
- Compile discovered rules, patterns and techniques into a comprehensive guide.
2020-10-21 17:44:39 +02:00
2020-10-23 12:06:29 +02:00
Case studies constrained by a set of formal rules are an effective way to find
new patterns and techniques in a wide range of domains.
I'd love to see similar experiments in the future.
2020-10-21 17:44:39 +02:00
## 8. Appendix
2021-01-02 15:57:47 +01:00
### 8.1. Links
2020-10-21 17:44:39 +02:00
General resources I've used extensively:
2020-10-22 15:22:13 +02:00
- [MDN Web Docs](https://developer.mozilla.org) as a reference for DOM APIs
- [Can I use...](https://caniuse.com) as a reference for browser support
- [React](https://reactjs.org) as inspiration for the architecture
2020-10-21 17:44:39 +02:00
Useful articles regarding FLIP animations:
- [FLIP Your Animations (aerotwist.com)](https://aerotwist.com/blog/flip-your-animations)
- [Animating Layouts with the FLIP Technique (css-tricks.com)](https://css-tricks.com/animating-layouts-with-the-flip-technique)
- [Animating the Unanimatable (medium.com)](https://medium.com/developers-writing/animating-the-unanimatable-1346a5aab3cd)
Projects I've inspected for drag & drop architecture:
- [React DnD](https://github.com/react-dnd/react-dnd/)
2020-10-21 17:44:39 +02:00
- [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)
- [dragula](https://github.com/bevacqua/dragula)
2020-10-28 16:21:12 +01:00
2021-01-02 15:57:47 +01:00
### 8.2. Response
#### 10/2020
- Trending on [Hacker News](https://news.ycombinator.com/item?id=24893247)
- [Lobsters](https://lobste.rs/s/5gcrxh/case_study_on_vanilla_web_development)
- [@desandro (Twitter)](https://twitter.com/desandro/status/1321095247091433473)
(developer for the original TeuxDeux)
- [Reddit](https://www.reddit.com/r/javascript/comments/jj10k9/vanillatodo_a_case_study_on_viable_techniques_for/)
Thanks!
2020-10-28 16:21:12 +01:00
## 9. Changelog
2023-12-02 11:06:09 +01:00
### 12/2023
2023-12-02 12:19:31 +01:00
- Added GitHub action for running checks and deployment
2023-12-02 11:06:09 +01:00
- Edited closing section
- Updated numbers
2023-11-13 20:03:04 +01:00
### 11/2023
2023-11-30 18:55:58 +01:00
- Introduced [tooling section](#4-tooling)
- Refactored business logic into pure functional module
- Added support for [code coverage](#431-code-coverage)
- Added [local development server](#41-local-development-server) with hot reloading
2023-11-19 15:07:08 +01:00
- Fixed some visual issues
- Updated dependencies
2023-11-13 20:03:04 +01:00
### 05/2023
2023-11-19 15:07:08 +01:00
- Added basic testing
- Fixed stylelint errors
- Updated dependencies
2022-08-06 16:56:52 +02:00
### 08/2022
- Small improvements
2023-11-19 15:07:08 +01:00
- Fixed date seeking bug on Safari
2022-08-06 16:56:52 +02:00
### 05/2022
- Refactored for ES2020
- Refactored for event-driven communication exclusively
- Moved original ES5-based version of the study to [/es5](./es5)
2022-05-09 16:51:41 +02:00
- Added assessment regarding library development
- Added date picker
2021-01-02 15:57:47 +01:00
### 01/2021
- Added [response section](#82-response)
2021-01-02 15:57:47 +01:00
2020-10-28 16:21:12 +01:00
### 10/2020
2020-10-29 12:21:05 +01:00
- Refactored for `dataset` [#2](https://github.com/morris/vanilla-todo/issues/2) &mdash;
[@opethrocks](https://github.com/opethrocks)
2020-10-28 16:21:12 +01:00
- Fixed [#3](https://github.com/morris/vanilla-todo/issues/3) (navigation bug) &mdash;
[@anchepiece](https://github.com/anchepiece),
[@jcoussard](https://github.com/jcoussard)
- Fixed [#4](https://github.com/morris/vanilla-todo/issues/4) (double item creation) &mdash;
[@n0nick](https://github.com/n0nick)
- Fixed [#1](https://github.com/morris/vanilla-todo/issues/4) (bad links) &mdash;
[@roryokane](https://github.com/roryokane)
2023-11-19 15:07:08 +01:00
- Initial version