2020-10-20 17:27:16 +02:00
|
|
|
# VANILLA TODO
|
|
|
|
|
|
|
|
A [TeuxDeux](https://teuxdeux.com) clone in plain HTML, CSS and
|
2020-10-21 11:28:36 +02:00
|
|
|
JavaScript, with zero dependencies.
|
2020-10-21 17:44:39 +02:00
|
|
|
It's fully animated and runs smoothly at 60 FPS,
|
2020-10-22 14:44:31 +02:00
|
|
|
with a total transferred size of roughly **35KB**.
|
2020-10-20 17:27:16 +02:00
|
|
|
|
|
|
|
More importantly, it's also a
|
2020-10-21 14:25:49 +02:00
|
|
|
**case study on viable techniques for vanilla web development.**
|
2020-10-20 17:27:16 +02:00
|
|
|
|
2020-10-21 18:01:19 +02:00
|
|
|
**[Try it online →](https://rawcdn.githack.com/morris/vanilla-todo/b4ce72a87b83015b593d270aa7f31adbd7e13f98/public/index.html)**
|
2020-10-20 17:27:16 +02:00
|
|
|
|
2020-10-21 11:28:36 +02:00
|
|
|
_This document presents a "live" case study, expected to evolve a bit over time.
|
2020-10-20 17:27:16 +02:00
|
|
|
Intermediate understanding of the web platform is required to follow through._
|
|
|
|
|
2020-10-21 14:58:21 +02:00
|
|
|
## TL;DR
|
|
|
|
|
2020-10-21 15:09:59 +02:00
|
|
|
- By introducing a [few simple concepts](#3-implementation),
|
|
|
|
vanilla web development seems viable at scale but comes with significant
|
|
|
|
verbosity and effort in browser testing.
|
|
|
|
The former may be solved by simple build steps (SCSS, TypeScript).
|
2020-10-22 13:35:49 +02:00
|
|
|
- At a fraction of the code size and bandwidth, the resulting product has
|
|
|
|
[comparable UX](#51-user-experience) over the original and better load and
|
|
|
|
rendering performance.
|
2020-10-21 14:58:21 +02:00
|
|
|
- Frameworks and libraries provide lots of value but there's only a few
|
2020-10-21 15:09:59 +02:00
|
|
|
[critical areas](#523-the-bad) where a vanilla approach is clearly inferior.
|
|
|
|
- Case studies constrained by a set of well-defined [rules](#22-rules)
|
|
|
|
are an effective way to find new patterns and techniques in any domain.
|
2020-10-21 14:58:21 +02:00
|
|
|
|
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)
|
2020-10-21 11:28:36 +02:00
|
|
|
- [4. Testing](#4-testing)
|
|
|
|
- [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)
|
|
|
|
- [7. What's Next?](#7-whats-next)
|
2020-10-21 17:44:39 +02:00
|
|
|
- [8. Appendix](#8-appendix)
|
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.
|
|
|
|
|
|
|
|
We're missing 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.
|
|
|
|
|
|
|
|
## 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 equivalent 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 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; I used ES5 for maximum support.
|
|
|
|
|
|
|
|
(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
|
2020-10-21 11:45:02 +02:00
|
|
|
[rscss](https://rscss.io) (devised by [Rico Sta. Cruz](https://ricostacruz.com))
|
|
|
|
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,
|
|
|
|
and I think it's a must-have for bigger projects.
|
|
|
|
|
|
|
|
ES6 modules are ruled out so all JavaScript lives under
|
|
|
|
a global namespace (`VT`). This works everywhere but has some downsides
|
|
|
|
e.g. cannot be statically analyzed and may miss code completion.
|
|
|
|
|
2020-10-21 17:44:39 +02:00
|
|
|
Polyfills are directly fetched from [polyfill.io](https://polyfill.io/).
|
|
|
|
I've set the `nomodule` script attribute so polyfills are only fetched
|
|
|
|
for older browsers.
|
|
|
|
|
2020-10-22 14:44:31 +02:00
|
|
|
Basic code quality (code style, linting) is guided by
|
|
|
|
[Prettier](https://prettier.io), [stylelint](https://stylelint.io) and
|
|
|
|
[ESLint](https://eslint.org).
|
2020-10-20 17:27:16 +02:00
|
|
|
I've set the ESLint parser to ES5 to ensure only ES5 code is allowed.
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
Naturally, the JS 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 yields a simple mental model and synergizes
|
|
|
|
with the DOM and styles:
|
|
|
|
|
|
|
|
```
|
|
|
|
.todo-list -> VT.TodoList
|
|
|
|
scripts/TodoList.js
|
|
|
|
styles/todo-list.css
|
|
|
|
|
|
|
|
.app-collapsible -> VT.AppCollapsible
|
|
|
|
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
|
|
|
|
|
|
|
_Mount functions_ take a DOM element as their (only) argument.
|
|
|
|
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
|
|
|
|
// safely initialize namespace
|
|
|
|
window.MYAPP = window.MYAPP || {};
|
|
|
|
|
|
|
|
// define mount function
|
|
|
|
// loosely mapped to ".hello-world"
|
|
|
|
MYAPP.HelloWorld = function (el) {
|
|
|
|
// define initial state
|
|
|
|
var state = {
|
|
|
|
title: 'Hello, World!',
|
|
|
|
description: 'An example vanilla component',
|
|
|
|
counter: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// set rigid base HTML
|
|
|
|
// no ES6 template literals :(
|
|
|
|
el.innerHTML = [
|
|
|
|
'<h1 class="title"></h1>',
|
|
|
|
'<p class="description"></p>',
|
|
|
|
'<div class="my-counter"></div>',
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
// mount sub-components
|
|
|
|
el.querySelectorAll('.my-counter').forEach(MYAPP.MyCounter);
|
|
|
|
|
|
|
|
// attach event listeners
|
|
|
|
el.addEventListener('modifyCounter', function (e) {
|
|
|
|
update({ counter: state.counter + e.detail });
|
|
|
|
});
|
|
|
|
|
|
|
|
// expose public interface
|
|
|
|
el.helloWorld = {
|
|
|
|
update: update,
|
|
|
|
};
|
|
|
|
|
2020-10-21 12:23:39 +02:00
|
|
|
// initial update
|
|
|
|
update();
|
|
|
|
|
2020-10-20 17:27:16 +02:00
|
|
|
// define idempotent update function
|
|
|
|
function update(next) {
|
|
|
|
// update state
|
|
|
|
// optionally optimize, e.g. bail out if state hasn't changed
|
|
|
|
Object.assign(state, next);
|
|
|
|
|
|
|
|
// update own HTML
|
|
|
|
el.querySelector('.title').innerText = state.title;
|
|
|
|
el.querySelector('.description').innerText = state.description;
|
|
|
|
|
|
|
|
// pass data to sub-scomponents
|
|
|
|
el.querySelector('.my-counter').myCounter.update({
|
|
|
|
value: state.counter,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// define another component
|
|
|
|
// loosely mapped to ".my-counter"
|
|
|
|
MYAPP.MyCounter = function (el) {
|
|
|
|
// define initial state
|
|
|
|
var state = {
|
|
|
|
value: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// set rigid base HTML
|
|
|
|
// no ES6 template literals :(
|
|
|
|
el.innerHTML = [
|
|
|
|
'<p>',
|
2020-10-21 12:23:39 +02:00
|
|
|
' <span class="value"></span>',
|
2020-10-20 17:27:16 +02:00
|
|
|
' <button class="increment">Increment</button>',
|
|
|
|
' <button class="decrement">Decrement</button>',
|
|
|
|
'</p>',
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
// attach event listeners
|
|
|
|
el.querySelector('.increment').addEventListener('click', function () {
|
|
|
|
// dispatch an action
|
|
|
|
// use .detail to transport data
|
|
|
|
el.dispatchEvent(
|
|
|
|
new CustomEvent('modifyCounter', {
|
|
|
|
detail: 1,
|
|
|
|
bubbles: true,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
el.querySelector('.decrement').addEventListener('click', function () {
|
|
|
|
// dispatch an action
|
|
|
|
// use .detail to transport data
|
|
|
|
el.dispatchEvent(
|
|
|
|
new CustomEvent('modifyCounter', {
|
|
|
|
detail: -1,
|
|
|
|
bubbles: true,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
// expose public interface
|
|
|
|
el.myCounter = {
|
|
|
|
update: update,
|
|
|
|
};
|
|
|
|
|
|
|
|
// define idempotent update function
|
|
|
|
function update(next) {
|
|
|
|
Object.assign(state, next);
|
|
|
|
|
|
|
|
el.querySelector('.value').innerText = state.value;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// mount HelloWorld component(s)
|
|
|
|
// any <div class="hello-world"></div> in the document will be mounted
|
|
|
|
document.querySelectorAll('.hello-world').forEach(MYAPP.HelloWorld);
|
|
|
|
```
|
|
|
|
|
|
|
|
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 `VT.TodoItem` and `VT.AppDraggable`.
|
2020-10-20 17:27:16 +02:00
|
|
|
|
2020-10-22 13:35:49 +02:00
|
|
|
Compared to React components, mount functions provide superior flexibility as
|
|
|
|
components and behaviors can be implemented using the same idiom.
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
- **Data flows downwards** from parent components to child components
|
|
|
|
through their public interfaces (usually `update` functions).
|
|
|
|
- **Actions flow upwards** through custom DOM events (bubbling up),
|
|
|
|
usually resulting in some parent component state change which is in turn
|
|
|
|
propagated downwards through `update` functions.
|
|
|
|
|
2020-10-22 13:35:49 +02:00
|
|
|
The data store is factored into a separate mount function (`VT.TodoStore`).
|
2020-10-21 11:45:02 +02:00
|
|
|
It only receives and dispatches events, and encapsulates any data manipulation.
|
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-20 17:27:16 +02:00
|
|
|
properly with standard APIs.
|
|
|
|
|
2020-10-22 13:35:49 +02:00
|
|
|
Reference:
|
2020-10-20 17:27:16 +02:00
|
|
|
|
|
|
|
- [TodoDay.js](./public/scripts/TodoDay.js)
|
|
|
|
- [TodoStore.js](./public/scripts/TodoStore.js)
|
|
|
|
|
2020-10-21 11:28:36 +02:00
|
|
|
#### 3.2.3. Rendering
|
2020-10-20 17:27:16 +02:00
|
|
|
|
|
|
|
Naively re-rendering a whole component using `.innerHTML` should be avoided,
|
2020-10-22 14:44:31 +02:00
|
|
|
as this may hurt performance and will likely break important functionality such
|
|
|
|
as input state, focus, text selection etc. which browsers have already been
|
2020-10-20 17:27:16 +02:00
|
|
|
optimizing for years.
|
|
|
|
|
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
|
|
|
|
|
|
|
- **Idempotency** is key here, i.e. update functions may be called at any time
|
|
|
|
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 13:35:49 +02:00
|
|
|
As seen above, this approach is quite verbose and ugly compared to JSX, for
|
|
|
|
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
|
|
|
|
/* global VT */
|
|
|
|
window.VT = window.VT || {};
|
|
|
|
|
|
|
|
VT.TodoList = function (el) {
|
|
|
|
var state = {
|
|
|
|
items: [],
|
|
|
|
};
|
|
|
|
|
|
|
|
el.innerHTML = '<div class="items"></div>';
|
|
|
|
|
|
|
|
function update(next) {
|
|
|
|
Object.assign(state, next);
|
|
|
|
|
|
|
|
var container = el.querySelector('.items');
|
|
|
|
|
|
|
|
// mark current children for removal
|
|
|
|
var obsolete = new Set(container.children);
|
|
|
|
|
2020-10-21 12:23:39 +02:00
|
|
|
// map current children by data-key
|
|
|
|
var childrenByKey = new Map();
|
|
|
|
|
|
|
|
obsolete.forEach(function (child) {
|
|
|
|
childrenByKey.set(child.getAttribute('data-key'), child);
|
|
|
|
});
|
|
|
|
|
2020-10-20 17:27:16 +02:00
|
|
|
// build new list of child elements from data
|
|
|
|
var children = state.items.map(function (item) {
|
2020-10-21 12:23:39 +02:00
|
|
|
// find existing child by data-key
|
|
|
|
var child = childrenByKey.get(item.id);
|
2020-10-20 17:27:16 +02:00
|
|
|
|
|
|
|
if (child) {
|
2020-10-21 12:23:39 +02:00
|
|
|
// if child exists, keep it
|
2020-10-20 17:27:16 +02:00
|
|
|
obsolete.delete(child);
|
|
|
|
} else {
|
|
|
|
// otherwise, create new child
|
|
|
|
child = document.createElement('div');
|
|
|
|
child.classList.add('todo-item');
|
2020-10-22 13:35:49 +02: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
|
|
|
|
|
|
|
// mount component function
|
2020-10-20 17:27:16 +02:00
|
|
|
VT.TodoItem(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
// update child
|
|
|
|
child.todoItem.update({ item: item });
|
|
|
|
|
|
|
|
return child;
|
|
|
|
});
|
|
|
|
|
|
|
|
// remove obsolete children
|
|
|
|
obsolete.forEach(function (child) {
|
|
|
|
container.removeChild(child);
|
|
|
|
});
|
|
|
|
|
2020-10-22 13:35:49 +02:00
|
|
|
// (re-)insert new list of children (may reorder existing children)
|
2020-10-20 17:27:16 +02:00
|
|
|
children.forEach(function (child, index) {
|
|
|
|
if (child !== container.children[index]) {
|
|
|
|
container.insertBefore(child, container.children[index]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
el.todoList = {
|
|
|
|
update: update,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
2020-10-21 11:28:36 +02:00
|
|
|
It's very verbose and has lots of opportunity to introduce bugs.
|
2020-10-20 17:27:16 +02:00
|
|
|
Compared with 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
|
|
|
|
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.
|
|
|
|
It's clearly the most complex part of the study, but I was able to implement it
|
|
|
|
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/AppDraggable.js)
|
|
|
|
- [TodoApp.js](./public/scripts/AppSortable.js)
|
|
|
|
|
|
|
|
## 4. Testing
|
|
|
|
|
2020-10-21 17:44:39 +02:00
|
|
|
_TODO_
|
2020-10-20 17:27:16 +02:00
|
|
|
|
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
|
2020-10-21 14:58:21 +02:00
|
|
|
- Local Storage persistence
|
2020-10-21 14:25:49 +02:00
|
|
|
|
|
|
|
Additionally, most interactions are smoothly animated at 60 frames per second.
|
2020-10-21 14:58:21 +02:00
|
|
|
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 some weeks ago. 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 13:35:49 +02:00
|
|
|
_TODO test more browsers and devices._
|
|
|
|
|
2020-10-22 14:44:31 +02:00
|
|
|
A fresh load of the original TeuxDeux application transfers around **435 KB** and
|
|
|
|
finishes loading around **1000 ms**, sometimes up to 2000ms
|
|
|
|
(measured on 10/21 2020).
|
|
|
|
Reloads finish at around **500ms**.
|
2020-10-21 17:44:39 +02:00
|
|
|
|
2020-10-22 14:44:31 +02:00
|
|
|
With a transferred size of around **35 KB**, the vanilla application consistently
|
|
|
|
loads in **300-500 ms**—not minified and with each script, stylesheet and icon
|
|
|
|
served as an individual file. Reloads finish at **100-200ms**; again, not
|
|
|
|
optimized at all (with e.g. asset hashing/indefinite caching).
|
|
|
|
|
|
|
|
_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-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 14:44:31 +02:00
|
|
|
- State separate from DOM
|
|
|
|
- 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.
|
|
|
|
|
2020-10-21 17:44:39 +02:00
|
|
|
All source files (HTML, CSS and JS) combine to **under 2500 lines of code**,
|
|
|
|
including comments and empty lines.
|
|
|
|
|
2020-10-22 14:44:31 +02:00
|
|
|
For comparison, prettifying the original TeuxDeux's minified JS application
|
|
|
|
bundle yields **48787 LOC** (10/21 2020).
|
|
|
|
|
|
|
|
_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.
|
2020-10-21 11:28:36 +02:00
|
|
|
- ES5 is generally a lot more verbose than ES6.
|
2020-10-20 17:27:16 +02:00
|
|
|
- Especially arrow functions, template literals,
|
|
|
|
and async/await would make the code more readable.
|
2020-10-21 11:28:36 +02:00
|
|
|
- ES6 modules would eliminate the need for a global namespace.
|
2020-10-20 17:27:16 +02:00
|
|
|
- `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,
|
|
|
|
event delegation is not trivial to implement without code duplication.
|
2020-10-20 17:27:16 +02:00
|
|
|
|
2020-10-21 17:44:39 +02:00
|
|
|
Eliminating verbosities through build steps and a minimal set of helpers
|
|
|
|
would reduce the comparably low code size (see above) even further.
|
|
|
|
|
2020-10-20 17:27:16 +02:00
|
|
|
#### 5.2.3. The Bad
|
|
|
|
|
|
|
|
- The separation between base HTML and dynamic rendering is not ideal
|
|
|
|
when compared to JSX, for example.
|
|
|
|
- Reconciliation is verbose, brittle and repetitive.
|
|
|
|
I wouldn't recommend the proposed technique
|
|
|
|
without a well-tested helper function, at least.
|
2020-10-21 11:28:36 +02:00
|
|
|
- JSX/virtual DOM techniques provide much better development ergonomics.
|
2020-10-20 17:27:16 +02:00
|
|
|
- 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 TypeScripts' type system provides the best of both worlds,
|
|
|
|
I cannot recommend using it enough.
|
2020-10-22 13:35:49 +02:00
|
|
|
- Most frameworks handle a lot of browser inconsistencies **for free** and
|
|
|
|
continuously test for 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)
|
|
|
|
|
2020-10-21 11:28:36 +02:00
|
|
|
## 6. Conclusion
|
2020-10-20 17:27:16 +02:00
|
|
|
|
|
|
|
The result of this study is a working todo application with decent UI/UX and
|
|
|
|
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-20 17:27:16 +02:00
|
|
|
Some extra features were introduced to demonstrate the implementation of
|
|
|
|
cross-cutting concerns in the study's codebase.
|
|
|
|
|
|
|
|
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-21 11:28:36 +02:00
|
|
|
without diverging into building a custom framework. except for
|
2020-10-20 17:27:16 +02:00
|
|
|
rendering variable numbers of elements efficiently.
|
|
|
|
Further research is needed in this area, but for now this appears to be
|
|
|
|
a valid candidate for a (possibly external) general-purpose utility.
|
|
|
|
|
|
|
|
When looking at the downsides, remember that all of the individual parts are
|
|
|
|
self-contained, highly decoupled, portable, and congruent to the web platform.
|
|
|
|
The resulting implementation cannot "rust", by definition.
|
|
|
|
|
|
|
|
---
|
|
|
|
|
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.
|
|
|
|
|
2020-10-22 14:44:31 +02:00
|
|
|
As detailed in the assessment,
|
2020-10-20 17:27:16 +02:00
|
|
|
the study would likely be more convincing if build steps were allowed.
|
|
|
|
Modern JavaScript and SCSS could reduce most of
|
|
|
|
the unnecessarily verbose parts to a minimum.
|
|
|
|
|
|
|
|
Finally, this case study does not question
|
|
|
|
using dependencies or frameworks in general.
|
|
|
|
It was a constrained experiment designed to discover novel methods
|
|
|
|
for vanilla web development and, hopefully,
|
|
|
|
inspire innovation and further research in the area.
|
|
|
|
|
2020-10-21 11:28:36 +02:00
|
|
|
## 7. What's Next?
|
2020-10-20 17:27:16 +02:00
|
|
|
|
|
|
|
I'd love to hear feedback and ideas on any aspect of the case study.
|
|
|
|
It's still lacking in some important areas, e.g. testing techniques.
|
|
|
|
|
|
|
|
Pull requests, questions, and bug reports are more than welcome!
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
Here are a few ideas I'd like to see explored in the future:
|
|
|
|
|
|
|
|
- Run another case study with TypeScript, SCSS, and build steps (seems promising).
|
|
|
|
- Research validation rules for utility functions and external dependencies.
|
|
|
|
- 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
|
|
|
|
|
|
|
## 8. Appendix
|
|
|
|
|
|
|
|
General resources I've used extensively:
|
|
|
|
|
|
|
|
- [MDN Web Docs](https://developer.mozilla.org)
|
|
|
|
- [Can I use...](https://caniuse.com)
|
|
|
|
- [React](https://reactjs.org)
|
|
|
|
|
|
|
|
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://react-dnd.github.io)
|
|
|
|
- [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)
|
|
|
|
- [dragula](https://github.com/bevacqua/dragula)
|