1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-22 21:52:54 +02:00

readme, cleanups, polyfills

This commit is contained in:
Morris Brodersen
2020-10-21 11:28:36 +02:00
parent e36cdfeb11
commit 4ed979b0fc
6 changed files with 98 additions and 76 deletions

View File

@@ -12,6 +12,12 @@ module.exports = {
}, },
rules: {}, rules: {},
settings: { settings: {
polyfills: ['Array.from', 'Set', 'Map', 'fetch', 'Object.assign'], polyfills: [
'Set',
'Map',
'fetch',
'Object.assign',
'requestAnimationFrame',
],
}, },
}; };

112
README.md
View File

@@ -1,17 +1,47 @@
# VANILLA TODO # VANILLA TODO
A [TeuxDeux](https://teuxdeux.com) clone in plain HTML, CSS and A [TeuxDeux](https://teuxdeux.com) clone in plain HTML, CSS and
JavaScript (zero dependencies). JavaScript, with zero dependencies.
It's fully animated and runs smoothly at 60 FPS. It's fully animated and runs smoothly at 60 FPS.
More importantly, it's also a More importantly, it's also a
**case study on viable techniques and patterns for vanilla web development.** **case study on viable techniques and patterns for vanilla web development.**
[Try it online →](https://github.com/morris/vanilla-todo) **[Try it online →](https://github.com/morris/vanilla-todo)**
_This document is a "live" case study, expected to evolve a bit over time. _This document presents a "live" case study, expected to evolve a bit over time.
Intermediate understanding of the web platform is required to follow through._ 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 Function Pattern](#321-mount-function-pattern)
- [3.2.2. Data Flow](#322-data-flow)
- [3.2.3. Rendering](#323-rendering)
- [3.2.4. Reconciliation](#324-reconciliation)
- [3.2.5. Drag & Drop](#325-drag--drop)
- [3.2.6. Animations](#326-animations)
- [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)
- [5.2.2. The Verbose](#521-the-verbose)
- [5.2.3. The Bad](#521-the-bad)
- [5.3. Generality of Patterns](#53-generality-of-patterns)
- [6. Conclusion](#6-conclusion)
- [7. What's Next?](#7-whats-next)
## 1. Motivation ## 1. Motivation
I believe too little has been invested in researching I believe too little has been invested in researching
@@ -90,7 +120,7 @@ This includes testing major browsers and devices.
The resulting implementation should adhere to The resulting implementation should adhere to
established code quality standards in the industry. established code quality standards in the industry.
This will be hard to do objectively, as we will see later. This will be difficult to assess objectively, as we will see later.
#### 2.3.3. Generality of Patterns #### 2.3.3. Generality of Patterns
@@ -299,7 +329,7 @@ See for example:
- [TodoItem.js](./public/scripts/TodoItem.js) - [TodoItem.js](./public/scripts/TodoItem.js)
- [TodoItemInput.js](./public/scripts/TodoItemInput.js) - [TodoItemInput.js](./public/scripts/TodoItemInput.js)
### 3.2.2. Data Flow #### 3.2.2. Data Flow
I found it effective to implement one-way data flow similar to React's approach. I found it effective to implement one-way data flow similar to React's approach.
@@ -323,10 +353,10 @@ See for example:
- [TodoDay.js](./public/scripts/TodoDay.js) - [TodoDay.js](./public/scripts/TodoDay.js)
- [TodoStore.js](./public/scripts/TodoStore.js) - [TodoStore.js](./public/scripts/TodoStore.js)
### 3.2.3. Rendering #### 3.2.3. Rendering
Naively re-rendering a whole component using `.innerHTML` should be avoided, Naively re-rendering a whole component using `.innerHTML` should be avoided,
as this may hurt performance and may likely break important functionality such as as this may hurt performance and will likely break important functionality such as
input state, focus, text selection etc. which browsers have already been input state, focus, text selection etc. which browsers have already been
optimizing for years. optimizing for years.
@@ -355,7 +385,7 @@ See for example:
Expectedly, the hardest part of the study was rendering a variable Expectedly, the hardest part of the study was rendering a variable
amount of dynamic components efficiently. Here's a commented example amount of dynamic components efficiently. Here's a commented example
from the implementation: from the implementation outlining the algorithm:
```js ```js
/* global VT */ /* global VT */
@@ -405,7 +435,7 @@ VT.TodoList = function (el) {
container.removeChild(child); container.removeChild(child);
}); });
// insert new list of children (may reorder existing) // insert new list of children (may reorder existing children)
children.forEach(function (child, index) { children.forEach(function (child, index) {
if (child !== container.children[index]) { if (child !== container.children[index]) {
container.insertBefore(child, container.children[index]); container.insertBefore(child, container.children[index]);
@@ -419,9 +449,9 @@ VT.TodoList = function (el) {
}; };
``` ```
Very verbose and lots of opportunity to introduce bugs. It's very verbose and has lots of opportunity to introduce bugs.
Compared with a simple loop in JSX, this seems insane. Compared with a simple loop in JSX, this seems insane.
It is quite performant but otherwise clearly messy; It is quite performant as it does minimal work but is otherwise messy;
definitely a candidate for a utility function or library. definitely a candidate for a utility function or library.
#### 3.2.5. Drag & Drop #### 3.2.5. Drag & Drop
@@ -432,7 +462,8 @@ especially regarding browser/device consistency.
Using a library would have been a lot more cost-effective initially. Using a library would have been a lot more cost-effective initially.
However, having a customized implementation paid off once I started However, having a customized implementation paid off once I started
introducing animations, as both had to be coordinated closely. introducing animations, as both had to be coordinated closely.
I can imagine this would have been a hassle when using third party code for either. I can imagine this would have been a difficult problem
when using third party code for either.
The drag & drop implementation is (again) based on DOM events and integrates The drag & drop implementation is (again) based on DOM events and integrates
well with the remaining architecture. well with the remaining architecture.
@@ -453,11 +484,12 @@ This is a cross-cutting concern which was implemented using the
[FLIP](https://aerotwist.com/blog/flip-your-animations/) technique as devised [FLIP](https://aerotwist.com/blog/flip-your-animations/) technique as devised
by [Paul Lewis](https://twitter.com/aerotwist) (thanks!). by [Paul Lewis](https://twitter.com/aerotwist) (thanks!).
Implementing FLIP animations without a large refactoring was the biggest challenge Implementing FLIP animations without a large refactoring was the biggest
of this case study, especially in combination with drag & drop. challenge of this case study, especially in combination with drag & drop.
After days of work I was able to implement the algorithm in isolation and 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. coordinate it with other concerns at the application's root level.
The `useCapture` mode of `addEventListener` proved to be very useful in this case. The `useCapture` mode of `addEventListener` was proven to be useful
in this case.
Reference: Reference:
@@ -468,16 +500,12 @@ Reference:
TODO TODO
## 5. Evaluation ## 5. Assessment
### 5.1. User Experience ### 5.1. User Experience
TODO TODO
- Great load performance
- Great rendering performance
- Works
### 5.2. Code Quality ### 5.2. Code Quality
Unfortunately, it is quite hard to find undisputed, objective measurements Unfortunately, it is quite hard to find undisputed, objective measurements
@@ -492,17 +520,14 @@ and some of my own opinions based on my experience in the industry.
#### 5.2.1. The Good #### 5.2.1. The Good
- No build steps - No build steps
- No external dependencies at runtime - No external dependencies at runtime besides polyfills
- Used only standard technologies: - Used only standard technologies:
- Plain HTML, CSS and JavaScript - Plain HTML, CSS and JavaScript
- DOM APIs, in particular: - Standard DOM APIs
- `querySelector` and `querySelectorAll`
- DOM Events (especially `CustomEvent`)
- Local Storage
- `requestAnimationFrame`
- Very few concepts introduced: - Very few concepts introduced:
- Mount functions (loosely mapped by CSS class names) - Mount functions (loosely mapped by CSS class names)
- Component = Rigid Base HTML + Event Listeners + Idempotent Update Function - Component = Rigid Base HTML + Event Listeners + Idempotent Update Function
- Data flow using DOM events
- Compare the proposed architecture to the API/conceptual surface of Angular or React... - Compare the proposed architecture to the API/conceptual surface of Angular or React...
- Progressive developer experience - Progressive developer experience
- Markup, style, and behavior are orthogonal and can be developed separately. - Markup, style, and behavior are orthogonal and can be developed separately.
@@ -516,16 +541,17 @@ and some of my own opinions based on my experience in the industry.
#### 5.2.2. The Verbose #### 5.2.2. The Verbose
- Stylesheets are a bit verbose. SCSS would help here.
- Simple components require quite some boilerplate code. - Simple components require quite some boilerplate code.
- SCSS would simplify stylesheets a lot. - ES5 is generally a lot more verbose than ES6.
- ES6 would be very helpful.
- Especially arrow functions, template literals, - Especially arrow functions, template literals,
and async/await would make the code more readable. and async/await would make the code more readable.
- ES6 modules would eliminate the need for a global namespace.
- `el.querySelectorAll(':scope ...')` is somewhat default/expected and - `el.querySelectorAll(':scope ...')` is somewhat default/expected and
would justify a simplified helper. would justify a helper.
- Listening to and dispatching events is slightly verbose. - Listening to and dispatching events is slightly verbose.
- Although not used in this study, event delegation is not trivial to - Although not used in this study,
implement without code duplication. event delegation is not trivial to implement without code duplication.
#### 5.2.3. The Bad #### 5.2.3. The Bad
@@ -534,9 +560,9 @@ and some of my own opinions based on my experience in the industry.
- Reconciliation is verbose, brittle and repetitive. - Reconciliation is verbose, brittle and repetitive.
I wouldn't recommend the proposed technique I wouldn't recommend the proposed technique
without a well-tested helper function, at least. without a well-tested helper function, at least.
- JSX/virtual DOMs provide much better development ergonomics. - JSX/virtual DOM techniques provide much better development ergonomics.
- You have to remember mounting behaviors correctly when - You have to remember mounting behaviors correctly when
creating new elements. It would be nice to automate this somehow, creating new elements. It would be helpful to automate this somehow,
e.g. watch elements of selector X (at all times) and ensure the desired e.g. watch elements of selector X (at all times) and ensure the desired
behaviors are mounted once on them. behaviors are mounted once on them.
- No type safety. I've always been a proponent of dynamic languages - No type safety. I've always been a proponent of dynamic languages
@@ -556,25 +582,22 @@ The underlying principles power the established frameworks, after all:
- Rendering is idempotent and complete (React's pure `render` function). - Rendering is idempotent and complete (React's pure `render` function).
- One-way data flow (React) - One-way data flow (React)
## Conclusion ## 6. Conclusion
The result of this study is a working todo application with decent UI/UX and The result of this study is a working todo application with decent UI/UX and
most of the functionality of the original TeuxDeux app, most of the functionality of the original TeuxDeux app,
built using only standard web technologies and with zero dependencies. built using only standard web technologies.
Some extra features were introduced to demonstrate the implementation of Some extra features were introduced to demonstrate the implementation of
cross-cutting concerns in the study's codebase. cross-cutting concerns in the study's codebase.
The codebase seems manageable through a handful of simple concepts, The codebase seems manageable through a handful of simple concepts,
although it is quite verbose and even messy in some areas. although it is quite verbose and even messy in some areas.
This could be mitigated by a small number of helper functions and
Setting some constraints up-front forced me to challenge simple build steps (e.g. SCSS and TypeScript).
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.
The study's method helped discovering patterns and techniques that The study's method helped discovering patterns and techniques that
are at least on par with a framework-based approach for the given subject, are at least on par with a framework-based approach for the given subject,
without diverging into building a custom framework, except for without diverging into building a custom framework. except for
rendering variable numbers of elements efficiently. rendering variable numbers of elements efficiently.
Further research is needed in this area, but for now this appears to be Further research is needed in this area, but for now this appears to be
a valid candidate for a (possibly external) general-purpose utility. a valid candidate for a (possibly external) general-purpose utility.
@@ -585,6 +608,11 @@ The resulting implementation cannot "rust", by definition.
--- ---
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.
As detailed in the discussion section, As detailed in the discussion section,
the study would likely be more convincing if build steps were allowed. the study would likely be more convincing if build steps were allowed.
Modern JavaScript and SCSS could reduce most of Modern JavaScript and SCSS could reduce most of
@@ -596,7 +624,7 @@ It was a constrained experiment designed to discover novel methods
for vanilla web development and, hopefully, for vanilla web development and, hopefully,
inspire innovation and further research in the area. inspire innovation and further research in the area.
## What's Next? ## 7. What's Next?
I'd love to hear feedback and ideas on any aspect of the case study. 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. It's still lacking in some important areas, e.g. testing techniques.

View File

@@ -21,6 +21,10 @@
<body> <body>
<div class="todo-app"></div> <div class="todo-app"></div>
<script
nomodule
src="https://polyfill.io/v3/polyfill.min.js?features=Set%2CMap%2CObject.assign%2Cfetch%2CrequestAnimationFrame%2CNodeList.prototype.forEach"
></script>
<script src="scripts/AppCollapsible.js"></script> <script src="scripts/AppCollapsible.js"></script>
<script src="scripts/AppDraggable.js"></script> <script src="scripts/AppDraggable.js"></script>
<script src="scripts/AppFlip.js"></script> <script src="scripts/AppFlip.js"></script>

View File

@@ -5,9 +5,8 @@ VT.TodoApp = function (el) {
var state = { var state = {
items: [], items: [],
customLists: [], customLists: [],
date: VT.formatDateId(new Date()), at: VT.formatDateId(new Date()),
index: 0, customAt: 0,
showLists: true,
}; };
el.innerHTML = [ el.innerHTML = [

View File

@@ -27,21 +27,21 @@ VT.TodoStore = function (el) {
done: false, done: false,
}); });
update({ items: state.items }); dispatch({ items: state.items });
}); });
el.addEventListener('checkItem', function (e) { el.addEventListener('checkItem', function (e) {
if (e.detail.item.done === e.detail.done) return; if (e.detail.item.done === e.detail.done) return;
e.detail.item.done = e.detail.done; e.detail.item.done = e.detail.done;
update({ items: state.items }); dispatch({ items: state.items });
}); });
el.addEventListener('saveItem', function (e) { el.addEventListener('saveItem', function (e) {
if (e.detail.item.label === e.detail.label) return; if (e.detail.item.label === e.detail.label) return;
e.detail.item.label = e.detail.label; e.detail.item.label = e.detail.label;
update({ items: state.items }); dispatch({ items: state.items });
}); });
el.addEventListener('moveItem', function (e) { el.addEventListener('moveItem', function (e) {
@@ -64,11 +64,11 @@ VT.TodoStore = function (el) {
item.index = index; item.index = index;
}); });
update({ items: state.items }); dispatch({ items: state.items });
}); });
el.addEventListener('deleteItem', function (e) { el.addEventListener('deleteItem', function (e) {
update({ dispatch({
items: state.items.filter(function (item) { items: state.items.filter(function (item) {
return item.id !== e.detail.id; return item.id !== e.detail.id;
}), }),
@@ -88,7 +88,7 @@ VT.TodoStore = function (el) {
title: e.detail.title || '', title: e.detail.title || '',
}); });
update({ customLists: state.customLists }); dispatch({ customLists: state.customLists });
}); });
el.addEventListener('saveList', function (e) { el.addEventListener('saveList', function (e) {
@@ -100,7 +100,7 @@ VT.TodoStore = function (el) {
list.title = e.detail.title; list.title = e.detail.title;
update({ customLists: state.customLists }); dispatch({ customLists: state.customLists });
}); });
el.addEventListener('moveList', function (e) { el.addEventListener('moveList', function (e) {
@@ -119,11 +119,11 @@ VT.TodoStore = function (el) {
item.index = index; item.index = index;
}); });
update({ customLists: state.customLists }); dispatch({ customLists: state.customLists });
}); });
el.addEventListener('deleteList', function (e) { el.addEventListener('deleteList', function (e) {
update({ dispatch({
customLists: state.customLists.filter(function (customList) { customLists: state.customLists.filter(function (customList) {
return customList.id !== e.detail.id; return customList.id !== e.detail.id;
}), }),
@@ -134,19 +134,19 @@ VT.TodoStore = function (el) {
var t = new Date(state.at); var t = new Date(state.at);
t.setDate(t.getDate() + e.detail); t.setDate(t.getDate() + e.detail);
update({ dispatch({
at: VT.formatDateId(t), at: VT.formatDateId(t),
}); });
}); });
el.addEventListener('seekHome', function () { el.addEventListener('seekHome', function () {
update({ dispatch({
at: VT.formatDateId(new Date()), at: VT.formatDateId(new Date()),
}); });
}); });
el.addEventListener('customSeek', function (e) { el.addEventListener('customSeek', function (e) {
update({ dispatch({
customAt: Math.max( customAt: Math.max(
0, 0,
Math.min(state.customLists.length - 1, state.customAt + e.detail) Math.min(state.customLists.length - 1, state.customAt + e.detail)
@@ -154,7 +154,7 @@ VT.TodoStore = function (el) {
}); });
}); });
function update(next) { function dispatch(next) {
Object.assign(state, next); Object.assign(state, next);
store(); store();
@@ -172,7 +172,7 @@ VT.TodoStore = function (el) {
} }
try { try {
update(JSON.parse(localStorage.todo)); dispatch(JSON.parse(localStorage.todo));
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);
} }
@@ -191,7 +191,7 @@ VT.TodoStore = function (el) {
} }
el.todoStore = { el.todoStore = {
update: update, dispatch: dispatch,
load: load, load: load,
}; };
}; };

View File

@@ -1,15 +0,0 @@
/* global VT */
window.VT = window.VT || {};
VT.TodoTrash = function (el) {
el.innerHTML = '<i class="app-icon" data-id="trashbin-24"></i>';
el.addEventListener('draggableDrop', function (e) {
el.dispatchEvent(
new CustomEvent('deleteItem', {
detail: e.detail.data.item,
bubbles: true,
})
);
});
};