{"id":15722326,"url":"https://github.com/johnfactotum/todomvc","last_synced_at":"2025-06-23T17:06:59.541Z","repository":{"id":206939801,"uuid":"717998803","full_name":"johnfactotum/todomvc","owner":"johnfactotum","description":"TodoMVC in ~100 lines of vanilla JavaScript with Web Components","archived":false,"fork":false,"pushed_at":"2023-11-15T08:04:42.000Z","size":28,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-23T04:13:52.992Z","etag":null,"topics":["todomvc","vanilla-javascript","web-components"],"latest_commit_sha":null,"homepage":"https://johnfactotum.github.io/todomvc/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc0-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnfactotum.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2023-11-13T06:37:00.000Z","updated_at":"2025-03-24T14:30:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"94c28628-c706-4281-bfda-51df462a1762","html_url":"https://github.com/johnfactotum/todomvc","commit_stats":{"total_commits":6,"total_committers":1,"mean_commits":6.0,"dds":0.0,"last_synced_commit":"4b475d62ae78f140f07613f6ff02acc4a6919af4"},"previous_names":["johnfactotum/todomvc"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/johnfactotum/todomvc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnfactotum%2Ftodomvc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnfactotum%2Ftodomvc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnfactotum%2Ftodomvc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnfactotum%2Ftodomvc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnfactotum","download_url":"https://codeload.github.com/johnfactotum/todomvc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnfactotum%2Ftodomvc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261518934,"owners_count":23171226,"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":["todomvc","vanilla-javascript","web-components"],"created_at":"2024-10-03T22:06:44.247Z","updated_at":"2025-06-23T17:06:59.507Z","avatar_url":"https://github.com/johnfactotum.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TodoMVC in ~100 lines of vanilla JavaScript\n\nOr: How I Learned to Stop Worrying and Love the DOM\n\n## The problem: keeping the state and the UI in sync\n\nHow to keep the state and the UI in sync is a central problem when building web apps.\n\nTo begin, let's say you have some app state data stored in a JavaScript object. Let's say it looks like this:\n\n```js\nconst state = {\n    items: [\n        { text: 'Foo' },\n        { text: 'Bar },\n    ],\n}\n```\n\nNow, we want to display this in our HTML page. We'd like to render it somehow like this:\n\n```html\n\u003cul\u003e\n    \u003cli\u003eFoo\u003c/li\u003e\n    \u003cli\u003eBar\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nIt's easy enough to transform the data into HTML or DOM objects. But it gets tricky when the data changes:\n\n```js\nstate.items.push({ text: 'Baz' })\n// What happens now?\n```\n\nAn even bigger problem arises when you want to change the data from the HTML UI:\n\n```html\n\u003cul\u003e\n    \u003cli\u003eFoo \u003cbutton onclick=\"remove()\"\u003eRemove\u003c/button\u003e\u003c/li\u003e\n    \u003cli\u003eBar \u003cbutton onclick=\"remove()\"\u003eRemove\u003c/button\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cscript\u003e\nfunction remove() {\n    // Need to remove element from the `items` array.\n    // What to do here?\n}\n\u003c/script\u003e\n```\n\nThings can get complicated quickly when rendering data manually this way.\n\nHow do you solve this? Answer: you don't. Rather than trying to solve the hard problem of keeping the UI in sync with the state, you ensure that the UI *never* changes (given any state). This is known as *declarative UI*.\n\nPopular JavaScript libraries such as [React](https://react.dev) allow you to build UI declaratively, using various run-time or compile-time optimizations, such as virtual DOMs, to make re-rendering efficient. While this approach has a lot of advantages, there is one big problem: you're always fighting the DOM, and that adds a lot of complexity, and sometimes hefty performance penalties. Are there simpler solutions?\n\n## What if you just put the state in the DOM?\n\nLet's take a step back and consider our assumptions: (1) we have the data in JavaScript, and (2) this data needs to be rendered as HTML.\n\nBut wait. An HTML document *is* data, and the browser takes care of the rendering for you. So why don't we just put the data directly in the DOM?\n\nLet's consider our earlier example again. Our state is stored in a JavaScript object:\n\n```js\nconst state = {\n    items: [\n        { text: 'Foo' },\n        { text: 'Bar },\n    ],\n}\n```\n\nTo add an item, we would need to do something like this:\n\n```js\nstate.items.push({ text: 'Baz' })\nrender() // somehow update the DOM\n```\n\nYou could use a library or framework that handles this automatically for you. But *someone* has to do the chore of updating the DOM no matter what.\n\nNow what if the state *is* the DOM? With [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components), we can store the same data as HTML elements, like so:\n\n```html\n\u003cmy-list\u003e\n    \u003cmy-item text=\"Foo\"\u003e\u003c/my-item\u003e\n    \u003cmy-item text=\"Bar\"\u003e\u003c/my-item\u003e\n\u003c/my-list\u003e\n```\n\nTo add an item, you just append a new element:\n\n```js\nconst item = document.createElement('my-item')\nitem.setAttribute('text', 'Baz')\ndocument.querySelector('my-list').append(item)\n```\n\nThat's it! There's nothing more to do. What about removing an item? Thanks to the [`Element.remove()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/remove) method, it's as simple as\n\n```js\nitem.remove()\n```\n\nWant to add a \"Remove\" button? No need to loop through the whole list just to remove an item. Just call `this.remove()` in your item component and you're done!\n\nReacting to changes can be achieved with the [Mutation Observer](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) API:\n\n```js\nnew MutationObserver(mutations =\u003e {\n    console.log('some items were added or removed')\n}).observe(document.querySelector('my-list'), { childList: true })\n```\n\nThis is actually better than plain JavaScript objects, which can't really be observed (proxies are not observers). Now instead of fighting the DOM, you're using the DOM to your advantage.\n\nWhat are some other advantages of storing data in the DOM? For one, remember that it's a *tree*. That means unlike JavaScript objects, you can keep and traverse lists and nested data very easily and efficiently. Have you ever used a tree or linked list library in JavaScript? Or even implemented one yourself? You don't have to! Just use the DOM!\n\n## The result\n\nTo experiment with this approach, I made a [TodoMVC](https://todomvc.com) implementation. It contains only 101 lines of code in vanilla JavaScript, which is 1kb minified and gzipped.\n\nBut I'm cheating a little with that number. Because some bits aren't implemented with JavaScript at all. For example, filtering items by completion state is done using just CSS:\n\n```css\n#todos[data-filter=\"active\"] [completed] {\n    display: none;\n}\n#todos[data-filter=\"completed\"] :not([completed]) {\n    display: none;\n}\n```\n\nUnlike most TodoMVC examples, there isn't a \"store\" where you have a bunch of `addTodo()`, `removeTodo()` methods and whatnot. Everything lives directly in the DOM. Even the data stored in `localStorage` is in HTML, not JSON!\n\n## Conclusion: you're using the DOM wrong\n\nFor years, people have been keeping states in JavaScript and rendering them in the DOM. But that means you're unnecessarily keeping the data in two separate places—and then you complain about how keeping them in sync is difficult!\n\nThe DOM is designed to hold data. The \"M\" in \"DOM\" literally stands for \"model\". Using it only as a view is counterintuitive and counterproductive.\n\nYou might not need data binding, or virtual DOM, or state management, or the whole jungle... if the state is stored in the DOM, which can be implemented cleanly today with Web Components and Mutation Observers.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnfactotum%2Ftodomvc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnfactotum%2Ftodomvc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnfactotum%2Ftodomvc/lists"}