1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-13 17:43:59 +02:00
This commit is contained in:
Morris Brodersen
2023-12-10 14:28:36 +01:00
parent 6623d0ecba
commit a01590af93

View File

@@ -114,6 +114,7 @@ However, it is lacking in some key areas:
- Routing - Routing
- Asynchronous resource requests - Asynchronous resource requests
- Complex forms
- Server-side rendering - Server-side rendering
### 2.2. Rules ### 2.2. Rules
@@ -139,15 +140,15 @@ The results are going to be assessed by three major concerns:
#### 2.3.1. User Experience #### 2.3.1. User Experience
The resulting product should be comparable to or better The product should be comparable to or better
than the original regarding functionality, performance and design. than the original regarding functionality, performance and design.
This includes testing major browsers and devices. This includes testing major browsers and devices.
#### 2.3.2. Code Quality #### 2.3.2. Code Quality
The resulting implementation should adhere to The implementation should be _maintainable_ and
established code quality standards in the industry. follow established code quality standards.
This will be difficult to assess objectively, as we will see later. This will be difficult to assess objectively, as we will see later.
@@ -158,16 +159,16 @@ range of scenarios.
## 3. Implementation ## 3. Implementation
This section walks through the resulting implementation, highlighting techniques This section walks through the implementation, highlighting techniques
and problems found during the process. You're encouraged to inspect the and problems found during the process. You're encouraged to inspect the
[source code](./public) alongside this section. [source code](./public) alongside this section.
### 3.1. Basic Structure ### 3.1. Basic Structure
Since build steps are ruled out, the codebase is organized around Since build steps are ruled out, the codebase consists of
plain HTML, CSS and JS files. The HTML and CSS mostly follows plain HTML, CSS and JS files. The HTML and CSS follows
[rscss](https://ricostacruz.com/rscss/) (devised by [Rico Sta. Cruz](https://ricostacruz.com)) [rscss](https://ricostacruz.com/rscss/) (devised by [Rico Sta. Cruz](https://ricostacruz.com))
which yields an intuitive, component-oriented structure. resulting in an intuitive, component-oriented structure.
The stylesheets are slightly verbose. The stylesheets are slightly verbose.
[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)
@@ -183,8 +184,9 @@ comments to functions to get additional code completion in VSCode.
This helps, but using TypeScript would be much safer and less verbose. This helps, but using TypeScript would be much safer and less verbose.
Note that I've opted out of web components completely. Note that I've opted out of web components completely.
I can't clearly articulate what I find problematic about them My attempts to refactor the implementation using web components
but I never missed them throughout the study. either added more complexity, or did not show significant value
over the initial, more basic approach.
--- ---
@@ -192,8 +194,7 @@ The basic structure comes with some boilerplate,
e.g. referencing all the individual stylesheets and scripts from the HTML; e.g. referencing all the individual stylesheets and scripts from the HTML;
probably enough to justify a simple build step. probably enough to justify a simple build step.
It is otherwise straight-forward and trivial to understand It is otherwise straight-forward—literally a bunch of HTML, CSS and JS files.
(literally just a bunch of HTML, CSS and JS files).
### 3.2. JavaScript Architecture ### 3.2. JavaScript Architecture
@@ -230,7 +231,7 @@ _Mount functions_ take a DOM element as their first argument.
Their responsibility is to set up initial state, event listeners, and Their responsibility is to set up initial state, event listeners, and
provide behavior and rendering for the target element. provide behavior and rendering for the target element.
Here's a "Hello, World!" example (implementing a simple counter) of mount functions: For example, this mount function implements a simple counter:
```js ```js
// Define mount function. // Define mount function.
@@ -270,32 +271,30 @@ export function MyCounter(el) {
// This event handler supports the increment/decrement actions above, // This event handler supports the increment/decrement actions above,
// as well as resetting the counter from the outside. // as well as resetting the counter from the outside.
el.addEventListener('counter', (e) => { el.addEventListener('counter', (e) => {
// Update state and re-render // Update state and re-render.
value = e.detail; value = e.detail;
update(); update();
}); });
// Define idempotent update function // Define idempotent update function.
function update() { function update() {
el.querySelector('.value').innerText = value; el.querySelector('.value').innerText = value;
} }
// Initial update // Initial update.
update(); update();
} }
// Mount MyCounter component(s) // Mount MyCounter component(s).
// Any <div class="my-counter"></div> in the document will be mounted // Any <div class="my-counter"></div> in the document will be mounted.
document.querySelectorAll('.my-counter').forEach(MyCounter); document.querySelectorAll('.my-counter').forEach(MyCounter);
``` ```
This comes with quite some boilerplate but has useful properties, This comes with quite some boilerplate but has useful properties,
as we will see in the following sections. as we will see in the following sections.
Note that any part of a mount function is entirely optional. Note that a mount function does not have to set any base HTML,
For example, a mount function does not have to set any base HTML,
and may instead only set event listeners to enable some behavior. and may instead only set event listeners to enable some behavior.
Also note that an element can be mounted with multiple mount functions. Also note that an element can be mounted with multiple mount functions.
For example, to-do items are mounted with `TodoItem` and `AppDraggable`. For example, to-do items are mounted with `TodoItem` and `AppDraggable`.
@@ -315,15 +314,15 @@ I found it effective to implement one-way data flow similar to React's approach,
however exclusively using custom DOM events. however exclusively using custom DOM events.
- **Data flows downwards** from parent components to child components - **Data flows downwards** from parent components to child components
through custom DOM events. through custom DOM events. Data events are in noun-form.
- **Actions flow upwards** through custom DOM events (bubbling up), - **Actions flow upwards** through custom DOM events (bubbling up),
usually resulting in some parent component state change which is in turn usually resulting in some parent component state change which is in turn
propagated downwards through data events. propagated downwards through data events. Action events are in verb-form.
The business logic is factored into a pure functional core The business logic is factored into a pure functional core
([TodoLogic.js](./public/scripts/TodoLogic.js)). ([TodoLogic.js](./public/scripts/TodoLogic.js)).
This is a sensible approach in most UI architectures as it encapsulates This is a sensible approach in most UI architectures as it encapsulates
state transitions in a portable, testable unit. state transitions in portable, testable units.
The controller is factored into a separate behavior The controller is factored into a separate behavior
([TodoController.js](./public/scripts/TodoController.js)). ([TodoController.js](./public/scripts/TodoController.js)).
@@ -346,8 +345,7 @@ Reference:
#### 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 will likely break important functionality as this may hurt performance and will likely break important functionality like:
which browsers have already been optimizing for decades:
- `<a>`, `<button>`, `<input>`, etc. may lose focus. - `<a>`, `<button>`, `<input>`, etc. may lose focus.
- Form inputs may lose data. - Form inputs may lose data.
@@ -359,10 +357,10 @@ 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 some rigid base HTML and an idempotent, complete update function which only
makes necessary changes. makes necessary changes.
- **Idempotence** is key here, i.e. update functions may be called at any time - **Idempotence:** Update functions may be called at any time
and should always render the component correctly. and should always render the component correctly.
- **Completeness** is equally important, i.e. update functions should render - **Completeness:** Update functions should render
the whole component, regardless of what triggered an update. the whole component, regardless of what triggered the update.
In effect, this means almost all DOM manipulation is done in update functions, In effect, this means almost all DOM manipulation is done in update functions,
which greatly contributes to robustness and readability of the codebase. which greatly contributes to robustness and readability of the codebase.
@@ -444,8 +442,8 @@ export function TodoList(el) {
``` ```
It's very verbose, with lots of opportunities to introduce bugs. It's very verbose, with lots of opportunities to introduce bugs.
Compared to a simple loop in JSX, this appears highly complex. Compared to a simple loop in JSX, this approach seems unreasonable.
It is quite efficient as it does minimal work but is otherwise messy; It is quite efficient as it does minimal work, but it's
definitely a candidate for a utility function or library. definitely a candidate for a utility function or library.
### 3.3. Drag & Drop ### 3.3. Drag & Drop
@@ -521,10 +519,10 @@ Initially, I used [serve](https://www.npmjs.com/package/serve)
which was good enough to get going but requires manually reloading which was good enough to get going but requires manually reloading
the application on every change. the application on every change.
Tooling for most modern frameworks supports _hot reloading_, Most modern frameworks support _hot reloading_,
i.e. updating the application in place when changing source files. i.e. updating the application in place when changing source files.
Hot reloading provides fast feedback during development; Hot reloading provides fast feedback during development,
especially useful when fine-tuning visuals. especially useful for fine-tuning visuals.
Unfortunately, I could not find a local development server Unfortunately, I could not find a local development server
supporting some form of hot reloading supporting some form of hot reloading
@@ -546,7 +544,7 @@ Feedback is highly appreciated.
### 4.2. Formatting and Linting ### 4.2. Formatting and Linting
Basic code quality is provided by Basic code consistency is provided by
- [Prettier](https://prettier.io), - [Prettier](https://prettier.io),
- [ESLint](https://eslint.org), and - [ESLint](https://eslint.org), and
@@ -611,7 +609,7 @@ Reference:
### 4.4. Pipeline ### 4.4. Pipeline
I've added a simple CI/CD pipeline via GitHub Actions. I've added a simple CI/CD pipeline via GitHub Actions.
It runs linters and tests, and deploys to GitHub pages on success. 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. This was straight-forward and is orthogonal to the application code and other tooling.
Reference: Reference:
@@ -648,7 +646,7 @@ _The latter was an improvement over the original application when I started
working on the case study in 2019. In the meantime, the TeuxDeux working on the case study in 2019. In the meantime, the TeuxDeux
team released an update with a much better drag & drop experience. Great job!_ team released an update with a much better drag & drop experience. Great job!_
One notable missing feature is Markdown support. It would be insensible One notable missing feature is Markdown support. It would be unreasonable
to implement Markdown from scratch; this is a valid candidate for using to implement Markdown from scratch; this is a valid candidate for using
an external library as it is entirely orthogonal to the remaining codebase. an external library as it is entirely orthogonal to the remaining codebase.
@@ -705,8 +703,7 @@ and some opinionated statements based on my experience in the industry.
- Little indirection - Little indirection
- Low coupling - Low coupling
- The result is literally just a bunch of HTML, CSS, and JS files. - The result is literally just a bunch of HTML, CSS, and JS files.
- Straight-forward testing with Playwright - Straight-forward testing with Playwright (including code coverage)
- Includes code coverage
All source files (HTML, CSS and JS) combine to **under 3000 lines of code**, All source files (HTML, CSS and JS) combine to **under 3000 lines of code**,
including comments and empty lines. including comments and empty lines.
@@ -724,7 +721,7 @@ _To be fair, my implementation misses quite a few features from the original. I
would justify a 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, - Although not used in this study,
event delegation seems not trivial to implement without code duplication. event delegation seems hard to implement without code duplication.
Eliminating verbosity through build steps and a minimal set of helpers Eliminating verbosity through build steps and a minimal set of helpers
would reduce the comparably low code size (see above) even further. would reduce the comparably low code size (see above) even further.
@@ -734,7 +731,8 @@ would reduce the comparably low code size (see above) even further.
- Class names share a global namespace. - Class names share a global namespace.
- Event names share a global namespace. - Event names share a global namespace.
- Especially problematic for events that bubble up. - Especially problematic for events that bubble up.
- No code completion in HTML strings. - No syntax highlighting or code completion in HTML strings.
- Can be mitigated with [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
- The separation between base HTML and dynamic rendering is not ideal - The separation between base HTML and dynamic rendering is not ideal
when compared to JSX, for example. when compared to JSX, for example.
- JSX/virtual DOM techniques provide much better development ergonomics. - JSX/virtual DOM techniques provide much better development ergonomics.
@@ -750,9 +748,9 @@ would reduce the comparably low code size (see above) even further.
I cannot recommend using it enough. I cannot recommend using it enough.
- We're effectively locked out of using NPM dependencies that don't provide - We're effectively locked out of using NPM dependencies that don't provide
browser-ready builds (ES modules or UMD). browser-ready builds (ES modules or UMD).
- Most frameworks handle a lot of browser inconsistencies **for free** and - Most frameworks handle a lot of browser inconsistencies and
continuously monitor regressions with extensive test suites. continuously monitor regressions with extensive test suites.
The cost of browser testing is surely a lot higher The cost of browser testing is possibly higher
when using a vanilla approach. when using a vanilla approach.
--- ---
@@ -803,7 +801,7 @@ simple build steps (e.g. SCSS and TypeScript).
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. without accidentally building a custom framework.
A notable exception to the latter is rendering variable numbers of elements 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 a concise way. I was unable to eliminate the verbosity involved
@@ -813,7 +811,7 @@ a valid candidate for a (possibly external) general-purpose utility.
When looking at the downsides, remember that all of the individual parts are When looking at the downsides, remember that all of the individual parts are
self-contained, highly decoupled, portable, and congruent to the web platform. self-contained, highly decoupled, portable, and congruent to the web platform.
The resulting implementation cannot "rust", by definition, as no dependencies The implementation cannot "rust", by definition, as no dependencies
can become out of date. can become out of date.
Another thought to be taken with a grain of salt: I believe frameworks Another thought to be taken with a grain of salt: I believe frameworks
@@ -875,6 +873,13 @@ Projects I've inspected for drag & drop architecture:
- [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) - [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)
- [dragula](https://github.com/bevacqua/dragula) - [dragula](https://github.com/bevacqua/dragula)
Useful VSCode extensions:
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)
- [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
### 8.2. Response ### 8.2. Response
#### 10/2020 #### 10/2020