1. Start With the Required Belongings
To make the structure a bit extra distinctive, we’ll use some handmade SVG illustrations and a customized font taken from Envato Components.
It’s value noting that the majority of those property will come from a earlier tutorial. In precise reality, we’ll additionally use numerous the positioning methods that we discovered on this tutorial, so it’s value studying it.
2. Proceed With the Web page Markup
We’ll begin with an SVG and a div
container:
<svg type="show:none;">...</svg> <div class="container">...</div>
SVG Sprites
Like we’ve carried out many instances up to now, as an excellent follow, we’ll retailer all SVGs as image
s in an SVG sprite container. Then, we’ll render them on the display screen every time we’d like by calling the use
aspect.
Right here’s the markup for the SVG sprite:
<svg type="show:none;"> <image id="enter" viewBox="0 0 345.27 56.51" preserveAspectRatio="none">...</image> <image id="checkbox_empty" viewBox="0 0 33.18 33.34">...</image> <image id="checkmark" viewBox="0 0 37.92 33.3" preserveAspectRatio="none">...</image> <image id="button" viewBox="0 0 256.6 60.02" preserveAspectRatio="none">...</image> <image id="shut" viewBox="0 0 29.71 30.59">...</image> <image id="stats" viewBox="0 0 998.06 602.62" preserveAspectRatio="none">...</image> </svg>
Discover the preserveAspectRatio="none"
attribute which we connected to many of the illustrations. We’ve carried out this as a result of, as we’ll see later, our icons will scale and lose their preliminary dimensions.
Container
The container will embrace a type, a div
aspect, and an empty ordered checklist:
<div class="container"> <type class="todo-form">...</type> <div class="todo-stats">...</div> <ol class="todo-list"></ol> </div>
Inside the shape, we’ll have an enter and a submit button together with their related SVGs:
<type class="todo-form"> <div class="form-wrapper"> <enter sort="textual content" title="title" autofocus> <svg> <use xlink:href="#enter"></use> </svg> </div> <div class="form-wrapper"> <button sort="submit">Add new activity</button> <svg> <use xlink:href="#button"></use> </svg> </div> </type>
Discover the title
attribute that we’ve added to the enter discipline. Later we’ll use this attribute to entry the enter worth after the shape submission.
Word: In our demo, the autofocus
attribute of the textual content discipline gained’t work. In actual fact, it’ll throw the next error which you’ll see in the event you open your browser console:
Nonetheless, in the event you run this app regionally (not as a Codepen venture), this concern gained’t exist. Alternatively, you possibly can set the main target by way of JavaScript.
Contained in the div
, we’ll place three nested div
s and the related SVG. On this part we’ll maintain monitor of the overall variety of duties (each remaining and accomplished):
<div class="todo-stats"> <div class="total-tasks"> Whole Duties: <span>0</span> </div> <div class="completed-tasks"> Accomplished Duties: <span>0</span> </div> <div class="remaining-tasks"> Remaining Duties: <span>0</span> </div> <svg> <use xlink:href="#stats"></use> </svg> </div>
Lastly, the objects of the ordered checklist will likely be added dynamically by way of JavaScript.
3. Outline Some Fundamental Kinds
With the markup prepared, we’ll proceed with some reset types:
@font-face { font-family: "Summer time"; src: url(SummerFont-Common.woff); } @font-face { font-family: "Summer time Daring"; src: url(SummerFont-Daring.woff); } :root { --white: #fff; } * { padding: 0; margin: 0; border: none; define: none; box-sizing: border-box; } enter, button { font-family: inherit; font-size: 100%; background: none; } [type="checkbox"] { place: absolute; left: -9999px; } button, label { cursor: pointer; } ol { list-style: none; } physique { font: 28px/1.2 "Summer time"; margin: 1.5rem 0; }
4. Set the Important Kinds
Let’s now focus on the principle types of our TODO app.
Container Kinds
The container could have a most width with horizontally centered content material:
.container { max-width: 700px; padding: 0 10px; margin: 0 auto; }
Type Kinds
On small screens all type components will likely be stacked:
Nonetheless, on viewports 600 pixels vast and above, the shape structure will change as follows:
Let’s be aware of two issues:
- On vast viewports, the enter will likely be twice the scale of the button.
- The SVGs will likely be completely positioned components and sit under their adjoining type management. Once more, for a extra detailed clarification, take a look at this earlier tutorial.
Listed below are the types for this part:
/*CUSTOM VARIABLES HERE*/ .todo-form .form-wrapper { place: relative; } .todo-form enter, .todo-form button { place: relative; width: 100%; z-index: 1; padding: 15px; } .todo-form svg { place: absolute; high: 0; left: 0; width: 100%; peak: 100%; } .todo-form button { shade: var(--white); text-transform: uppercase; } @media display screen and (min-width: 600px) { .todo-form { show: grid; grid-template-columns: 2fr 1fr; grid-column-gap: 5px; } }
Stats Kinds
Subsequent, let’s have a look at the standing bar which can give us a fast report concerning the complete variety of duties.
On small screens it is going to have the next stacked look:
Nonetheless, on viewports 600 pixels vast and above, it ought to change as follows:
Let’s be aware of two issues:
- On vast viewports, all baby
div
components could have equal widths. - Equally to the earlier SVGs, this may also be completely positioned and act as a background picture that covers the entire part.
The associated types:
/*CUSTOM VARIABLES HERE*/ .todo-stats { place: relative; text-align: heart; padding: 5px 10px; margin: 10px 0; shade: var(--white); } .todo-stats > div { place: relative; z-index: 1; } .todo-stats svg { place: absolute; high: 0; left: 0; width: 100%; peak: 100%; } @media display screen and (min-width: 600px) { .todo-stats { show: grid; grid-template-columns: repeat(3, 1fr); } }
Job Kinds
The duties structure, which we’ll generate dynamically within the upcoming part, will appear to be this:
Every activity which will likely be represented by a li
could have two elements.
Within the first half, you’ll see a checkbox together with the duty title. Within the second half, you’ll discover a delete button for eradicating the duty.
Listed below are the associated types:
.todo-list li { show: grid; align-items: baseline; grid-template-columns: auto 20px; grid-column-gap: 10px; padding: 0 10px; } .todo-list li + li { margin-top: 10px; }
When a activity is incomplete, an empty checkbox will seem. Then again, if a activity is marked as accomplished, a checkmark will seem. Moreover, its title will likely be given 50% opacity in addition to a line by way of it.
Listed below are the types accountable for this conduct:
.todo-list .checkbox-wrapper { show: flex; align-items: baseline; } .todo-list .checkbox-wrapper label { show: grid; margin-right: 10px; } .todo-list .checkbox-wrapper svg { grid-column: 1; grid-row: 1; width: 20px; peak: 20px; } .todo-list .checkbox-wrapper .checkmark { show: none; } .todo-list [type="checkbox"]:checked + label .checkmark { show: block; } .todo-list [type="checkbox"]:checked ~ span { text-decoration: line-through; opacity: 0.5; }
Lastly, under are the types for the delete button:
.todo-list .remove-task { show: flex; padding: 2px; } .todo-list .remove-task svg { width: 16px; peak: 16px; }
5. Add the JavaScript
At this level, we’re able to construct the core performance of our TODO checklist app. Let’s do it!
On Type Submission
Every time a person submits the shape by urgent the Enter key or the Submit button, we’ll do the next issues:
- Cease the shape from submitting, thereby stopping a reload of the web page.
- Seize the worth which is contained within the enter discipline.
- Assuming that the enter discipline isn’t empty, we’ll create a brand new object literal which can characterize the duty. Every activity could have a singular id, a reputation, and be energetic (not accomplished) by default.
- Add this activity to the
duties
array. - Retailer the array in native storage. Native storage solely helps strings, so to do it, now we have to make use of the
JSON.stringify()
technique to transform the objects contained in the array into strings. - Name the
createTask()
perform for visually representing the duty on the display screen. - Clear the shape.
- Give focus to the enter discipline.
Right here’s the related code:
const todoForm = doc.querySelector(".todo-form"); let duties = []; todoForm.addEventListener("submit", perform(e) { // 1 e.preventDefault(); // 2 const enter = this.title; const inputValue = enter.worth; if (inputValue != "") { // 3 const activity = { id: new Date().getTime(), title: inputValue, isCompleted: false }; // 4 duties.push(activity); // 5 localStorage.setItem("duties", JSON.stringify(duties)); // 6 createTask(activity); // 7 todoForm.reset(); } // 8 enter.focus(); });
Create a Job
The createTask()
perform will likely be accountable for creating the duty’s markup.
As an illustration, right here’s the construction for the “Go for a stroll” activity:
Two issues are essential right here:
- If the duty is accomplished, the checkmark will seem.
- If the duty isn’t accomplished, its
span
aspect will obtain thecontenteditable
attribute. This attribute will give us the power to edit/replace its title.
Beneath is the syntax for this perform:
perform createTask(activity) { const taskEl = doc.createElement("li"); taskEl.setAttribute("id", activity.id); const taskElMarkup = ` <div class="checkbox-wrapper"> <enter sort="checkbox" id="${activity.title}-${activity.id}" title="duties" ${ activity.isCompleted ? "checked" : "" }> <label for="${activity.title}-${activity.id}"> <svg class="checkbox-empty"> <use xlink:href="#checkbox_empty"></use> </svg> <svg class="checkmark"> <use xlink:href="#checkmark"></use> </svg> </label> <span ${!activity.isCompleted ? "contenteditable" : ""}>${activity.title}</span> </div> <button class="remove-task" title="Take away ${activity.title} activity"> <svg> <use xlink:href="#shut"></use> </svg> </button> `; taskEl.innerHTML = taskElMarkup; todoList.appendChild(taskEl); countTasks(); }
Replace a Job
A activity will be up to date in two alternative ways:
- By altering its standing from “incomplete” to “accomplished” and vice versa.
- By modifying its title in case the duty is incomplete. Keep in mind that on this case, the
span
aspect has thecontenteditable
attribute.
To maintain monitor of those adjustments, we’ll benefit from the enter
occasion. That is a suitable occasion for us as a result of it applies each to enter
components and components with contenteditable
enabled.
The tough factor is that we can not immediately connect this occasion to the goal components (checkbox, span
) as a result of they’re created dynamically and aren’t a part of the DOM on web page load.
Due to the occasion delegation, we’ll connect the enter
occasion to the guardian checklist which is a part of the preliminary markup. Then, by way of the goal
property of that occasion we’ll examine the weather on which the occasion occurred and name the updateTask()
perform:
todoList.addEventListener("enter", (e) => { const taskId = e.goal.closest("li").id; updateTask(taskId, e.goal); });
Contained in the updateTask()
perform, we’ll do the next issues:
- Seize the duty that must be up to date.
- Examine the aspect that triggered the occasion. If the aspect has the
contenteditable
attribute (i.e. it’s thespan
aspect), we’ll set the duty’s title equal to thespan
’s textual content content material. - In any other case (i.e. it’s the checkbox), we’ll toggle the duty’s standing and its
checked
attribute. Plus, we’ll additionally toggle thecontenteditable
attribute of the adjoiningspan
. - Replace the worth of the
duties
key in native storage. - Name the
countTasks()
perform.
Right here’s the syntax for this perform:
perform updateTask(taskId, el) { // 1 const activity = duties.discover((activity) => activity.id === parseInt(taskId)); if (el.hasAttribute("contentEditable")) { // 2 activity.title = el.textContent; } else { // 3 const span = el.nextElementSibling.nextElementSibling; activity.isCompleted = !activity.isCompleted; if (activity.isCompleted) { span.removeAttribute("contenteditable"); el.setAttribute("checked", ""); } else { el.removeAttribute("checked"); span.setAttribute("contenteditable", ""); } } // 4 localStorage.setItem("duties", JSON.stringify(duties)); // 5 countTasks(); }
Take away a Job
We will take away a activity by way of the “shut” button.
Just like the replace operation, we can not immediately connect an occasion to this button as a result of it isn’t within the DOM when the web page hundreds.
Thanks once more to the occasion delegation, we’ll connect a click on
occasion to the guardian checklist and carry out the next actions:
- Examine if the aspect that’s clicked is the “shut” button or its baby SVG.
- If that occurs, we’ll seize the
id
of the guardian checklist merchandise. - Go this
id
to theremoveTask()
perform.
Right here’s the related code:
const todoList = doc.querySelector(".todo-list"); todoList.addEventListener("click on", (e) => { // 1 if ( e.goal.classList.accommodates("remove-task") || e.goal.parentElement.classList.accommodates("remove-task") ) { // 2 const taskId = e.goal.closest("li").id; // 3 removeTask(taskId); } });
Contained in the removeTask()
perform, we’ll do the next issues:
- Take away from the
duties
array the related activity. - Replace the worth of the
duties
key in native storage. - Take away the related checklist merchandise.
- Name the
countTasks()
perform.
Right here’s the syntax for this perform:
perform removeTask(taskId) { // 1 duties = duties.filter((activity) => activity.id !== parseInt(taskId)); // 2 localStorage.setItem("duties", JSON.stringify(duties)); // 3 doc.getElementById(taskId).take away(); // 4 countTasks(); }
Depend Duties
As we’ve already mentioned, most of the capabilities above embrace the countTask()
perform.
Its job is to observe the duties for adjustments (additions, updates, deletions) and replace the content material of the associated components.
Right here’s its signature:
const totalTasks = doc.querySelector(".total-tasks span"); const completedTasks = doc.querySelector(".completed-tasks span"); const remainingTasks = doc.querySelector(".remaining-tasks span"); perform countTasks() { totalTasks.textContent = duties.size; const completedTasksArray = duties.filter((activity) => activity.isCompleted === true); completedTasks.textContent = completedTasksArray.size; remainingTasks.textContent = duties.size - completedTasksArray.size; }
Forestall Including New Traces
Every time a person updates the title of a activity, they shouldn’t be capable of create new traces by urgent the Enter key.
To disable this performance, as soon as once more we’ll benefit from the occasion delegation and fasten the keydown
occasion to the checklist, like this:
todoList.addEventListener("keydown", perform (e) { if (e.keyCode === 13) { e.preventDefault(); } });
Word that on this state of affairs solely the span
components may set off that occasion, so there’s no have to make an extra examine like this:
if (e.goal.hasAttribute("contenteditable") && e.keyCode === 13) { e.preventDefault(); }
Persist Knowledge on Web page Load
To this point, if we shut the browser and navigate to the demo venture, our duties will disappear.
However, wait that isn’t 100% true! Keep in mind that every time we do a activity manipulation, we additionally retailer the duties
array in native storage. For instance, in Chrome, to see the native storage keys and values, click on the Utility tab then, increase the Native Storage menu and eventually click on a site to view its key-value pairs.
In my case, listed below are the values for the duties
key:
So, to show these duties, we first have to retrieve them from native storage. To do that, we’ll use the JSON.parse()
technique which can convert the strings again to JavaScript objects.
Subsequent, we’ll retailer all duties within the acquainted duties
array. Understand that if there’s no information in native storage (as an illustration the very first time we go to the app), this array is empty. Then, now we have to iterate by way of the array, and for every activity, name the createTask()
perform. And, that’s all!
The corresponding code snippet:
let duties = JSON.parse(localStorage.getItem("duties")) || []; if (localStorage.getItem("duties")) { duties.map((activity) => { createTask(activity); }); }
Conclusion
Phew! Thanks for following alongside on this lengthy journey people. Hopefully, you gained some new information right this moment which you’ll be capable to apply to your individual initiatives.
Let’s remind ourselves what we constructed:
For sure, constructing such an app with a JavaScript framework is perhaps extra secure, simple, and environment friendly (repainting the DOM is pricey). Nonetheless, figuring out to unravel this sort of train with plain JavaScript will enable you to get a strong grasp on its fundamentals and make you a greater JavaScript developer.
Earlier than closing, let me suggest two concepts for extending this train:
- Use the HTML Drag and Drop API or a JavaScript library like Sortable.js for reordering the duties.
- Retailer information (duties) within the cloud as a substitute of the browser. For instance, exchange native storage with a real-time database like Firebase.
As all the time, thanks quite a bit for studying!
Extra Vanilla JavaScript Apps
If you wish to need to study constructing small apps with plain JavaScript, try the next tutorials: