{"id":13553370,"url":"https://github.com/morris/vanilla-todo","last_synced_at":"2025-05-14T21:08:40.999Z","repository":{"id":52305701,"uuid":"305752128","full_name":"morris/vanilla-todo","owner":"morris","description":"A case study on viable techniques for vanilla web development.","archived":false,"fork":false,"pushed_at":"2025-02-15T19:35:06.000Z","size":480,"stargazers_count":1174,"open_issues_count":1,"forks_count":55,"subscribers_count":18,"default_branch":"main","last_synced_at":"2025-04-06T14:06:25.986Z","etag":null,"topics":["css","frontend","html","javascript","vanilla"],"latest_commit_sha":null,"homepage":"https://morris.github.io/vanilla-todo/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/morris.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-20T15:27:27.000Z","updated_at":"2025-03-25T18:50:56.000Z","dependencies_parsed_at":"2023-12-10T14:29:59.076Z","dependency_job_id":"973b7f6a-0d99-4abd-bf19-af9613ef0465","html_url":"https://github.com/morris/vanilla-todo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morris%2Fvanilla-todo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morris%2Fvanilla-todo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morris%2Fvanilla-todo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morris%2Fvanilla-todo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/morris","download_url":"https://codeload.github.com/morris/vanilla-todo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248750010,"owners_count":21155682,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["css","frontend","html","javascript","vanilla"],"created_at":"2024-08-01T12:02:23.075Z","updated_at":"2025-04-13T16:53:38.399Z","avatar_url":"https://github.com/morris.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# VANILLA TODO\n\nA [TeuxDeux](https://teuxdeux.com) clone in plain HTML, CSS and JavaScript (no\nbuild steps). It's fully animated and runs smoothly at 60 FPS with a total\ntransfer size of **55 KB** (unminified).\n\n**[Try it online →](https://morris.github.io/vanilla-todo/)**\n\nMore importantly, it's a case study showing that **vanilla web development** is\nviable in terms of [maintainability](#521-the-good), and worthwhile in terms of\n[user experience](#51-user-experience) (**50%** less time to load and **95%**\nless bandwidth in this case).\n\n**There's no custom framework invented here.** Instead, the case study was\n[designed](#22-rules) to discover minimum viable\n[patterns](#321-mount-functions) that are truly vanilla. The result is\nmaintainable, albeit [verbose](#522-the-verbose) and with considerable\nduplication.\n\nIf anything, the case study validates the value of build steps and frameworks,\nbut also demonstrates that standard web technologies can be used effectively and\nthere are only a few [critical areas](#523-the-bad) where a vanilla approach is\nclearly inferior.\n\n_While the first version of the case study has been published in 2020, it has\nreceived significant [updates](#9-changelog) over time. Also, further work is\nbeing done with **[Vanilla Prime](https://github.com/morris/vanilla-prime)**, a\npractical guide to (almost) vanilla web development based on insights from this\ncase study._\n\n_Intermediate understanding of the web platform is required to follow through._\n\n## Table of Contents\n\n- [1. Motivation](#1-motivation)\n- [2. Method](#2-method)\n  - [2.1. Subject](#21-subject)\n  - [2.2. Rules](#22-rules)\n  - [2.3. Goals](#23-goals)\n    - [2.3.1. User Experience](#231-user-experience)\n    - [2.3.2. Code Quality](#232-code-quality)\n    - [2.3.3. Generality of Patterns](#233-generality-of-patterns)\n- [3. Implementation](#3-implementation)\n  - [3.1. Basic Structure](#31-basic-structure)\n  - [3.2. JavaScript Architecture](#32-javascript-architecture)\n    - [3.2.1. Mount Functions](#321-mount-functions)\n    - [3.2.2. Data Flow](#322-data-flow)\n    - [3.2.3. Rendering](#323-rendering)\n    - [3.2.4. Reconciliation](#324-reconciliation)\n  - [3.3. Drag \u0026 Drop](#33-drag--drop)\n  - [3.4. Animations](#34-animations)\n- [4. Tooling](#4-tooling)\n  - [4.1. Local Development Server](#41-local-development-server)\n  - [4.2. Formatting and Linting](#42-formatting-and-linting)\n  - [4.3. Testing](#43-testing)\n    - [4.3.1. Code Coverage](#431-code-coverage)\n  - [4.4. Pipeline](#44-pipeline)\n  - [4.5. Debugging](#45-debugging)\n- [5. Assessment](#5-assessment)\n  - [5.1. User Experience](#51-user-experience)\n  - [5.2. Code Quality](#52-code-quality)\n    - [5.2.1. The Good](#521-the-good)\n    - [5.2.2. The Verbose](#522-the-verbose)\n    - [5.2.3. The Bad](#523-the-bad)\n  - [5.3. Generality of Patterns](#53-generality-of-patterns)\n- [6. Conclusion](#6-conclusion)\n- [7. Beyond Vanilla](#7-beyond-vanilla)\n- [8. Appendix](#8-appendix)\n  - [8.1. Links](#81-links)\n  - [8.2. Response](#82-response)\n- [9. Changelog](#9-changelog)\n\n## 1. Motivation\n\nI believe too little has been invested in researching practical, scalable\nmethods for building web applications without third party dependencies.\n\nIt's not enough to describe how to create DOM nodes or how to toggle a class\nwithout a framework. It's also rather harmful to write an article saying you\ndon't need library X, and then proceed in describing how to roll your own\nuntested, inferior version of X.\n\nWhat's missing are thorough examples of complex web applications built only with\nstandard web technologies, covering as many aspects of the development process\nas possible.\n\nThis case study is an attempt to fill this gap, at least a little bit, and\ninspire further research in the area.\n\n## 2. Method\n\nThe method for this case study is as follows:\n\n- Pick an interesting subject.\n- Implement it using only standard web technologies.\n- Document techniques and patterns found during the process.\n- Assess the results by common quality standards.\n\nThis section describes the method in more detail.\n\n### 2.1. Subject\n\nI've chosen to build a (functionally reduced) clone of\n[TeuxDeux](https://teuxdeux.com) for this study. The user interface has\ninteresting challenges, in particular performant drag \u0026 drop when combined with\nanimations.\n\n_The original TeuxDeux app deserves praise here. In my opinion it has the best\nover-all concept and UX of all the to-do apps out there.\n[Thank you!](https://fictivekin.com/)_\n\nThe user interface is arguably small (which is good for a case study) but large\nenough to require thought on its architecture.\n\nHowever, it is lacking in some key areas:\n\n- Routing\n- Asynchronous resource requests\n- Complex forms\n- Server-side rendering\n\n### 2.2. Rules\n\nTo produce valid vanilla solutions, and because constraints spark creativity, I\ncame up with a set of rules to follow throughout the process:\n\n- Only use standard web technologies.\n- Only use widely supported JS features unless they can be polyfilled (1).\n- No runtime JS dependencies (except polyfills).\n- No build steps.\n- No general-purpose utility functions related to the DOM/UI (2).\n\n(1) This is a moving target; the current version is using ES2020.\n\n(2) These usually end up becoming a custom micro-framework, thereby questioning\nwhy you didn't use one of the established and tested libraries/frameworks in the\nfirst place.\n\n### 2.3. Goals\n\nThe results are going to be assessed by three major concerns:\n\n#### 2.3.1. User Experience\n\nThe product should be comparable to or better than the original regarding\nfunctionality, performance and design.\n\nThis includes testing major browsers and devices.\n\n#### 2.3.2. Code Quality\n\nThe implementation should be _maintainable_ and follow established code quality\nstandards.\n\nThis will be difficult to assess objectively, as we will see later.\n\n#### 2.3.3. Generality of Patterns\n\nThe discovered techniques and patterns should be applicable in a wide range of\nscenarios.\n\n## 3. Implementation\n\nThis section walks through the implementation, highlighting techniques and\nproblems found during the process. You're encouraged to inspect the\n[source code](./public) alongside this section.\n\n### 3.1. Basic Structure\n\nSince build steps are ruled out, the codebase consists of plain HTML, CSS and JS\nfiles. The HTML and CSS follows [rscss](https://ricostacruz.com/rscss/) (devised\nby [Rico Sta. Cruz](https://ricostacruz.com)) resulting in an intuitive,\ncomponent-oriented structure.\n\nThe stylesheets are slightly verbose.\n[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)\ndid help but I missed [SCSS](https://sass-lang.com/) here; I think it's a\nmust-have for bigger projects. Additionally, the global CSS namespace problem is\nunaddressed (see e.g.\n[CSS Modules](https://github.com/css-modules/css-modules)).\n\nAll JavaScript files are ES modules (`import`/`export`). I added a few\n[JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html)\ncomments to functions to get additional code completion in VSCode. This helps,\nbut using TypeScript would be much safer and less verbose.\n\nNote that I've opted out of web components completely. My attempts to refactor\nthe implementation using web components either added more complexity, or did not\nshow significant value over the initial, more basic approach.\n\n---\n\nThe basic structure comes with some boilerplate, e.g. referencing all the\nindividual stylesheets and scripts from the HTML; probably enough to justify a\nsimple build step.\n\nIt is otherwise straight-forward\u0026mdash;literally a bunch of HTML, CSS and JS\nfiles.\n\n### 3.2. JavaScript Architecture\n\nNaturally, the JavaScript architecture is the most interesting part of this\nstudy.\n\nI found that using a combination of functions, query selectors and DOM events is\nsufficient to build a scalable, maintainable codebase, albeit with some\ntrade-offs as we will see later.\n\nConceptually, the proposed architecture loosely maps CSS selectors to JS\nfunctions which are _mounted_ (i.e. called) once per matching element. This\nsimple mental model aligns well with the DOM and styles:\n\n```\nTodoList -\u003e .todo-list\n  scripts/TodoList.js\n  styles/todo-list.css\n\nAppCollapsible -\u003e .app-collapsible\n  scripts/AppCollapsible.js\n  styles/app-collapsible.css\n\n...\n```\n\nThis proved to be a useful, repeatable pattern throughout all of the\nimplementation process.\n\n#### 3.2.1. Mount Functions\n\n_Mount functions_ take a DOM element as their first argument. Their\nresponsibility is to set up initial state, event listeners, and provide behavior\nand rendering for the target element.\n\nFor example, this mount function implements a simple counter:\n\n```js\n// Define mount function.\n// Loosely mapped to \".my-counter\".\nexport function MyCounter(el) {\n  // Define initial state.\n  let value = 0;\n\n  // Set rigid base HTML.\n  el.innerHTML = `\n    \u003cspan class=\"value\"\u003e\u003c/span\u003e\n    \u003cbutton class=\"increment\"\u003eIncrement\u003c/button\u003e\n    \u003cbutton class=\"decrement\"\u003eDecrement\u003c/button\u003e\n  `;\n\n  // Attach event listeners.\n  el.querySelector('.increment').addEventListener('click', () =\u003e {\n    // Dispatch a custom event, using .detail to transport data.\n    // Parent components can listen to this event to receive the counter's value.\n    el.dispatchEvent(\n      new CustomEvent('counter', { detail: value + 1, bubbles: true }),\n    );\n  });\n\n  el.querySelector('.decrement').addEventListener('click', () =\u003e {\n    el.dispatchEvent(\n      new CustomEvent('counter', { detail: value - 1, bubbles: true }),\n    );\n  });\n\n  // This event handler supports the increment/decrement actions above,\n  // as well as resetting the counter from the outside.\n  el.addEventListener('counter', (e) =\u003e {\n    // Update state and re-render.\n    value = e.detail;\n    update();\n  });\n\n  // Define idempotent update function.\n  function update() {\n    el.querySelector('.value').innerText = value;\n  }\n\n  // Initial update.\n  update();\n}\n\n// Mount MyCounter component(s).\n// Any \u003cdiv class=\"my-counter\"\u003e\u003c/div\u003e in the document will be mounted.\ndocument.querySelectorAll('.my-counter').forEach(MyCounter);\n```\n\nThis comes with quite some boilerplate but has useful properties, as we will see\nin the following sections.\n\nNote that a mount function does not have to set any base HTML, and may instead\nonly set event listeners to enable some behavior. Also note that an element can\nbe mounted with multiple mount functions. For example, to-do items are mounted\nwith `TodoItem` and `AppDraggable`.\n\nCompared to React components, mount functions provide interesting flexibility as\ncomponents and behaviors can be implemented using the same idiom and combined\narbitrarily.\n\nReference:\n\n- [AppIcon.js](./public/scripts/AppIcon.js)\n- [TodoItem.js](./public/scripts/TodoItem.js)\n- [TodoItemInput.js](./public/scripts/TodoItemInput.js)\n\n#### 3.2.2. Data Flow\n\nI found it effective to implement one-way data flow similar to React's approach,\nhowever exclusively using custom DOM events.\n\n- **Data flows downwards** from parent components to child components through\n  custom DOM events. Data events are in noun-form.\n- **Actions flow upwards** through custom DOM events (bubbling up), usually\n  resulting in some parent component state change which is in turn propagated\n  downwards through data events. Action events are in verb-form.\n\nThe business logic is factored into a pure functional core\n([TodoLogic.js](./public/scripts/TodoLogic.js)). This is a sensible approach in\nmost UI architectures as it encapsulates state transitions in portable, testable\nunits.\n\nThe controller is factored into a separate behavior\n([TodoController.js](./public/scripts/TodoController.js)). It only receives and\ndispatches events, calling the business logic to apply changes and emit state.\nIt also handles persistence in Local Storage.\n\nListening to and dispatching events is slightly verbose with standard APIs and\ncertainly justifies introducing helpers. I didn't need event delegation à la\njQuery for this study but I believe it's a useful concept that is difficult to\ndo concisely with standard APIs.\n\nReference:\n\n- [TodoDay.js](./public/scripts/TodoDay.js)\n- [TodoController.js](./public/scripts/TodoController.js)\n- [TodoLogic.js](./public/scripts/TodoLogic.js)\n\n#### 3.2.3. Rendering\n\nNaively re-rendering a whole component using `.innerHTML` should be avoided as\nthis may hurt performance and will likely break important functionality like:\n\n- `\u003ca\u003e`, `\u003cbutton\u003e`, `\u003cinput\u003e`, etc. may lose focus.\n- Form inputs may lose data.\n- Text selection may be reset.\n- CSS transitions may not work correctly.\n- Event listeners may need to be reattached.\n\nAs seen in [3.2.1.](#321-mount-functions), rendering is therefore split into\nsome rigid base HTML and an idempotent, complete update function which only\nmakes necessary changes.\n\n- **Idempotence:** Update functions may be called at any time and should always\n  render the component correctly.\n- **Completeness:** Update functions should render the whole component,\n  regardless of what triggered the update.\n\nIn effect, this means almost all DOM manipulation is done in update functions,\nwhich greatly contributes to robustness and readability of the codebase.\n\nAs seen above this approach is quite verbose and ugly compared to JSX, for\nexample. However, it's very performant and can be further optimized by checking\nfor data changes, caching selectors, etc. It is also simple to understand.\n\nReference:\n\n- [TodoItem.js](./public/scripts/TodoItem.js)\n- [TodoCustomList.js](./public/scripts/TodoCustomList.js)\n\n#### 3.2.4. Reconciliation\n\nExpectedly, the hardest part of the study was rendering a variable amount of\ndynamic components efficiently. Here's a commented example from the\nimplementation outlining the reconciliation algorithm:\n\n```js\nexport function TodoList(el) {\n  let items = [];\n\n  el.innerHTML = `\u003cdiv class=\"items\"\u003e\u003c/div\u003e`;\n\n  el.addEventListener('todoItems', (e) =\u003e {\n    items = e.detail;\n    update();\n  });\n\n  function update() {\n    const container = el.querySelector('.items');\n\n    // Mark current children for removal\n    const obsolete = new Set(container.children);\n\n    // Map current children by data-key\n    const childrenByKey = new Map();\n    obsolete.forEach((child) =\u003e childrenByKey.set(child.dataset.key, child));\n\n    // Build new list of child elements from data\n    const children = items.map((item) =\u003e {\n      // Find existing child by data-key\n      let child = childrenByKey.get(item.id);\n\n      if (child) {\n        // If child exists, keep it\n        obsolete.delete(child);\n      } else {\n        // Otherwise, create new child\n        child = document.createElement('div');\n        child.classList.add('todo-item');\n\n        // Set data-key\n        child.dataset.key = item.id;\n\n        // Mount component\n        TodoItem(child);\n      }\n\n      // Update child\n      child.dispatchEvent(new CustomEvent('todoItem', { detail: item }));\n\n      return child;\n    });\n\n    // Remove obsolete children\n    obsolete.forEach((child) =\u003e container.removeChild(child));\n\n    // (Re-)insert new list of children\n    children.forEach((child, index) =\u003e {\n      if (child !== container.children[index]) {\n        container.insertBefore(child, container.children[index]);\n      }\n    });\n  }\n}\n```\n\nIt's very verbose, with lots of opportunities to introduce bugs. Compared to a\nsimple loop in JSX, this approach seems unreasonable. It is quite efficient as\nit does minimal work, but it's definitely a candidate for a utility function or\nlibrary.\n\n### 3.3. Drag \u0026 Drop\n\nImplementing drag \u0026 drop from scratch was challenging, especially regarding\nbrowser/device consistency.\n\nUsing a library would have been a lot more cost-effective initially. However,\nhaving a customized implementation paid off once I started introducing\nanimations as both had to be coordinated closely. I can imagine this would have\nbeen a difficult problem when using third party code for either.\n\nThe drag \u0026 drop implementation is (again) based on DOM events and integrates\nwell with the remaining architecture. It's clearly the most complex part of the\nstudy but I was able to implement it without changing existing code besides\nmounting behaviors and adding event handlers.\n\nI suspect the drag \u0026 drop implementation to have some subtle problems on touch\ndevices, as I haven't extensively tested them. Using a library for identifying\nthe gestures could be more sensible and would reduce costs in testing browsers\nand devices.\n\nReference:\n\n- [AppDraggable.js](./public/scripts/AppDraggable.js)\n- [AppSortable.js](./public/scripts/AppSortable.js)\n- [TodoList.js](./public/scripts/TodoList.js)\n\n### 3.4. Animations\n\nFor the final product I wanted smooth animations for most user interactions.\nThis is a cross-cutting concern which was implemented using the\n[FLIP](https://aerotwist.com/blog/flip-your-animations/) technique as devised by\n[Paul Lewis](https://twitter.com/aerotwist).\n\nImplementing FLIP animations without a large refactoring was the biggest\nchallenge of this case study, especially in combination with drag \u0026 drop. After\ndays of work I was able to implement the algorithm in isolation and coordinate\nit with other concerns at the application's root level. The `useCapture` mode of\n`addEventListener` proved to be very useful in this case.\n\nReference:\n\n- [AppFlip.js](./public/scripts/AppFlip.js)\n- [TodoApp.js](./public/scripts/TodoApp.js)\n\n## 4. Tooling\n\nWhile no runtime dependencies or build steps were allowed, I did introduce some\nlocal tooling to support the development experience.\n\nAs a quick start, here are the steps to get a local development server up and\nrunning:\n\n- Install [git](https://git-scm.com/)\n- Install [Node.js](https://nodejs.org/) (\u003e= 20)\n- Install an IDE (I used [VSCode](https://code.visualstudio.com/))\n- Clone this repository\n- Open a terminal in the repository's directory\n- Run `npm install`\n- Run `npm run dev`\n- Visit [http://localhost:8080](http://localhost:8080)\n\nThe following sections describe the tooling in more detail.\n\n### 4.1. Local Development Server\n\nBecause ES modules are not allowed under the `file://` protocol I needed to run\na local web server for development. Initially, I used\n[serve](https://www.npmjs.com/package/serve) which was good enough to get going\nbut requires manually reloading the application on every change.\n\nMost modern frameworks support _hot reloading_, i.e. updating the application in\nplace when changing source files. Hot reloading provides fast feedback during\ndevelopment, especially useful for fine-tuning visuals.\n\nUnfortunately, I could not find a local development server supporting some form\nof hot reloading without introducing a framework or build system, but I was able\nto implement a [minimal local development server](https://github.com/morris/s4d)\nwith the following behavior:\n\n- Changes to stylesheets or images will hot replace the changed resources.\n- Other changes (e.g. JavaScript or HTML) will cause a full page reload.\n\nWhile it's not proper\n[hot module replacement](https://webpack.js.org/concepts/hot-module-replacement/)\n(which needs immense infrastructure), it requires zero changes to the\napplication source and provides a similar experience because page reloads are\nfast.\n\n### 4.2. Formatting and Linting\n\nBasic code consistency is provided by\n\n- [Prettier](https://prettier.io),\n- [ESLint](https://eslint.org), and\n- [stylelint](https://stylelint.io).\n\nI've set the ESLint parser to ES2020 to ensure only ES2020 code is allowed. I've\nalso added stylelint rules to check for rscss-compatible CSS.\n\nRun these commands to try it out:\n\n- `npm run format-check` to check formatting\n- `npm run format` to apply formatting\n- `npm run lint` to lint JavaScript\n- `npm run lint-styles` to lint CSS\n\nThese tools only required minimal configuration to be effective. They also\nintegrate well with VSCode so I've rarely had to run these manually.\n\n### 4.3. Testing\n\nI've implemented some end-to-end and unit tests using\n[Playwright](https://playwright.dev/). While running a local web server (see\nabove), you can run the tests with\n\n- `npm run test` for headless tests, or\n- `npm run test-ui` for interactive mode.\n\nThese might ask you to install Playwright; just follow the instructions.\n\nThere's a lot more to explore here, but it's not much different from testing\nother frontend stacks. It's actually simpler as there was zero configuration and\njust one dependency.\n\nReference:\n\n- [addItem.test.js](./test/e2e/addItem.test.js)\n- [util.test.js](./test/unit/util.test.js)\n\n#### 4.3.1. Code Coverage\n\nI was able to set up code coverage for unit _and_ end-to-end tests via\n[Playwright's code coverage feature](https://playwright.dev/docs/api/class-coverage)\nand [c8](https://github.com/bcoe/c8). This introduced another dependency and was\nslightly more involved to get right, e.g. mapping localhost URLs to file URLs.\n\nUse `npm run test-coverage` to run the tests and produce an LCOV test coverage\nreport in `./coverage`.\n\nNote that the implementation is specific to the project structure, e.g.\n`/public` as web root and port `8080` are hard-coded.\n\nReference:\n\n- [test-coverage.sh](./scripts/test-coverage.sh)\n- [coverage.js](./test/coverage.js)\n\n### 4.4. Pipeline\n\nI've added a simple CI/CD pipeline via GitHub Actions. It runs linters and\ntests, and deploys to GitHub Pages on success. This was straight-forward and is\northogonal to the application code and other tooling.\n\nReference:\n\n- [pipeline.yml](./.github/workflows/pipeline.yml)\n\n### 4.5. Debugging\n\nI've mostly used [Chrome DevTools](https://developer.chrome.com/docs/devtools)\nfor debugging and the experience was fantastic. It feels incredibly _immediate_\ninspecting an application without third-party code or any kind of cruft (e.g.\nsource maps).\n\n## 5. Assessment\n\n### 5.1. User Experience\n\nMost important features from the original TeuxDeux application are implemented\nand usable:\n\n- Daily to-do lists\n- Add/edit/delete to-do items\n- Custom to-do lists\n- Add/edit/delete custom to-do lists\n- Drag \u0026 drop to-do items across lists\n- Reorder custom to-do lists via drag \u0026 drop\n- Local Storage persistence\n\nAdditionally, most interactions are smoothly animated at 60 frames per second.\nIn particular, dragging and dropping gives proper visual feedback when elements\nare reordered.\n\n_The latter was an improvement over the original application when I started\nworking on the case study in 2019. In the meantime, the TeuxDeux team released\nan update with a much better drag \u0026 drop experience. Great job!_\n\nOne notable missing feature is Markdown support. It would be unreasonable to\nimplement Markdown from scratch; this is a valid candidate for using an external\nlibrary as it is entirely orthogonal to the remaining codebase.\n\nThe application has been tested on latest Chrome, Firefox, Safari, and Safari on\niOS.\n\n_TODO Test more browsers and devices._\n\nA fresh load of the original TeuxDeux application transfers around **1.2 MB**\nand finishes loading at over **1000 ms**, sometimes up to 2000ms (measured in\n12/2023). Reloads finish at around **700 ms**.\n\nWith a transferred size of around **55 KB**, the vanilla application\nconsistently loads in **300-500 ms**\u0026mdash;not minified and with each script,\nstylesheet and icon served as an individual file. Reloads finish at **100-200\nms**; again, not optimized at all (with e.g. asset hashing/indefinite caching).\n\n_To be fair, my implementation misses quite a few features from the original. I\nsuspect a fully equivalent clone to be well below 100 KB transfer, though._\n\nWhile there is still optimization potential, the\n[Lighthouse](https://developer.chrome.com/docs/lighthouse) score is perfect.\n\n### 5.2. Code Quality\n\nUnfortunately, it is quite hard to find undisputed, objective measurements for\ncode quality (besides trivialities like code style, linting, etc.). The only\ngenerally accepted assessment seems to be peer reviewal.\n\nTo have at least some degree of assessment of the code's quality, the following\nsections summarize relevant facts about the codebase and some opinionated\nstatements based on my experience in the industry.\n\n#### 5.2.1. The Good\n\n- No build steps\n- No external dependencies at runtime besides polyfills\n  - No dependency maintenance\n  - No breaking changes to monitor\n- Used only standard technologies:\n  - Plain HTML, CSS and JavaScript\n  - Standard DOM APIs\n- Very few concepts introduced:\n  - Mount functions (loosely mapped by CSS class names)\n  - State separated from the DOM\n  - Idempotent updates\n  - Data flow using custom events\n- Compare the proposed architecture to the API/conceptual surface of Angular or\n  React...\n- Progressive developer experience\n  - Markup, style, and behavior are orthogonal and can be developed separately.\n  - Adding behavior has little impact on the markup besides adding classes.\n- Debugging is straight-forward using modern browser developer tools.\n- The app can be naturally enhanced from the outside by handling/dispatching\n  events (just like you can naturally animate some existing HTML).\n- Little indirection\n- Low coupling\n- The result is literally just a bunch of HTML, CSS, and JS files.\n- Straight-forward testing with Playwright (including code coverage)\n\nAll source files (HTML, CSS and JS) combine to **under 3000 lines of code**,\nincluding comments and empty lines.\n\nFor comparison, prettifying the original TeuxDeux's minified JS assets yields\n**81602 LOC** (12/2023).\n\n_To be fair, my implementation misses quite a few features from the original. I\nsuspect a fully equivalent clone to be well below 10000 LOC, though._\n\n#### 5.2.2. The Verbose\n\n- Stylesheets are a bit verbose. SCSS would help here.\n- Simple components require quite some boilerplate code.\n- `el.querySelectorAll(':scope ...')` is somewhat default/expected and would\n  justify a helper.\n- Listening to and dispatching events is slightly verbose.\n- Although not used in this study, event delegation seems hard to implement\n  without code duplication.\n\nEliminating verbosity through build steps and a minimal set of helpers would\nreduce the comparably low code size (see above) even further.\n\n#### 5.2.3. The Bad\n\n- Class names share a global namespace.\n- Event names share a global namespace.\n  - Especially problematic for events that bubble up.\n- No syntax highlighting or code completion in HTML strings.\n  - Can be mitigated with\n    [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)\n- The separation between base HTML and dynamic rendering is not ideal when\n  compared to JSX, for example.\n- JSX/virtual DOM techniques provide much better development ergonomics.\n- Reconciliation is verbose, brittle and repetitive. I wouldn't recommend the\n  proposed technique without a well-tested helper function, at least.\n- You have to remember mounting behaviors correctly when creating new elements.\n  It would be helpful to automate this somehow, e.g. watch elements of selector\n  X (at all times) and ensure the desired behaviors are mounted once on them.\n- No type safety. I've always been a proponent of dynamic languages but since\n  TypeScript's type system provides the best of both worlds, I cannot recommend\n  using it enough.\n- We're effectively locked out of using NPM dependencies that don't provide\n  browser-ready builds (ES modules or UMD).\n- Most frameworks handle a lot of browser inconsistencies and continuously\n  monitor regressions with extensive test suites. The cost of browser testing is\n  possibly higher when using a vanilla approach.\n\n---\n\nBesides the issues described above, I believe the codebase is well organized and\nthere are clear paths for bugfixes and feature development. Since there's no\nthird party code, bugs are easy to find and fix, and there are no dependency\nlimitations to work around.\n\nA certain degree of DOM API knowledge is required but I believe this should be a\ngoal for any web developer.\n\n### 5.3. Generality of Patterns\n\nAssessing the generality of the discovered techniques objectively is not really\npossible without production usage. From my experience, however, I can't imagine\nany scenario where mount functions, event-based data flow etc. are not\napplicable. The underlying principles power the established frameworks, after\nall:\n\n- State is separated from the DOM (React, Angular, Vue).\n- Rendering is idempotent and complete (React's pure `render` function).\n- One-way data flow (React)\n\nAn open question is if these patterns hold for library authors. Although not\nconsidered during the study, some observations can be made:\n\n- The JavaScript itself would be fine to share as ES modules.\n- Event naming needs great care, as dispatching (bubbling) events from imported\n  behaviors can trigger parent listeners in consumer code.\n  - Can be mitigated by providing options to prefix or map event names.\n- CSS names share a global namespace and need to be managed as well.\n  - Can also be mitigated by prefixing, however making the JavaScript a bit more\n    complex.\n\n## 6. Conclusion\n\nThe result of this study is a working to-do application with decent UI/UX and\nmost of the functionality of the original TeuxDeux app, built using only\nstandard web technologies. It comes with better overall performance at a\nfraction of the code size and bandwidth.\n\nThe codebase seems manageable through a handful of simple concepts, although it\nis quite verbose and even messy in some areas. This could be mitigated by a\nsmall number of helper functions and simple build steps (e.g. SCSS and\nTypeScript).\n\nThe study's method helped discovering patterns and techniques that are at least\non par with a framework-based approach for the given subject, without\naccidentally building a custom framework.\n\nA notable exception to the latter is rendering variable numbers of elements in a\nconcise way. I was unable to eliminate the verbosity involved in basic but\nefficient reconciliation. Further research is needed in this area, but for now\nthis appears to be a valid candidate for a (possibly external) general-purpose\nutility.\n\nWhen looking at the downsides, remember that all of the individual parts are\nself-contained, highly decoupled, portable, and congruent to the web platform.\nThe implementation cannot \"rust\", by definition, as no dependencies can become\nout of date.\n\nAnother thought to be taken with a grain of salt: I believe frameworks make\nsimple tasks even simpler, but hard tasks (e.g. implementing cross-cutting\nconcerns or performance optimizations) often more difficult.\n\n---\n\nSetting some constraints up-front forced me to challenge my assumptions and\npreconceptions about vanilla web development. It was quite liberating to avoid\ngeneral-purpose utilities and get things done with what's readily available.\n\nWhile I think the study is relatively complete, there's always more to explore.\n[Ideas, questions, bug reports](https://github.com/morris/vanilla-todo/issues)\nand pull requests are more than welcome!\n\nFinally, this case study does not question using dependencies, libraries or\nframeworks in general\u0026mdash;code sharing is an essential part of software\nengineering. It was a constrained experiment designed to discover novel methods\nfor vanilla web development and, hopefully, inspire innovation and further\nresearch in the area.\n\n## 7. Beyond Vanilla\n\nAs detailed in the assessment, the result of the case study could be\nsignificantly improved if build steps and helpers were allowed. Beyond the\nstrict rules I've used in this experiment, here are a few ideas I'd like to see\nexplored in the future:\n\n- Run another case study with TypeScript, SCSS, and build steps (seems\n  promising).\n  - _See [Vanilla Prime](https://github.com/morris/vanilla-prime)._\n- Extrapolate deep utility functions (e.g. `reconcile()`) to mitigate some of\n  the discovered downsides.\n  - _See [exdom](https://github.com/morris/exdom)._\n- Experiment with architectures based on virtual DOM rendering and standard DOM\n  events.\n- Compile discovered rules, patterns and techniques into a comprehensive guide.\n  - _See [Vanilla Prime](https://github.com/morris/vanilla-prime)._\n\nCase studies constrained by a set of formal rules are an effective way to find\nnew patterns and techniques in a wide range of domains. I'd love to see similar\nexperiments in the future\n\n## 8. Appendix\n\n### 8.1. Links\n\nGeneral resources I've used extensively:\n\n- [MDN Web Docs](https://developer.mozilla.org) as a reference for DOM APIs\n- [Can I use...](https://caniuse.com) as a reference for browser support\n- [React](https://reactjs.org) as inspiration for the architecture\n\nUseful articles regarding FLIP animations:\n\n- [FLIP Your Animations (aerotwist.com)](https://aerotwist.com/blog/flip-your-animations)\n- [Animating Layouts with the FLIP Technique (css-tricks.com)](https://css-tricks.com/animating-layouts-with-the-flip-technique)\n- [Animating the Unanimatable (medium.com)](https://medium.com/developers-writing/animating-the-unanimatable-1346a5aab3cd)\n\nProjects I've inspected for drag \u0026 drop architecture:\n\n- [React DnD](https://github.com/react-dnd/react-dnd/)\n- [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)\n- [dragula](https://github.com/bevacqua/dragula)\n\nUseful VSCode extensions:\n\n- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)\n- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)\n- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)\n- [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)\n\n### 8.2. Response\n\n#### 12/2023\n\n- [Lobsters](https://lobste.rs/s/jofho9/case_study_on_vanilla_web_development)\n\n#### 10/2020\n\n- Trending on [Hacker News](https://news.ycombinator.com/item?id=24893247)\n- [Lobsters](https://lobste.rs/s/5gcrxh/case_study_on_vanilla_web_development)\n- [@desandro (Twitter)](https://twitter.com/desandro/status/1321095247091433473)\n  (developer for the original TeuxDeux)\n- [Reddit](https://www.reddit.com/r/javascript/comments/jj10k9/vanillatodo_a_case_study_on_viable_techniques_for/)\n\nThanks!\n\n## 9. Changelog\n\n### 02/2025\n\n- Update dependencies\n\n### 12/2024\n\n- Add dark mode\n- Add Playwright config for testing more browsers\n- Scroll automatically when dragging items at the window border\n- Improve drag and drop behavior on touch devices\n- Update dependencies\n\n### 08/2024\n\n- Link to [Vanilla Prime](https://github.com/morris/vanilla-prime)\n- Link to [exdom](https://github.com/morris/exdom)\n- Update dependencies\n\n### 06/2024\n\n- Use [s4d](https://github.com/morris/s4d) as local development server\n- Run pipeline checks for pull requests\n  ([#13](https://github.com/morris/vanilla-todo/issues/13))\n- Move past items to today when not done\n  ([#14](https://github.com/morris/vanilla-todo/issues/14))\n- Update dependencies\n\n### 01/2024\n\n- Add [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest)\n- Fix merged [code coverage](#431-code-coverage) from end-to-end and unit tests\n- Remove FPS counter\n\n### 12/2023\n\n- Add [debugging section](#45-debugging)\n- Redesign with CSS variables\n- Add GitHub action for running checks and deployment\n- Edit closing section\n- Update numbers\n\n### 11/2023\n\n- Add [tooling section](#4-tooling)\n- Refactor business logic into pure functional module\n- Add support for [code coverage](#431-code-coverage)\n- Add [local development server](#41-local-development-server) with live reload\n- Fix some visual issues\n- Update dependencies\n\n### 05/2023\n\n- Add basic testing\n- Fix stylelint errors\n- Update dependencies\n\n### 08/2022\n\n- Fix date seeking bug on Safari\n\n### 05/2022\n\n- Refactor for ES2020\n- Refactor for event-driven communication exclusively\n- Move original ES5-based version of the study to [/es5](./es5)\n- Add assessment regarding library development\n- Add date picker\n\n### 01/2021\n\n- Add [response section](#82-response)\n\n### 10/2020\n\n- Refactor for `dataset` ([#2](https://github.com/morris/vanilla-todo/issues/2))\n  \u0026mdash; [@opethrocks](https://github.com/opethrocks)\n- Fix [#3](https://github.com/morris/vanilla-todo/issues/3) (navigation bug)\n  \u0026mdash; [@anchepiece](https://github.com/anchepiece),\n  [@jcoussard](https://github.com/jcoussard)\n- Fix [#4](https://github.com/morris/vanilla-todo/issues/4) (double item\n  creation) \u0026mdash; [@n0nick](https://github.com/n0nick)\n- Fix [#1](https://github.com/morris/vanilla-todo/issues/4) (bad links) \u0026mdash;\n  [@roryokane](https://github.com/roryokane)\n- Initial version\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorris%2Fvanilla-todo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmorris%2Fvanilla-todo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorris%2Fvanilla-todo/lists"}