{"id":1169,"date":"2024-03-08T18:23:53","date_gmt":"2024-03-09T00:23:53","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=1169"},"modified":"2024-03-08T18:23:53","modified_gmt":"2024-03-09T00:23:53","slug":"building-a-todo-app-from-scratch-step-5-extra-functionality","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-5-extra-functionality\/","title":{"rendered":"Building a TODO App from Scratch \u2014 Step 5 \u2014 Extra Functionality"},"content":{"rendered":"\n<p>We left off in <a href=\"https:\/\/codepen.io\/chriscoyier\/pen\/YzgboPm\">a decent place<\/a>, but there was some missing functionality that we wanted all along. <\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>We need to be able to view completed to-dos (and be able to delete them entirely)<\/li>\n\n\n\n<li>We need to be able edit to-dos. <\/li>\n<\/ol>\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\/building-a-todo-app-from-scratch-step-1-planning-design\/\">Building a TODO App from Scratch \u2014 Step 1 \u2014 Planning &#038; Design<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-2-html\/\">Building a TODO App from Scratch \u2014 Step 2 \u2014 HTML<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-3-basic-javascript-functionality\/\">Building a TODO App from Scratch \u2014 Step 3 \u2014 Basic JavaScript Functionality<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-4-styling-interactive-choices\/\">Building a TODO App from Scratch \u2014 Step 4 \u2014 Styling &#038; Interactive Choices<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-5-extra-functionality\/\">Building a TODO App from Scratch \u2014 Step 5 \u2014 Extra Functionality<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Editing To-Dos<\/h2>\n\n\n\n<p>The interaction we decided on for editing is to double-click the todo. This will turn the to-do, in place, into an editable input. Then you hit enter (e.g. submit) or leave the input (e.g. blur) and it will save the changes. <\/p>\n\n\n\n<p>We can use a bit of event delegation to set up this event:<\/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\">list.addEventListener(<span class=\"hljs-string\">\"dblclick\"<\/span>, (event) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> listItem = event.target.closest(<span class=\"hljs-string\">\"li\"<\/span>);\n\n  <span class=\"hljs-comment\">\/\/ If already editing, let it be.<\/span>\n  <span class=\"hljs-keyword\">if<\/span> (listItem.classList.contains(<span class=\"hljs-string\">\"editing\"<\/span>)) <span class=\"hljs-keyword\">return<\/span>;\n\n  listItem.classList.add(<span class=\"hljs-string\">\"editing\"<\/span>);\n\n  <span class=\"hljs-comment\">\/\/ Do editing.<\/span>\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>Now anywhere you double-click on the list will set the relevant list item into &#8220;editing mode&#8221;, here indicated by just adding a class we can style against, like hide the existing text.<\/p>\n\n\n\n<p>More importantly, we need to insert some new HTML turning the text into an editable input. We can use a template literal of a <code>&lt;form&gt;<\/code> to inject as needed, like so:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table\"><span class='shcb-loc'><span>list.addEventListener(<span class=\"hljs-string\">\"dblclick\"<\/span>, (event) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> listItem = event.target.closest(<span class=\"hljs-string\">\"li\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-comment\">\/\/ If already editing, let it be.<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (listItem.classList.contains(<span class=\"hljs-string\">\"editing\"<\/span>)) <span class=\"hljs-keyword\">return<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  listItem.classList.add(<span class=\"hljs-string\">\"editing\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> textItem = listItem.querySelector(<span class=\"hljs-string\">\".text\"<\/span>);\n<\/span><\/span><mark class='shcb-loc'><span>  listItem.insertAdjacentHTML(\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-string\">\"beforeend\"<\/span>,\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-string\">`&lt;form onsubmit=\"updateTodo(event);\" class=\"form-edit\"&gt;<\/span>\n<\/span><\/mark><mark class='shcb-loc'><span><span class=\"hljs-string\">       &lt;input onblur=\"updateTodo(event);\" type=\"text\" class=\"input-edit\" value=\"<span class=\"hljs-subst\">${textItem.textContent}<\/span>\"&gt;<\/span>\n<\/span><\/mark><mark class='shcb-loc'><span><span class=\"hljs-string\">     &lt;\/form&gt;`<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>});\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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>That calls an <code>updateTodo()<\/code> event we&#8217;ll have to write. But first, let&#8217;s make sure we focus the input and put the cursor at the end. Just a bit of nice UX right?<\/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 shcb-code-table\"><span class='shcb-loc'><span>list.addEventListener(<span class=\"hljs-string\">\"dblclick\"<\/span>, (event) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> listItem = event.target.closest(<span class=\"hljs-string\">\"li\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-comment\">\/\/ If already editing, let it be.<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (listItem.classList.contains(<span class=\"hljs-string\">\"editing\"<\/span>)) <span class=\"hljs-keyword\">return<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  listItem.classList.add(<span class=\"hljs-string\">\"editing\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> textItem = listItem.querySelector(<span class=\"hljs-string\">\".text\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  listItem.insertAdjacentHTML(\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"beforeend\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">`&lt;form onsubmit=\"updateTodo(event);\" class=\"form-edit\"&gt;&lt;input onblur=\"updateTodo(event);\" type=\"text\" class=\"input-edit\" value=\"<span class=\"hljs-subst\">${textItem.textContent}<\/span>\"&gt;&lt;\/form&gt;`<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> input = listItem.querySelector(<span class=\"hljs-string\">\".input-edit\"<\/span>);\n<\/span><\/mark><mark class='shcb-loc'><span>  input.focus();\n<\/span><\/mark><mark class='shcb-loc'><span>\n<\/span><\/mark><mark class='shcb-loc'><span>  <span class=\"hljs-comment\">\/\/ put cursor at end of input<\/span>\n<\/span><\/mark><mark class='shcb-loc'><span>  input.setSelectionRange(input.value.length, input.value.length);\n<\/span><\/mark><span class='shcb-loc'><span>});\n<\/span><\/span><\/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>Updating the to-do is pretty straightforward. We get our hands on the new text, update it in the DOM, toggle the editing class, and write the data back to localStorage. It looks like a lot of lines, but a lot of it is just getting our hands on DOM elements and basic manipulation.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">updateTodo<\/span>(<span class=\"hljs-params\">event<\/span>) <\/span>{\n  event.preventDefault();\n  <span class=\"hljs-keyword\">const<\/span> listItem = event.target.closest(<span class=\"hljs-string\">\"li\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> textItem = listItem.querySelector(<span class=\"hljs-string\">\".text\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> inputItem = listItem.querySelector(<span class=\"hljs-string\">\".input-edit\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> form = listItem.querySelector(<span class=\"hljs-string\">\".form-edit\"<\/span>);\n  textItem.textContent = inputItem.value;\n  listItem.classList.remove(<span class=\"hljs-string\">\"editing\"<\/span>);\n  form.remove();\n  TODOs = TODOs.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">todo<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">if<\/span> (todo.id === listItem.id) {\n      todo.title = inputItem.value;\n    }\n    <span class=\"hljs-keyword\">return<\/span> todo;\n  });\n  localStorage&#91;<span class=\"hljs-string\">\"data\"<\/span>] = <span class=\"hljs-built_in\">JSON<\/span>.stringify(TODOs);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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>And it works!<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='276' src='https:\/\/videopress.com\/embed\/PlKE4DQw?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">Viewing Completed To-Dos<\/h2>\n\n\n\n<p>Before this, we could delete a to-do, but that was it. Even though our data structure was set up to have a <code>complete<\/code> attribute that could change, all we did was <code>filter<\/code> out the completed ones entirely from the data. <\/p>\n\n\n\n<p>Here&#8217;s what that data structure is like:<\/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\">{\n  <span class=\"hljs-attr\">title<\/span>: <span class=\"hljs-string\">`text of to-do`<\/span>,\n  <span class=\"hljs-attr\">complete<\/span>: <span class=\"hljs-literal\">false<\/span>,\n  <span class=\"hljs-attr\">id<\/span>: self.crypto.randomUUID()\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<p>Now, when we check that checkbox in the UI to complete a to-do, we need to:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Set <code>complete<\/code> to <code>true<\/code> if the list item is in the active list<\/li>\n\n\n\n<li>Remove the list item entirely if the to-do is already in the completed list<\/li>\n<\/ol>\n\n\n\n<p>We&#8217;ll update our function to be called <code>toggleTodo<\/code> and do it like this:<\/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-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">toggleTodo<\/span>(<span class=\"hljs-params\">event<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> listItem = event.target.parentElement;\n  <span class=\"hljs-comment\">\/\/ Trigger complete animation<\/span>\n  listItem.classList.toggle(<span class=\"hljs-string\">\"complete\"<\/span>);\n  setTimeout(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-comment\">\/\/ list item is already complete, remove it<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (listItem.dataset.complete === <span class=\"hljs-string\">\"true\"<\/span>) {\n      TODOs = TODOs.filter(<span class=\"hljs-function\">(<span class=\"hljs-params\">todo<\/span>) =&gt;<\/span> !todo.complete);\n    } <span class=\"hljs-keyword\">else<\/span> {\n      <span class=\"hljs-comment\">\/\/ list item is just being set to complete now<\/span>\n      TODOs.forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\">todo<\/span>) =&gt;<\/span> {\n        <span class=\"hljs-keyword\">if<\/span> (todo.id === listItem.id) {\n          todo.complete = !todo.complete;\n        }\n      });\n    }\n\n    localStorage&#91;<span class=\"hljs-string\">\"data\"<\/span>] = <span class=\"hljs-built_in\">JSON<\/span>.stringify(TODOs);\n\n    <span class=\"hljs-keyword\">if<\/span> (!<span class=\"hljs-built_in\">document<\/span>.startViewTransition) {\n      buildUI();\n    } <span class=\"hljs-keyword\">else<\/span> {\n      <span class=\"hljs-built_in\">document<\/span>.startViewTransition(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n        buildUI();\n      });\n    }\n  }, <span class=\"hljs-number\">1000<\/span>);\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>This isn&#8217;t a big change from last time, just one little fork in the logic that either removes it or updates the data. <\/p>\n\n\n\n<p>Now we need a control though to decide if we&#8217;re looking at the active to-dos or the completed ones. Let&#8217;s make that control in HTML:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" 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\">class<\/span>=<span class=\"hljs-string\">\"todo-type-toggles\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">aria-pressed<\/span>=<span class=\"hljs-string\">\"true\"<\/span>&gt;<\/span>Active<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span>&gt;<\/span>Completed<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><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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>Now when you press the buttons, we&#8217;ll swap the state and re-build the UI accordingly:<\/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\">const<\/span> toggles = <span class=\"hljs-built_in\">document<\/span>.querySelectorAll(<span class=\"hljs-string\">\".todo-type-toggles &gt; button\"<\/span>);\n\ntoggles.forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\">toggle<\/span>) =&gt;<\/span> {\n  toggle.addEventListener(<span class=\"hljs-string\">\"click\"<\/span>, (event) =&gt; {\n    toggles.forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\">toggle<\/span>) =&gt;<\/span> {\n      toggle.setAttribute(<span class=\"hljs-string\">\"aria-pressed\"<\/span>, <span class=\"hljs-literal\">false<\/span>);\n    });\n    toggle.setAttribute(<span class=\"hljs-string\">\"aria-pressed\"<\/span>, <span class=\"hljs-literal\">true<\/span>);\n\n    <span class=\"hljs-keyword\">if<\/span> (toggle.textContent === states.ACTIVE) {\n      buildUI(states.ACTIVE);\n    } <span class=\"hljs-keyword\">else<\/span> {\n      buildUI(states.COMPLETED);\n    }\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>Now I&#8217;m calling <code>buildUI()<\/code> with a param to declare which type I want to see. I like using little ENUM type variables for this just to make sure we don&#8217;t do typos.<\/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\"><span class=\"hljs-keyword\">const<\/span> states = {\n  <span class=\"hljs-attr\">ACTIVE<\/span>: <span class=\"hljs-string\">\"Active\"<\/span>,\n  <span class=\"hljs-attr\">COMPLETED<\/span>: <span class=\"hljs-string\">\"Completed\"<\/span>\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<p>Then we update the function to display one or the other&#8230;<\/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-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">buildUI<\/span>(<span class=\"hljs-params\">state<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> HTML = <span class=\"hljs-string\">``<\/span>;\n  <span class=\"hljs-keyword\">let<\/span> viewTODOs = &#91;];\n\n  <span class=\"hljs-keyword\">if<\/span> (state === states.COMPLETED) {\n    viewTODOs = TODOs.filter(<span class=\"hljs-function\">(<span class=\"hljs-params\">todo<\/span>) =&gt;<\/span> todo.complete);\n  } <span class=\"hljs-keyword\">else<\/span> {\n    viewTODOs = TODOs.filter(<span class=\"hljs-function\">(<span class=\"hljs-params\">todo<\/span>) =&gt;<\/span> !todo.complete);\n  }\n\n  <span class=\"hljs-keyword\">if<\/span> (viewTODOs.length === <span class=\"hljs-number\">0<\/span>) {\n    HTML = <span class=\"hljs-string\">`&lt;li class=\"empty\"&gt;Nothing to do!&lt;\/li&gt;`<\/span>;\n  }\n\n  <span class=\"hljs-comment\">\/\/ Loop over the viewTODOs and build HTML to insert, exactly as before.<\/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>This gives us an empty state as well.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">And we&#8217;ve done it!<\/h2>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='426' src='https:\/\/videopress.com\/embed\/olpuVk2K?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Perhaps more back-endy readers will be like <em>&#8220;is this dude not sanitizing data before it goes to data storage?&#8221;<\/em> and that would be a smart observation. We should probably be sanitizing the HTML. But for now, the only person you can pwn with this is yourself, so not a massive deal. <\/p>\n\n\n\n<p>Here&#8217;s where we&#8217;ve gotten now:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_XWQJJRq\" src=\"\/\/codepen.io\/anon\/embed\/XWQJJRq?height=450&amp;theme-id=47434&amp;slug-hash=XWQJJRq&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed XWQJJRq\" title=\"CodePen Embed XWQJJRq\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\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\/building-a-todo-app-from-scratch-step-1-planning-design\/\">Building a TODO App from Scratch \u2014 Step 1 \u2014 Planning &#038; Design<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-2-html\/\">Building a TODO App from Scratch \u2014 Step 2 \u2014 HTML<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-3-basic-javascript-functionality\/\">Building a TODO App from Scratch \u2014 Step 3 \u2014 Basic JavaScript Functionality<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-4-styling-interactive-choices\/\">Building a TODO App from Scratch \u2014 Step 4 \u2014 Styling &#038; Interactive Choices<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/building-a-todo-app-from-scratch-step-5-extra-functionality\/\">Building a TODO App from Scratch \u2014 Step 5 \u2014 Extra Functionality<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>We left off in a decent place, but there was some missing functionality that we wanted all along. Editing To-Dos The interaction we decided on for editing is to double-click the todo. This will turn the to-do, in place, into an editable input. Then you hit enter (e.g. submit) or leave the input (e.g. blur) [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1181,"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":[31,3,6],"class_list":["post-1169","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-html","tag-javascript","tag-todo"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/todo5-thumb.jpg?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1169","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\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=1169"}],"version-history":[{"count":7,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1169\/revisions"}],"predecessor-version":[{"id":1188,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1169\/revisions\/1188"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/1181"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=1169"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=1169"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=1169"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}