Learn how to build a Todo List in JavaScript following Test Driven Development TDD!
npm install javascript-todo-list-tutorialA _step-by-step_ tutorial showing you how to
build a Todo List App _from scratch_ in JavaScript.






alt="Try the Demo on Heroku!">
alt="Step one: learn JavaScript!">
> Before you continue, try the demo: https://todomvc-app.herokuapp.com
> Add a few items to the list. Double-click/tap the item to edit it.
Check-off your todos and navigate the footer to filter for Active/Completed.
Try and "break" it! Refresh the page and notice how your todo items
are "still there" (_they were saved to localStorage!_).
Once you have had a "play" with the demo, come back and _build_ it!!
The _purpose_ of this Todo List _mini_ project
is to _practice_ your "VanillaJS" skills and
_consolidate_ your understanding of The Elm Architecture (TEA)
by creating a "real world" _useable_ App following _strict_
Documentation and Test Driven Development.
This will _show_ you that it's not only _possible_
to write docs and tests _first_,
you will see _first hand_ that code is more concise,
well-documented and thus _easier_ to maintain
and you will get your "work" done much faster.
These are _foundational_ skills that will
pay _immediate_ returns on the time invested,
and will continue to return "interest"
for as long as you write (_and people use your_) software!
> _It's impossible to "over-state" how vital writing tests first
is to both your personal effectiveness and long-term sanity.
Thankfully, by the end of this chapter, you will see how easy it is._
Build a fully functional "Todo List" Application!
Along the way we will cover:
+ [x] Building an App using a pre-made CSS Styles/Framework!
+ [x] The Document Object Model (DOM) + JSDOM
+ [x] Browser Routing/Navigation
+ [x] Local Storage for Offline Support
+ [x] Keyboard event listeners for rapid todo list creation and editing!
We will be abstracting all "architecture" related ("generic") code
into a "mini frontend framework" called "elmish".
(_elmish is inspired by Elm but only meant for educational purposes!_)
The journey to creating elmish is captured inelmish.md
and fully documented code is in elmish.js.
This means our Todo List App can be as concise
and "declarative" as possible.
If you are _unfamiliar_ with Todo lists, simply put:
they are a way of keeping a list of the tasks that need to be done.
see: https://en.wikipedia.org/wiki/Time_management#Setting_priorities_and_goals
Todo Lists or "Checklists" are the _best_ way of tracking tasks.
Atul Gawande wrote a _superb_ book on this subject:
https://www.amazon.com/Checklist-Manifesto-How-Things-Right/dp/0312430000
Or if you don't have time to read,
watch: https://www.youtube.com/results?search_query=checklist+manifesto
If you have not come across TodoMVC before,
it's a website that showcases various "frontend" frameworks
using a common user interface (UI): a Todo List Application.
!TodoMVC-intro
We _highly recommend_ checking out the following links:
+ Website: http://todomvc.com
+ GitHub project: https://github.com/tastejs/todomvc
For our purposes we will simply be re-using the TodoMVC CSS
to make our TEA Todo List _look_ good
(_not have to "worry" about styles so we can focus on functionality_).
All the JavaScript code will be written "_from scratch_"
to ensure that everything is clear.
This tutorial is for anyone/everyone who wants
to develop their "core" JavaScript skills (_without using a framework/library_)
while building a "real world" (_fully functional_) Todo List Application.
> As always, if you get "stuck", _please_ open an issue:
https://github.com/dwyl/javascript-todo-list-tutorial/issues
by opening a question you help _everyone_ learn more effectively!
Most beginners with basic JavaScript and HTML knowledge
should be able to follow this example without any prior experience.
The code is commented and the most "complex" function is an event listener.
With that said, if you feel "stuck" at any point,
please consult the recommend reading (_and Google_)
and if you cannot find an answer,
please open an issue!
+ Test Driven Developement: https://github.com/dwyl/learn-tdd
+ Tape-specific syntax: https://github.com/dwyl/learn-tape
+ Elm Architecture: https://github.com/dwyl/learn-elm-architecture-in-javascript
Start by cloning this repository to your localhost
so that you can follow the example/tutorial offline:
``sh`
git clone https://github.com/dwyl/javascript-todo-list-tutorial.git
Install the devDependencies so you can run the tests:`sh`
cd javascript-todo-list-tutorial && npm install
Now you have _everything_ you need to build a Todo List from scratch!
In order to _simplify_ the code for our Todo List App,
we _abstracted_ much of the "_generic_" code
into a "front-end micro framework" called Elm(_ish_).Elm
The functions & functionality of (_ish_) should be _familiar_ to youElm
so you _should_ be able to build the Todo List using the (_ish_)mount
helper functions e.g: , div, input and route.
You can _opt_ to _either_:
a) read the Elm(_ish_) docs/tutorialelmish.mdbefore building the Todo List App -
this will give you both TDD practice
and a deeper understanding of building a micro framework.
i.e. "_prospective_ learning"
b) refer the Elm(_ish_) docs/tutorialelmish.mdwhile building the Todo List App when you "_need_ to know"
how one of the helper functions works. i.e. "_contextual_ learning"
c) only _consult_ the Elm(_ish_) docs/tutorialelmish.mdif you are "stuck" while building the Todo List App.
i.e. "_debug_ learning"
The choice is yours; there is no "_right_" way to learn.
_Before_ diving into _building_ the Todo List App,
we need to consider how we are going to _test_ it.
By ensuring that we follow TDD from the _start_ of an App,
we will have "no surprises"
and _avoid_ having to "correct" any
"bad habits".
We will be using Tape and JSDOM for testing
both our functions and the final application.
If you are new to either of these tools,
please see:
github.com/dwyl/learn-tape
and
front-end-with-tape.md
We will be using JSDOC for documentation.
Please see our tutorial if this is new to you.
#### Create Files
In your editor/terminal create the following files:
+ test/todo-app.test.jslib/todo-app.js
+ index.html
+
These file names should be self-explanatory, but if unclear,
todo-app.test.js is where we will write the tests for ourtodo-app.js
Todo List App. is where all the JSDOCs and functions
for our Todo List App will be written.
#### Test Setup
In order to run our test(s), we need some "setup" code
that "requires" the libraries/files so we can _execute_ the functions.
In the test/todo-app.test.js file, type the following code:`js`
const test = require('tape'); // https://github.com/dwyl/learn-tape
const fs = require('fs'); // to read html files (see below)
const path = require('path'); // so we can open files cross-platform
const html = fs.readFileSync(path.resolve(__dirname, '../index.html'));
require('jsdom-global')(html); // https://github.com/rstacruz/jsdom-global
const app = require('../lib/todo-app.js'); // functions to test
const id = 'test-app'; // all tests use 'test-app' as root element
> Most of this code should be _familiar_ to you
if you have followed previous tutorials.
> If anything is _unclear_ please revisit
https://github.com/dwyl/learn-tape
and
front-end-with-tape.md
If you attempt to run the test file: node test/todo-app.test.js
you should see no output.
(_this is expected as we haven't written any tests yet!_)
The model for our Todo List App is _boringly_ simple.Object
All we need is an with atodos key which has an Array of Objects as it's value:
`js`
{
todos: [
{ id: 1, title: "Learn Elm Architecture", done: true },
{ id: 2, title: "Build Todo List App", done: false },
{ id: 3, title: "Win the Internet!", done: false }
]
}todos is an Array of Objects and each Todo (Array) itemid
has 3 keys:
+ : the index in the list.title
+ : the title/description of the todo item.done
+ : a boolean indicating if the item is complete or still "todo".
#### What about the count of items ?
> The TodoMVC Specification requires us to display a counter
of the items in the Todo list:
https://github.com/tastejs/todomvc/blob/master/app-spec.md#counter
In order to display the count of items in the Todo list,
we _could_ store 3 values in the model:
+ total_items - the total number of items, in this case 3.completed_items
+ - the number of completed items. in this case 1.incomplete_items
+ - the number of items still to be done; 2.
Each time a new item is added to the listtotal_items
we would need to update
both the incomplete_items
and the model
values in the .item
And each time an gets checked off as "done",incomplete_items
we would need to update _both_ the completed_items
and the .todos
This is _unnecessary_ effort we can avoid.
We can simply _compute_ these values based on the data in the Array
and display them for the user without storing any additional data.
Instead of _storing_ any additional data for a counter in the modelmodel
(_the count of active and completed Todo items_),
we will _compute_ the count and display the count at "runtime".
We don't _need_ to store any additional data in the .count
This may use a few CPU cycles computing the
each time the view is rendered but that's "OK"!
Even on an _ancient_ Android device
this will only take a millisecond to compute and
won't "slow down" the app or affect UX.
See below for how the three counts are computed.
e.g: in the model above there are 3 todo items in the todos Array;done=false
2 items which are "active" ()done=true
and 1 which is "done" ().
#### model _Test_
Given that the model is "just data"Elm
(
_it has no "methods" because (ish) is_
"Functional"
_not_
"Object Oriented"
),
there is no _functionality_ to test.
We are merely going to test for the "shape" of the data.
In the test/todo-app.test.js file, append following test code:
`jsmodel
test('todo (Object) has desired keys', function (t) {todos
const keys = Object.keys(app.model);
t.deepEqual(keys, ['todos', 'hash'], " and hash keys are present.");`
t.true(Array.isArray(app.model.todos), "model.todos is an Array")
t.end();
});
If you _run_ this test in your terminal:
`sh`
node test/todo-app.test.js
You should see _both_ assertions _fail_:
!model-tests-failing
#### model _Implementation_
Write the _minimum_ code required to _pass_ this test in todo-app.js.
e.g:
`js
/**
* initial_model is a simple JavaScript Object with two keys and no methods.
* it is used both as the "initial" model when mounting the Todo List App
* and as the "reset" state when all todos are deleted at once.
*/
var initial_model = {
todos: [], // empty array which we will fill shortly
hash: "#/" // the hash in the url (for routing)
}
/ module.exports is needed to run the functions using Node.js for testing! /
/ istanbul ignore next /
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
model: initial_model
}
}
`
Once you save the todo-app.js file and re-run the tests.`sh`
node test/todo-app.test.js
You _should_ expect to see both assertions _passing_:
!model-tests-passing
We're off to a _great_ start! Let's tackle some actual _functionality_ next!
The update function is the
"brain"
of the App.
#### update JSDOC
The JSDOC for our update function is:`jsupdate
/**
* transforms the model based on the action.`
* @param {String} action - the desired action to perform on the model.
* @param {Object} model - the App's data ("state").
* @return {Object} new_model - the transformed model.
*/
#### update Test > default case
As with the update in our counter exampleswitch
the function body is a statementaction
that "decides" how to handle a request based on the
(_also known as the "message"_).
Given that we _know_ that our update function "skeleton"switch
will be a statementdefault case
(_because that is the "TEA" pattern_)
a good test to _start_ with is the .
Append the following test code in test/todo-app.test.js:
`jsupdate
test('todo default case should return model unmodified', function (t) {`
const model = JSON.parse(JSON.stringify(app.model));
const unmodified_model = app.update('UNKNOWN_ACTION', model);
t.deepEqual(model, unmodified_model, "model returned unmodified");
t.end();
});
If you _run_ this test in your terminal:
`sh`
node test/todo-app.test.js
You should see the assertion _fail_:
!update-default-branch-test-failing
#### update Function Implementation > default case
Write the _minimum_ code necessary to pass the test.
> Yes, we could just write:
`js`
function update (action, model) { return model; }
And that _would_ make the test _pass_.
But, in light of the fact that we know the updateswitch
function body will contain a statement,model
make the test pass by returning the _unmodified_ in the default case.
e.g:
`jsupdate
/**
* transforms the model based on the action.`
* @param {String} action - the desired action to perform on the model.
* @param {Object} model - the App's (current) model (or "state").
* @return {Object} new_model - the transformed model.
*/
function update(action, model) {
switch (action) { // action (String) determines which case
default: // if action unrecognised or undefined,
return model; // return model unmodified
} // default? https://softwareengineering.stackexchange.com/a/201786/211301
}
When you re-run the test(s) in your terminal:
`sh`
node test/todo-app.test.js
You should see this assertion pass:
!update-default-branch-test-passing
Now that we have a _passing_ test
for the default case in our update function,
we can move on to
thinking about the first (_and most fundamental_) piece
of _functionality_ in the Todo List App: Adding an item to the list.
This is both the _first_ "feature" a "user" will encounter and
_by_ far the most _used_ feature of a Todo List.
(_by definition people add more items to their list than they finish,
to finish everything we would have to_
live forever!)
#### ADD item _Acceptance Criteria_
Adding a new todo item's text should
append the todo item Object to the model.todos Array. model
Such that the is transformed (_data is added_) in the following way:
_BEFORE_:
`js`
{
todos: [],
hash: "#/"
}`
_AFTER_:js`
{
todos: [
{id: 1, "Add Todo List Item", done: false }
],
hash: "#/"
}
#### Hold On, That Doesn't Seem "_Right_" How Does Todo Item _Text_ Get Added?
While considering the "Acceptance Criteria"
for adding an item to the Todo List,
we _notice_ that our update JSDOC`
and corresponding function "signature" (_defined above_) as:jsupdate
/**
* transforms the model based on the action.`
* @param {String} action - the desired action to perform on the model.
* @param {Object} model - the App's (current) model (or "state").
* @return {Object} updated_model - the transformed model.
*/
function update(action, model) {
switch (action) { // action (String) determines which case
default: // if action unrecognised or undefined,
return model; // return model unmodified
} // default? https://softwareengineering.stackexchange.com/a/201786/211301
}title
does not have a parameter for passing in the Todo List item Text (),model
i.e. how do we add "data" to the ...?
That's "_Oh kay_"! (_don't panic_!)
If we try to think about implementation up-front,
we would _invariably_ be "over-thinking" things
and get "stuck" in the
"analysis paralysis"
of
"waterfall"
As you are _about_ to see, we can _easily_ change the function signature,
in the _next_ test _without affecting_ our exiting (_passing_) test!
As you _practice_ "DDD" & "TDD" you will begin to _appreciate_
and even _embrace_ the _mental agility_ that comes from
_not_ "over-thinking" things.
Whenever you encounter a "New Requirement"
(_or realise that you didn't fully consider the original requirements_),
you know that your _suite_ of tests has
"
got your
back
".
You can "_refactor_" a function's _implementation_ to your heart's content,
safe in the knowledge that all your _existing_ tests still pass.
i.e. the _rest_ of the app "still works" _exactly_ as expected.
We don't want to "mess with" either of the other two (_existing_) parameters,
both action and model have clearly defined purposes,update
but we _need_ a way to pass "data" into the function!
With that in mind, let's _amend_ the update JSDOC comment
and function signature to:
`jsupdate
/**
* transforms the model based on the action.`
* @param {String} action - the desired action to perform on the model.
* @param {Object} model - the App's (current) model (or "state").
* @param {String} data - data we want to "apply" to the item. e.g: item Title.
* @return {Object} updated_model - the transformed model.
*/
function update(action, model, data) {
switch (action) { // action (String) determines which case
default: // if action unrecognised or undefined,
return model; // return model unmodified
} // default? https://softwareengineering.stackexchange.com/a/201786/211301
}
Without making _any_ other changes, re-run the tests:
`sh`
node test/todo-app.test.js
_Everything_ should still pass:
!update-default-branch-test-passing
Congratulations! You just _extended_ a function (_signature_)
without affecting any _existing_ tests.
#### ADD item _Test_
Append the following test code to your test/todo-app.test.js file:
`jsADD
test(' a new todo item to model.todos Array via update', function (t) {`
const model = JSON.parse(JSON.stringify(app.model)); // initial state
t.equal(model.todos.length, 0, "initial model.todos.length is 0");
const updated_model = app.update('ADD', model, "Add Todo List Item");
const expected = { id: 1, title: "Add Todo List Item", done: false };
t.equal(updated_model.todos.length, 1, "updated_model.todos.length is 1");
t.deepEqual(updated_model.todos[0], expected, "Todo list item added.");
t.end();
});
If you _run_ this test in your terminal:
`sh`
node test/todo-app.test.js
You should see the assertion _fail_:
#### ADD item _Implementation_
With the above test as your "guide",
write the _bare minimum_ code necessary to make all assertions pass.
_Sample_ implementation:
`jsupdate
/**
* transforms the model based on the action.`
* @param {String} action - the desired action to perform on the model.
* @param {Object} model - the App's (current) model (or "state").
* @param {String} data - the data we want to "apply" to the item.
* @return {Object} updated_model - the transformed model.
*/
function update(action, model, data) {
var new_model = JSON.parse(JSON.stringify(model)) // "clone" the model
switch(action) { // and an action (String) runs a switch
case 'ADD':
new_model.todos.push({
id: model.todos.length + 1,
title: data,
done: false
});
break;
default: // if action unrecognised or undefined,
return model; // return model unmodified
} // see: https://softwareengineering.stackexchange.com/a/201786/211301
return new_model;
}case 'ADD'
the is the _relevant_ code.
> Was _your_ implementation _similar_...?
> If you were able to make it _simpler_,
please share!
Once you have the test(s) _passing_ e.g:
!todo-add-item-tests-passing
Let's move on to the _next_ functionality!
Checking off a todo item involves changing the value of the done propertyfalse
from to true. e.g:
_FROM_:
`js`
{
todos: [
{id: 1, "Toggle a todo list item", done: false }
]
}`
_TO_:js`
{
todos: [
{id: 1, "Toggle a todo list item", done: true }
]
}
Given that we have already defined our update function above,
we can dive straight into writing a _test_:
#### TOGGLE item _Test_
Append the following test code to your test/todo-app.test.js file:
`jsTOGGLE
test(' a todo item from done=false to done=true', function (t) {`
const model = JSON.parse(JSON.stringify(app.model)); // initial state
const model_with_todo = app.update('ADD', model, "Toggle a todo list item");
const item = model_with_todo.todos[0];
const model_todo_done = app.update('TOGGLE', model_with_todo, item.id);
const expected = { id: 1, title: "Toggle a todo list item", done: true };
t.deepEqual(model_todo_done.todos[0], expected, "Todo list item Toggled.");
t.end();
});`
_execute_ the test:sh`
node test/todo-app.test.js
You should see something _similar_ to the following:
!toggle-todo-list-item
#### TOGGLE item _Implementation_
With the above test as your "guide",
write the _minimum_ code necessary to make the test pass.
(_ensure that you continue to make a "copy" of the model
rather than "mutate" it_)
Once you make it _pass_ you should see:
> Try to make the test pass alone (or with your pairing partner).
If you get "stuck" see: todo-app.js
#### Hold On, Does This Work _Both_ Ways?
_Yes_, you _guessed_ it!
Choosing to name the action as "TOGGLE"
is _precisely_ because we don't _need_
to have a _separate_ function
to "undo" an item if it has been "checked off".
Append the following test code to your test/todo-app.test.js file:
`jsTOGGLE
test(' (undo) a todo item from done=true to done=false', function (t) {`
const model = JSON.parse(JSON.stringify(app.model)); // initial state
const model_with_todo = app.update('ADD', model, "Toggle a todo list item");
const item = model_with_todo.todos[0];
const model_todo_done = app.update('TOGGLE', model_with_todo, item.id);
const expected = { id: 1, title: "Toggle a todo list item", done: true };
t.deepEqual(model_todo_done.todos[0], expected, "Toggled done=false >> true");
// add another item before "undoing" the original one:
const model_second_item = app.update('ADD', model_todo_done, "Another todo");
t.equal(model_second_item.todos.length, 2, "there are TWO todo items");
// Toggle the original item such that: done=true >> done=false
const model_todo_undone = app.update('TOGGLE', model_second_item, item.id);
const undone = { id: 1, title: "Toggle a todo list item", done: false };
t.deepEqual(model_todo_undone.todos[0],undone, "Todo item Toggled > undone!");
t.end();
});
You should not _need_ to modify any of the code in the update function.TOGGLE case
The above test should just _pass_ based on the code you wrote above.
If it does _not_, then _revise_ your implementation
of the in update until _all_ tests pass:
It won't have "_escaped_" you that _so far_ we have not written _any_ code
that a _user_ can actually _interact_ with.
_So far_ we have _successfully_ added two case blocks in the switch statementupdate
of our function. We now have the two _basic_ functions requiredADD
to both a new todo list item to the model.todos ArrayTOGGLE action
_and_ check-off a todo list item as "done" using the .
This is "_enough_" functionality to start _using_ the todo list (_ourselves_)
and UX-testing it with _prospective_ "users".
If you followed through the "Elm(ish)" tutorial
elmish.md
you will have seen that we created a _sample_ view in the last few _tests_view
to "_exercise_" the DOM element creation functions.
This means that we _already know_ how to build a for our Todo List App!view
We "_just_" need to _adapt_ the we made in Elm(_ish_) to displaymodel
the data in our .
#### Sample model to Render in Our view
Let's return to the sample model from above:
`js`
{
todos: [
{ id: 1, title: "Learn Elm Architecture", done: true },
{ id: 2, title: "Build Todo List App", done: false },
{ id: 3, title: "Win the Internet!", done: false }
],
hash: '#/' // the "route" to display
}todos
The model contains _three_ items in the Array. done=true
The first is complete ()done=false
whereas the second and third items are still "todo" ().
This is what this model looks like in the "VanillaJS"
TodoMVC:
Our _quest_ in the next "pomodoro" is to re-create this
using the DOM functions we created in Elm(_ish_)!
#### Focus on Rendering The _List_ First
For now, _ignore_ the
In your web browser, open Developer Tools
and _inspect_ the HTML for the Todo list:
http://todomvc.com/examples/vanillajs/
!todomvc-main-section-todo-list-html
This is the HTML copied directly from the browser:
`html`
> _Note: there is "redundant" markup in this HTML in the form of a
inside the , for now we are just replicating the HTML "faithfully",
we can "prune" it later._From this HTMl we can write our
"_Technical_ Acceptance Criteria":
+ [ ] Todo List items should be displayed as list items
in an _unordered list_ .
+ [ ] Each Todo List item should contain a
with a class="view" which "wraps":
+ [ ] - the "checkbox"
that people can "Toggle" to change the "state"
of the Todo item from "active" to "done"
(_which updates the model
From: model.todos[id].done=false
To: model.todos[id].done=true_)
+ [ ]