In last week's blog post, I showed you how to install all of the basic tools that you need to get up and running with the MEAN Stack. Didn't catch that one and need help getting started with the MEAN Stack? You can find everything you need in Introduction to the MEAN Stack, Part One.
This time we're going to take that shiny new MEAN Stack and put it to good use by building a very simple to-do list. We'll also set up a basic automated testing suite. This process will take 12 steps and you can follow along here. I will link to the diff on github for each step below as well. The
mean-stack-todo repo started off as a direct duplicate of the
mean-stack-skeleton repo from last week.
Over the course of this tutorial, you'll see some of the key features and concepts that make the MEAN stack such a powerful development tool. Here are a few highlights to keep in mind:
1) Create some sample data for the server to display (diff)
Our to-dos are going to have a description, a due date, and a boolean flag to indicate whether or not they're already done. Our app.js file shows that when the user accesses the
GET / route, ExpressJS will call
routes/index.js "index" function, which in turn renders
views/index.jade. Let's add a couple of sample to-dos to
index.jade's template parameters.
2) Modify the index.jade template to display our sample data using ExpressJS templates (diff)
Let's create a very simple layout to display our to-dos. We'll have to separate our to-dos into two lists: one for completed to-dos, and one for outstanding to-dos. It's very important here to note that by default, ExpressJS uses Jade for templating. Jade is a controversial templating library which uses a simplified and more human-readable version of HTML syntax. For example, let's say we have the following HTML:
<ul class="span6 text-center" id="tools-list"> <li> <a href="http://www.angularjs.org"> AngularJS </a> </li> <li> <a href="http://www.expressjs.com"> ExpressJS </a> </li> </ul>
In Jade, this looks like:
ul#tools-list.span6.text-center li a(href='http://www.angularjs.org') | AngularJS li a(href='http://www.expressjs.com') | ExpressJS
3) Add Bootstrap to our project using Bower (diff)
You'll want to scroll all the way to the bottom of the diff, because this diff includes all of Bootstrap and jQuery. In addition to changes in the layout, you'll notice that we take advantage of some of Jade's inheritance features. The
index.jade template inherits from
layout.jade using the
extends layout on the first line. In this commit, we added
block head to
layout.jade, and then in
index.jade we overwrote
block head to include bootstrap in our template.
As an aside, you'll want to install bootstrap via
bower install bootstrap only gives you an un-compiled version of Bootstrap. This is pretty silly considering the fact that Twitter maintains both Bower and Bootstrap. Fate, it seems, is not without a sense of irony.
4) Use AngularJS to make our template dynamic (diff)
One big problem with our to-do list right now is that there is no way to move a to-do from the outstanding list to the completed list, or vice versa. A Jade template is compiled once and then served up as static HTML to the user. However, if we use AngularJS's two-way data-binding as a templating tool, our template updates automatically to reflect the state on the client side. Once you've implemented this, items will automatically move to the completed list when you click the checkbox on the outstanding list.
5) Install AngularJS's end-to-end test runner and write a simple test (diff)
One of AngularJS's original co-authors was Misko Hevery, my former mentor at Google whose sole mission in the professional world is to make sure every piece of software has 100% test coverage. As you might expect, AngularJS was built with ease of automated testing as a core design consideration. Angular-scenario is a part of AngularJS that essentially hijacks an iframe to perform tests on your site as the user would see it. After this commit, run
node app.js and navigate to
http://localhost:3000/tests/e2e/runner.html. You should see something like this:
This page will run the test we defined in
/public/tests/e2e/scenarios.js. As you can see, Jasmine syntax does a pretty good job of making tests read like stories. Our test says when we navigate to "/," we should see two to-dos in the outstanding list and one in the completed list. Then we click on a checkbox and expect that one of the to-dos moved to the completed list. You can install angular-scenario via bower:
bower install angular-scenario
6) Write code for a REST-ful path to let the user add a to-do and a unit test (diff)
For this step, you need to install nodeunit with
npm install -g nodeunit
You can run the test suite with
from the repo root directory. You'll notice that the route is defined as a function that returns a function. The purpose of this is two-fold. First, it lets you write a proper unit test for this function. The core tenant of writing unit tests is that if you introduce a bug to a clean code base, the only tests that should fail are the ones that test the newly broken function. If the to-do objects were not passed to the route as a parameter, the route would have a hidden dependency that we wouldn't be able to mock out, so bugs in the to-do object itself could potentially break the route's test as well. This would be especially bad if to-dos were a complex object rather than a simple list. The secondary purpose for defining routes in the manner described above is that this syntax allows you to easily see which functions depend on a given module, which is very important if you have to change how a module works.
7) Open up the REST-ful path for adding a to-do (diff)
In line with the argument in step 6, we'll move the todos array into
app.js for now and inject it into our
GET / and
POST /todo.json routes. This pattern is called dependency injection.
8) Create a basic form to add a to-do (diff)
AngularJS gives us the ability to override the default form-submission action using the
ng-submit directive, so we can perform the
POST /todo.json action without refreshing the page by using AngularJS's
9) Add angular-ui-bootstrap and use its datepicker (diff)
Fair warning, this commit is a little difficult to read because it includes all of angular-bootstrap. You can install it through bower using
bower install angular-bootstrap
One critical feature that was hardcoded into the previous commit was the due date of the new to-do. It was set to one week from now and there was no way to change it. Thankfully, the angular-bootstrap module includes, among other features, a nice datepicker directive. The hardest part is setting up an AngularJS module so that we can include angular-bootstrap. AngularJS uses modules to organize dependencies, so we need to create a TodoModule in
10) Set up a MongooseJS model (diff)
MongooseJS is a schema and validation tool for NodeJS and MongoDB. You use MongooseJS essentially the same way you would use an ORM in other development environments. Here we define a basic schema which tells MongooseJS the form that objects in the todos collection must take and define a Todo "model" which allows us to perform ORM-like operations on the todos collection. If you want to learn more about MongooseJS, I've written about it in much greater detail here and here.
11) Use the Todo model for database persistence and then fix our tests (diff)
Now that we have an object that lets us save and query a todos collection in MongoDB, we can make our
GET / route query for all to-dos and our
POST /todo.json route add a new to-do to the collection. Note that the function to save a new to-do can fail – if the posted object takes on an incorrect form (e.g. the "description" is empty), we'll return an error object. For more information about handling this error object, you can read my post How to Easily Validate Any Form Ever Using AngularJS.
12) Tie up some loose ends: persist checking and un-checking, and reload data periodically (diff)
If you've been paying close attention, you may have noticed that up until now we missed a very important feature: we never actually update the backend when we mark a to-do as done. You would also be more observant than I was when I was outlining the steps for this post, because I missed that until I got to this step. However, like most of the tasks in this post, it is pretty trivial. We open up a
PUT /todo/:id.json route that updates an existing to-do, create a function called
update in TodoListController that uses
$http to perform a PUT, and use the ng-change directive to call "update" every time the checked/unchecked status of a to-do changes.
Finally, we can add one more feature that I find tragically lacking in my personal to-do list app of choice, Todoist: periodic syncing with the server. I find it really frustrating to check off something as done on my laptop and then have to refresh the page on my Ubuntu box when I get back home. The simplest way to improve this is to add a dependency to AngularJS's
$timeout service and set it to ping the server for a full to-do list every 30 minutes.
setInterval (diff), or, better yet, your own custom AngularJS service that wraps
Hey, guess what? You've just successfully built a to-do list app! How badass is that? What's even better is that in some ways this fancy new app you built is in some ways more useful than commercial offerings like Todoist. [Note: To any Todoist team members who read this- I love many things about your software but hot damn there are some really annoying issues with it that I think could be improved]. Hope you found this little code adventure useful. By the way, I've been toying with the idea of building a more sophisticated to-do list/task management app in the near future that addresses a lot of the issues I've had working with Todoist, Wedoist, Asana, Jira, Github, Trello, Streak, etc. Would you guys be interested in something like this? Something along the lines of using Github to successfully coordinate tasks that aren't actually code-related. What sort of features would you want to see in something like this? What annoys you about existing to-do or team management tools? Feel free to discuss in the comments section.
Have any questions about the code featured in this post? Want to suggest a better approach? Go ahead and leave a comment below, or tweet me at @code_barbarian. I'm always down for both unfettered praise and horribly vicious flame wars. Until then, feel free to rant about which task management apps you like and which make you want to feel like this in the comments. You know what's better than a task management app? A guy like William Kelly (@idostartups). Major props to him as always for helping make this post happen.