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

some editing

This commit is contained in:
Morris Brodersen
2023-12-03 14:51:20 +01:00
parent a869fe1ce0
commit 44f1030773

111
README.md
View File

@@ -205,15 +205,15 @@ albeit with some trade-offs as we will see later.
Conceptually, the proposed architecture loosely maps Conceptually, the proposed architecture loosely maps
CSS selectors to JS functions which are _mounted_ (i.e. called) once CSS selectors to JS functions which are _mounted_ (i.e. called) once
per matching element. This simple mental model also works well per matching element. This simple mental model aligns well
with the DOM and styles: with the DOM and styles:
``` ```
.todo-list -> TodoList TodoList -> .todo-list
scripts/TodoList.js scripts/TodoList.js
styles/todo-list.css styles/todo-list.css
.app-collapsible -> AppCollapsible AppCollapsible -> .app-collapsible
scripts/AppCollapsible.js scripts/AppCollapsible.js
styles/app-collapsible.css styles/app-collapsible.css
@@ -229,90 +229,47 @@ _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 of mount functions: Here's a "Hello, World!" example (implementing a simple counter) of mount functions:
```js ```js
// Define mount function // Define mount function.
// Loosely mapped to ".hello-world" // Loosely mapped to ".my-counter".
export function HelloWorld(el) {
// Define initial state
let title = 'Hello, World!';
let description = 'An example vanilla component';
let counter = 0;
// Set rigid base HTML
el.innerHTML = `
<h1 class="title"></h1>
<p class="description"></p>
<div class="my-counter"></div>
`;
// Mount sub-components
el.querySelectorAll('.my-counter').forEach(MyCounter);
// Attach event listeners
el.addEventListener('modifyCounter', (e) => {
counter += e.detail;
update();
});
// Initial update
update();
// Define idempotent update function
function update() {
// Update own HTML
el.querySelector('.title').innerText = title;
el.querySelector('.description').innerText = description;
// Pass data to sub-components
el.querySelector('.my-counter').dispatchEvent(
new CustomEvent('updateMyCounter', {
detail: { value: counter },
}),
);
}
}
// Define another component
// Loosely mapped to ".my-counter"
export function MyCounter(el) { export function MyCounter(el) {
// Define initial state // Define initial state.
let value = 0; let value = 0;
// Set rigid base HTML // Set rigid base HTML.
el.innerHTML = ` el.innerHTML = `
<p> <span class="value"></span>
<span class="value"></span> <button class="increment">Increment</button>
<button class="increment">Increment</button> <button class="decrement">Decrement</button>
<button class="decrement">Decrement</button>
</p>
`; `;
// Attach event listeners // Attach event listeners.
el.querySelector('.increment').addEventListener('click', () => { el.querySelector('.increment').addEventListener('click', () => {
// Dispatch an action // Dispatch a custom event, using .detail to transport data.
// Use .detail to transport data // Parent components can listen to this event to receive the counter's value.
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('modifyCounter', { new CustomEvent('counter', {
detail: 1, detail: value + 1,
bubbles: true, bubbles: true,
}), }),
); );
}); });
el.querySelector('.decrement').addEventListener('click', () => { el.querySelector('.decrement').addEventListener('click', () => {
// Dispatch an action
// Use .detail to transport data
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('modifyCounter', { new CustomEvent('counter', {
detail: -1, detail: value - 1,
bubbles: true, bubbles: true,
}), }),
); );
}); });
el.addEventListener('updateMyCounter', (e) => { // This event handler supports the increment/decrement actions above,
// as well as resetting the counter from the outside.
el.addEventListener('counter', (e) => {
// Update state and re-render
value = e.detail; value = e.detail;
update(); update();
}); });
@@ -321,11 +278,14 @@ export function MyCounter(el) {
function update() { function update() {
el.querySelector('.value').innerText = value; el.querySelector('.value').innerText = value;
} }
// Initial update
update();
} }
// Mount HelloWorld component(s) // Mount MyCounter component(s)
// Any <div class="hello-world"></div> in the document will be mounted // Any <div class="my-counter"></div> in the document will be mounted
document.querySelectorAll('.hello-world').forEach(HelloWorld); 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,
@@ -361,7 +321,7 @@ however exclusively using custom DOM events.
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 good idea in many 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 a portable, testable unit.
The controller is factored into a separate behavior The controller is factored into a separate behavior
@@ -441,10 +401,7 @@ export function TodoList(el) {
// Map current children by data-key // Map current children by data-key
const childrenByKey = new Map(); const childrenByKey = new Map();
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
obsolete.forEach((child) =>
childrenByKey.set(child.getAttribute('data-key'), child),
);
// Build new list of child elements from data // Build new list of child elements from data
const children = items.map((item) => { const children = items.map((item) => {
@@ -460,7 +417,7 @@ export function TodoList(el) {
child.classList.add('todo-item'); child.classList.add('todo-item');
// Set data-key // Set data-key
child.setAttribute('data-key', item.id); child.dataset.key = item.id;
// Mount component // Mount component
TodoItem(child); TodoItem(child);
@@ -485,9 +442,9 @@ export function TodoList(el) {
} }
``` ```
It's very verbose, with lots of opportunity to introduce bugs. It's very verbose, with lots of opportunities to introduce bugs.
Compared to a simple loop in JSX, this seems insane. Compared to a simple loop in JSX, this appears highly complex.
It is quite performant as it does minimal work but is otherwise messy; It is quite efficient 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.3. Drag & Drop ### 3.3. Drag & Drop