{"id":7,"date":"2022-09-08T18:25:00","date_gmt":"2022-09-08T18:25:00","guid":{"rendered":"http:\/\/fem.flywheelsites.com\/?p=7"},"modified":"2024-07-30T08:14:41","modified_gmt":"2024-07-30T13:14:41","slug":"vanilla-javascript-todomvc","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/vanilla-javascript-todomvc\/","title":{"rendered":"Writing a TodoMVC App with Modern Vanilla JavaScript"},"content":{"rendered":"\n<p>I took a shot at coding&nbsp;<a href=\"https:\/\/github.com\/1Marc\/modern-todomvc-vanillajs\">TodoMVC with modern (ES6+), vanilla JavaScript<\/a>, and it only took ~170 lines of code and just over an hour! Compare this to the old\/official TodoMVC vanilla JS solution, which has over 900 lines of&nbsp;<a href=\"https:\/\/github.com\/tastejs\/todomvc\/tree\/gh-pages\/examples\/vanillajs\">code<\/a>. An 80%+ reduction in code! I \u2764\ufe0f the new state of JavaScript.<\/p>\n\n\n\n<p>The code has received over \ud83e\udd29 600 stars on GitHub:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"794\" height=\"272\" src=\"https:\/\/i0.wp.com\/rc.frontendmasters.com\/blog\/wp-content\/uploads\/2023\/10\/vanillajs-todomvc-stars.png?resize=794%2C272\" alt=\"\" class=\"wp-image-113\" style=\"aspect-ratio:2.9191176470588234;width:436px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2023\/10\/vanillajs-todomvc-stars.png?w=794&amp;ssl=1 794w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2023\/10\/vanillajs-todomvc-stars.png?resize=300%2C103&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2023\/10\/vanillajs-todomvc-stars.png?resize=768%2C263&amp;ssl=1 768w\" sizes=\"auto, (max-width: 794px) 100vw, 794px\" \/><\/figure>\n\n\n\n<p>In general, the responses were very positive. But as with all popular things, eventually, they spark debate.<\/p>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/vanilla-javascript-todomvc\/\">Writing a TodoMVC App with Modern Vanilla JavaScript<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/vanilla-javascript-reactivity\/\">Patterns for Reactivity with Modern Vanilla JavaScript<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/patterns-for-memory-efficient-dom-manipulation\/\">Patterns for Memory Efficient DOM Manipulation with Modern Vanilla JavaScript<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"react--frameworks-vs-vanilla-js-top-four-arguments-for-frameworks\">React \/ Frameworks vs. Vanilla JS: Top Four Arguments for Frameworks<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"1-frameworks-enable-declarative-ui\">#1: \u201cFrameworks Enable Declarative UI\u201d<\/h3>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Modern frameworks like React and Vue don\u2019t exist to fill in the gap left by native JS, they exist so that you write your application in a declarative way where the view is rendered as a function of state.<\/p>\n<\/blockquote>\n\n\n\n<p>IMO this is simply a design pattern. Patterns apply in any language.<\/p>\n\n\n\n<p>You can accomplish roughly the same thing in vanilla JavaScript. In my code, when the model changes, it fires a&nbsp;<code>save<\/code>&nbsp;event, and then I wire&nbsp;<code>App.render()<\/code>&nbsp;to it, which renders the App using the Todos model.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">Todos.addEventListener(<span class=\"hljs-string\">'save'<\/span>, App.render);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Template strings end up pretty easy to work with when you want to re-render parts of the App from scratch as a framework would:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">`\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"view\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"toggle\"<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> ${<span class=\"hljs-attr\">todo.completed<\/span> ? '<span class=\"hljs-attr\">checked<\/span>' <span class=\"hljs-attr\">:<\/span> ''}&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"destroy\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"edit\"<\/span>&gt;<\/span>\n`\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The entire App&nbsp;<code>render<\/code>&nbsp;method is only eleven lines, and it re-renders everything the App needs to based on the state of the App:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">render() {\n  <span class=\"hljs-keyword\">const<\/span> count = Todos.all().length;\n  App.$.setActiveFilter(App.filter);\n  App.$.list.replaceChildren(\n    ...this.Todos.all(<span class=\"hljs-keyword\">this<\/span>.filter).map(<span class=\"hljs-function\">(<span class=\"hljs-params\">todo<\/span>) =&gt;<\/span> <span class=\"hljs-keyword\">this<\/span>.renderTodo(todo))\n  );\n  App.$.showMain(count);\n  App.$.showFooter(count);\n  App.$.showClear(Todos.hasCompleted());\n  App.$.toggleAll.checked = Todos.isAllCompleted();\n  App.$.displayCount(Todos.all(<span class=\"hljs-string\">'active'<\/span>).length);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here I could have chosen to rebuild the entire UI as a template string as a function of state, but instead, it is ultimately more performant to create these DOM helper methods and modify what I want.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"2-frameworks-provide-input-sanitization\">#2: \u201cFrameworks Provide Input Sanitization\u201d<\/h3>\n\n\n\n<p>The best way to sanitize user input is to use&nbsp;<code>node.textContent<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">insertHTML(li, `\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"view\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"toggle\"<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> ${<span class=\"hljs-attr\">todo.completed<\/span> ? '<span class=\"hljs-attr\">checked<\/span>' <span class=\"hljs-attr\">:<\/span> ''}&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"destroy\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"edit\"<\/span>&gt;<\/span>\n`);\nli.querySelector('label').textContent = todo.title;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Any user input must be set to the DOM using&nbsp;<code>textContent<\/code>. If you do that, then you\u2019re fine.<\/p>\n\n\n\n<p>Beyond this, there is a new&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Trusted_Types_API\">Trusted Types API<\/a>&nbsp;for sanitizing generated HTML. I would use this new API if I were generating nested markup with dynamic, user-input data. (Note that this new API isn\u2019t available yet in Safari, but hopefully, it will be soon)<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Trusted Types not being everywhere is fine. You can use them where they\u2019re supported and get early warning of issues. Security improves as browsers improve, and usage turns into an incentive for lagging engines (<a href=\"https:\/\/twitter.com\/slightlylate\/status\/1523425952218292224\">source<\/a>)<\/p>\n<\/blockquote>\n\n\n\n<p>Suppose you want a library to build your app template strings without using textContent manually. In that case, you can use a library like&nbsp;<a href=\"https:\/\/github.com\/cure53\/DOMPurify\">DOMPurify<\/a>, which uses Trusted Types API under the hood.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"3-frameworks-provide-dom-diffing-and-dom-diffing-is-necessary\">#3: \u201cFrameworks Provide DOM Diffing and DOM Diffing is Necessary\u201d<\/h3>\n\n\n\n<p>The most common criticism was the lack of DOM Diffing in vanilla JavaScript.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>A reactive UI\/diff engine is non-negotiable for me.<\/p>\n<\/blockquote>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Diffing is exactly what you need to do (barring newer methods like svelte) to figure out what to tell the browser to change. The vdom tree is much faster to manipulate than DOM nodes.<\/p>\n<\/blockquote>\n\n\n\n<p>However, I think this is a much more balanced take:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Diffing seems necessary when your UI gets complicated to the level that a small change requires a full page re-render. However, I don\u2019t think this is necessary for at least 95% of the websites on the internet.<\/p>\n<\/blockquote>\n\n\n\n<p>I agree most websites and web apps don\u2019t suffer from this issue, even when re-rendering the needed components based on vanilla\u2019s application state like a framework.<\/p>\n\n\n\n<p>Lastly, I\u2019ll note that DOM diffing is inefficient for getting reactive updates because it doubles up data structures. Lit, Svelte, Stencil, Solid, and many others don\u2019t need it and are way more performant as a result. These approaches win on performance and memory use, which matters because garbage collection hurts the UX.<\/p>\n\n\n\n<p>Modern frameworks necessitate that you render the entire App client-side which makes your apps slow by default.<\/p>\n\n\n\n<p>My issue with modern frameworks forcing declarative UI (see #1) and DOM diffing (see #2) approach is that they necessitate unnecessary rendering and slow startup times. Remix is trying to avoid this by rendering server-side then \u201chydrating,\u201d and new approaches like Quik are trying not to have hydration altogether. It\u2019s an industry-wide problem, and people are trying to address it.<\/p>\n\n\n\n<p>In my vanilla JavaScript projects, I only re-render the most minimal parts of the page necessary. Template strings everywhere, and especially adding DOM diffing, is inefficient. It forces you to render all of your App client-side increasing startup time and the amount the client has to do overall each time data changes.<\/p>\n\n\n\n<p>That said, if you do need DOM diffing in parts of a vanilla app, libraries like&nbsp;<a href=\"https:\/\/github.com\/patrick-steele-idem\/morphdom\">morphdom<\/a>&nbsp;do just that. There is also a fantastic templating library called&nbsp;<a href=\"https:\/\/lit.dev\/docs\/v1\/lit-html\/introduction\/\">Lit-html<\/a>&nbsp;that solves this problem of making your App more declarative in a tiny package (~3KB), and you can continue using template strings with that.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"4-frameworks-scale-vanilla-javascript-will-never-scale\">#4: \u201cFrameworks Scale, Vanilla JavaScript Will Never Scale\u201d<\/h3>\n\n\n\n<p>I have built many large vanilla JavaScript projects and scaled them across developers, making the companies I worked for tons of money, and these apps still exist today. \ud83d\udd7a\u2728<\/p>\n\n\n\n<p>Conventions and idioms are always needed, no matter if you build on top of a framework or not.<\/p>\n\n\n\n<p>At the end of the day, your codebase will only be only as good as your team, not the framework.<\/p>\n\n\n\n<p>The way vanilla JS scales is the same way any framework scales. You have to have intelligent people talk about the needs of the codebase and project.<\/p>\n\n\n\n<p><strong>App Architecture Branch<\/strong>:<\/p>\n\n\n\n<p>That said, here\u2019s an example of adding ~20 lines of structure to the code in the&nbsp;<a href=\"https:\/\/github.com\/1Marc\/todomvc-vanillajs-2022\/tree\/app-architecture\/js\">app architecture branch<\/a>. It splits the code into a&nbsp;<code>TodoList<\/code>&nbsp;and&nbsp;<code>App<\/code>&nbsp;component. Each component implements a render method that optionally renders a filtered view of the data.<\/p>\n\n\n\n<p>Overall I\u2019d argue these solutions are more performant, less code (~200 lines only), and more straightforward than most, if not all, the TodoMVC implementations on the internet&nbsp;<em>without<\/em>&nbsp;a framework.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"here-are-eight-vanilla-javascript-tips-from-the-code\">Here are Eight Vanilla JavaScript Tips from the Code<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"1-sanitization\">#1. Sanitization<\/h3>\n\n\n\n<p>User input must be sanitized before being displayed in the HTML to prevent XSS (Cross-Site Scripting). Therefore new todo titles are added to the template string using&nbsp;<code>textContent<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">li.querySelector(<span class=\"hljs-string\">'label'<\/span>).textContent = todo.title;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"2-event-delegation\">#2. Event Delegation<\/h3>\n\n\n\n<p>Since we render the todos frequently, it doesn\u2019t make sense to bind event listeners and clean them up every time. Instead, we bind our events to the parent list that always exists in the DOM and infer which todo was clicked or edited by setting the data attribute of the item&nbsp;<code>$li.dataset.id = todo.id;<\/code><\/p>\n\n\n\n<p>Event delegation uses the&nbsp;<code>matches<\/code>&nbsp;selector:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> delegate = <span class=\"hljs-function\">(<span class=\"hljs-params\">el, selector, event, handler<\/span>) =&gt;<\/span> {\n  el.addEventListener(event, e =&gt; {\n    <span class=\"hljs-keyword\">if<\/span> (e.target.matches(selector)) handler(e, el);\n  });\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When something inside the list is clicked, we read that data attribute id from the inner list item and use it to grab the todo from the model:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">delegate(App.$.<span class=\"hljs-keyword\">list<\/span>, selector, event, e =&gt; {\n  let $el = e.target.closest(<span class=\"hljs-string\">'&#91;data-id]'<\/span>);\n  handler(Todos.get($el.dataset.id), $el, e);\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"3-insertadjacenthtml\">#3. insertAdjacentHTML<\/h3>\n\n\n\n<p>insertAdjacentHTML is&nbsp;<a href=\"https:\/\/www.measurethat.net\/Benchmarks\/Show\/10750\/0\/insertadjacenthtml-vs-innerhtml#latest_results_block\">much faster<\/a>&nbsp;than innerHTML because it doesn\u2019t have to destroy the DOM first before inserting.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> insertHTML = <span class=\"hljs-function\">(<span class=\"hljs-params\">el, html<\/span>) =&gt;<\/span> {\n  el.insertAdjacentHTML(<span class=\"hljs-string\">\"afterbegin\"<\/span>, html);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Bonus tip:&nbsp;<a href=\"https:\/\/twitter.com\/jon_neal\">Jonathan Neal<\/a>&nbsp;taught me through a PR that you can empty elements and replace the contents with&nbsp;<code>el.replaceChildren()<\/code>&nbsp;\u2014 thanks Jonathan!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"4-grouping-dom-selectors--methods\">#4. Grouping DOM Selectors &amp; Methods<\/h3>\n\n\n\n<p>DOM selectors and modifications are scoped to the&nbsp;<code>App.$.*<\/code>&nbsp;namespace. In a way, it makes it self-documenting what our App could potentially modify in the document.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">$: {\n  <span class=\"hljs-attr\">input<\/span>: <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"new\"]'<\/span>),\n  <span class=\"hljs-attr\">toggleAll<\/span>: <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"toggle-all\"]'<\/span>),\n  <span class=\"hljs-attr\">clear<\/span>: <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"clear-completed\"]'<\/span>),\n  <span class=\"hljs-attr\">list<\/span>: <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"list\"]'<\/span>),\n  <span class=\"hljs-attr\">count<\/span>: <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"count\"]'<\/span>),\n  showMain(show) {\n    <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"main\"]'<\/span>).style.display = show ? <span class=\"hljs-string\">'block'<\/span>: <span class=\"hljs-string\">'none'<\/span>;\n  },\n  showFooter(show) {\n    <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-todo=\"main\"]'<\/span>).style.display = show ? <span class=\"hljs-string\">'block'<\/span>: <span class=\"hljs-string\">'none'<\/span>;\n  },\n  showClear(show) {\n    App.$.clear.style.display = show ? <span class=\"hljs-string\">'block'<\/span>: <span class=\"hljs-string\">'none'<\/span>;\n  },\n  setActiveFilter(filter) {\n    <span class=\"hljs-built_in\">document<\/span>.querySelectorAll(<span class=\"hljs-string\">'&#91;data-todo=\"filters\"] a'<\/span>).forEach(<span class=\"hljs-function\"><span class=\"hljs-params\">el<\/span> =&gt;<\/span> el.classList.remove(<span class=\"hljs-string\">'selected'<\/span>)),\n    <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">`&#91;data-todo=\"filters\"] &#91;href=\"#\/<span class=\"hljs-subst\">${filter}<\/span>\"]`<\/span>).classList.add(<span class=\"hljs-string\">'selected'<\/span>);\n  },\n  displayCount(count) {\n    replaceHTML(App.$.count, <span class=\"hljs-string\">`\n      &lt;strong&gt;<span class=\"hljs-subst\">${count}<\/span>&lt;\/strong&gt;\n      <span class=\"hljs-subst\">${count === <span class=\"hljs-number\">1<\/span> ? <span class=\"hljs-string\">'item'<\/span> : <span class=\"hljs-string\">'items'<\/span>}<\/span> left\n    `<\/span>);\n  }\n},\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"5-send-events-on-a-class-instance-with-subclassing-eventtarget\">#5. Send Events on a Class Instance with Subclassing EventTarget<\/h3>\n\n\n\n<p>We can subclass EventTarget to send out events on a class instance for our App to bind to:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> TodoStore = <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">EventTarget<\/span> <\/span>{\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In this case, when the store updates, it sends an event:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">this<\/span>.dispatchEvent(<span class=\"hljs-keyword\">new<\/span> CustomEvent(<span class=\"hljs-string\">'save'<\/span>));\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The App listens to that event and re-renders itself based on the new store data:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">Todos.addEventListener(<span class=\"hljs-string\">'save'<\/span>, App.render);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"6-group-setting-up-event-listeners\">#6. Group Setting Up Event Listeners<\/h3>\n\n\n\n<p>It is essential to know exactly where the global event listeners are set. An excellent place to do that is in the App init method:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">init() {\n  Todos.addEventListener(<span class=\"hljs-string\">'save'<\/span>, App.render);\n  App.filter = getURLHash();\n  <span class=\"hljs-built_in\">window<\/span>.addEventListener(<span class=\"hljs-string\">'hashchange'<\/span>, () =&gt; {\n    App.filter = getURLHash();\n    App.render();\n  });\n  App.$.input.addEventListener(<span class=\"hljs-string\">'keyup'<\/span>, e =&gt; {\n    <span class=\"hljs-keyword\">if<\/span> (e.key === <span class=\"hljs-string\">'Enter'<\/span> &amp;&amp; e.target.value.length) {\n      Todos.add({ <span class=\"hljs-attr\">title<\/span>: e.target.value, <span class=\"hljs-attr\">completed<\/span>: <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-string\">\"id_\"<\/span> + <span class=\"hljs-built_in\">Date<\/span>.now() })\n      App.$.input.value = <span class=\"hljs-string\">''<\/span>;\n    }\n  });\n  App.$.toggleAll.addEventListener(<span class=\"hljs-string\">'click'<\/span>, e =&gt; {\n    Todos.toggleAll();\n  });\n  App.$.clear.addEventListener(<span class=\"hljs-string\">'click'<\/span>, e =&gt; {\n    Todos.clearCompleted();\n  });\n  App.bindTodoEvents();\n  App.render();\n},\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here we set up all the global event listeners, subscribe to the store mentioned above, and then initially render the App.<\/p>\n\n\n\n<p>Similarly, when you create new DOM elements and insert them into the page, group the event listeners associated with the new elements near where they are made.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"7-use-data-attributes-in-markup--selectors\">#7. Use Data Attributes in Markup &amp; Selectors<\/h3>\n\n\n\n<p>One issue with JavaScript is your selectors get tightly coupled to the generated DOM.<\/p>\n\n\n\n<p>To fix this, classes should be used for CSS rules, and data atributes for JavaScript behavior.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">data-jsmodule<\/span>=<span class=\"hljs-string\">\"behavior\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'&#91;data-jsmodule=\"behavior\"]'<\/span>)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"8-render-the-state-of-the-world-based-on-data-data-flowing-down\">#8. Render the State of the World Based on Data (Data Flowing Down)<\/h3>\n\n\n\n<p>Lastly, to reiterate what I said above, render everything based on the state in the&nbsp;<code>render()<\/code>&nbsp;method. This is a pattern lifted from modern frameworks.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"make-sure-you-update-the-dom-based-on-your-app-state-not-the-other-way-around\">Make sure you update the DOM based on your App state, not the other way around.<\/h4>\n\n\n\n<p>It\u2019s even better if you avoid reading DOM to derive&nbsp;<em>any<\/em>&nbsp;part of your app state aside from finding your target for event delegation.<\/p>\n\n\n\n<p>Side note: I like to rely on the server to generate the markup for faster boot times, then take control of the bits we show. Have the CSS initially hide things you don\u2019t need, and then have the JavaScript show the elements based on the state. Let the server do most of the work where you can, rather than wait for the entire App to render client-side.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"in-conclusion\">In Conclusion<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"vanilla-js-is-viable-today-for-building-web-apps\">Vanilla JS is Viable Today for Building Web Apps<\/h3>\n\n\n\n<p>JavaScript is better today than it has ever been.<\/p>\n\n\n\n<p>The fact that I could shave off 80% of the code over the previous TodoMVC years ago at the drop of a hat feels terrific. Plus, we now have established design patterns that we can lift from modern frameworks to apply to vanilla JavaScript projects to make our UIs as declarative as we like.<\/p>\n\n\n\n<p>As an industry we should consider pure JavaScript as an option for more projects.<\/p>\n\n\n\n<p>Finally, as Web Components get more ergonomic, we will even have a way to share our code in an interoperable and framework-agnostic way.<\/p>\n\n\n\n<p>I hope you enjoyed the post. Please send your feedback to me&nbsp;<a href=\"https:\/\/twitter.com\/1Marc\">@1marc on Twitter<\/a>. Cheers!<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"bonus-performant-rendering-of-large-lists\">Bonus: Performant Rendering of Large Lists<\/h2>\n\n\n\n<p>The code for rendering the entire list contents on model change is clean because data flows down, but it potentially will not be as performant for rendering large lists.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"more-performant--granular-dom-updates-with-vanilla\">More Performant &amp; Granular DOM Updates with Vanilla<\/h3>\n\n\n\n<p>Here\u2019s a branch sending specific events with context from the model so we can make DOM updates more selectively as we need them: (<a href=\"https:\/\/github.com\/1Marc\/todomvc-vanillajs-2022\/commit\/fc89da1a6bd15489d5256575a4e193e11efd8d43\">see granular DOM updates diff<\/a>).<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/1Marc\/modern-todomvc-vanillajs\/tree\/performant-rendering\">performant-rendering branch<\/a><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"more-performant-dom-updates-with-lit-html-plus-animations\">More Performant DOM Updates with lit-html (plus animations!)<\/h3>\n\n\n\n<p>We can acheieve the same performant DOM updates with far less code by adopting lit-html using the repeat directive: (<a href=\"https:\/\/github.com\/1Marc\/todomvc-vanillajs-2022\/commit\/ef86a73166029991dc88c649f7ec4931a2a96c86\">see adding lit-html diff<\/a>).<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/1Marc\/modern-todomvc-vanillajs\/tree\/animation-lithtml\">animation-lithtml branch<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/vanilla-javascript-todomvc\/\">Writing a TodoMVC App with Modern Vanilla JavaScript<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/vanilla-javascript-reactivity\/\">Patterns for Reactivity with Modern Vanilla JavaScript<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/patterns-for-memory-efficient-dom-manipulation\/\">Patterns for Memory Efficient DOM Manipulation with Modern Vanilla JavaScript<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>I took a shot at coding&nbsp;TodoMVC with modern (ES6+), vanilla JavaScript, and it only took ~170 lines of code and just over an hour! Compare this to the old\/official TodoMVC vanilla JS solution, which has over 900 lines of&nbsp;code. An 80%+ reduction in code! I \u2764\ufe0f the new state of JavaScript. The code has received [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":42,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[3,5,6,4],"class_list":["post-7","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-javascript","tag-mvc","tag-todo","tag-vanilla"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2023\/10\/todos-featured.png?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=7"}],"version-history":[{"count":7,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7\/revisions"}],"predecessor-version":[{"id":3288,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7\/revisions\/3288"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/42"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}