A TODO Application

We will be writing a sample TODO app which helps our users to remember important tasks. During the process of development we will go through various concepts of AngularJS and how it can be used with RequireJS for dependency management and lazy loading of javascript files.

To keep our code in best possible quality, we will be relying on unit cases (and plenty of it) using Mocha and Chai which will run on Karma

Iteration 1 - Setup:

To start with any angular js application we need to create a HTML page with all the css and javascript depenendencies defined. We will include below dependencies on our HTML

Our end result should look like below:

  1. <html>
  2.   <head>
  3.     <meta charset="utf-8">
  4.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1">
  6.     <meta name="format-detection" content="telephone=no">
  7.     <!-- bootstrap css for layout -->
  8.     <link href="styles/bootstrap.min.css" rel="stylesheet">
  9.   </head>
  10.   <body>
  11.     <div class="container" ui-view></div>
  12.     <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.2/angular.min.js"></script>
  13.     <script data-main="scripts/main" src="scripts/require.js"></script>
  14.   </body>
  15. </html>

You would have noticed that we have added ui-view attribute to our only div tag in HTML. This is done for angular JS to load TODO app inside this div. It will be achieved with help of ui-router

Also as part of require JS inclusion we have added an attribute data-main which refer to a script file main.js location under scripts folder. This file will help us initialize angular js application and load other dependencies. Let's have a look at this file

  1. (function() {
  2.   "use strict";
  3.   define(["todo/initialize"], function() {
  4.     angular.bootstrap(document, ["todo"]);
  5.   });
  6. }());

We have done two tasks in this file

  • 1.  Initialized angular JS application using angular.bootstrap method for whole document.
  • 2.  Defined a new dependency for our application initialize.js which resides under todo folder
define comes from require js world. It is used to load javascript file asynchronously before any code can start execution. As you can notice, define accepts two argument where first is an array and second is a callback function. For more details refer to require js documentation

Now lets have a look at initialize.js file

  1. (function() {
  2.   "use strict";
  3.   define([], function() {
  4.     window.module = angular.module("todo", []);
  5.   });
  6. }());

At this point our initial Angular JS setup is done and now we can run our application though there is nothing to see in UI. Go ahead and run the application. Make sure there are no errors in browser console.

Iteration 2 - UI Router:

Now lets begin with interesting part where we will finally start to see something in UI. To begin we need to download angular-ui-router from here. Once downloaded save this file under scripts folder.

For starters we will add two routes

  • 1.  Todo List
  • 2.  Add New Todo Item
We need to do below tasks to ensure routing is properly configured for our application:
  • 1.  Add UI-Router as dependency for initialize.js
  • 2.  Write a method to create JSON object as required by UI Router to define state
  • 3.  Add one state each for every route using $stateProvider
After these changes initialize.js will look something like this

  1. (function() {
  2.   "use strict";
  3.   define(["angular-ui-router"], function() {
  4.     var routes = {
  5.       "/app/add": "parent",
  6.       "/app/list": "parent"
  7.     };
  8.     var paths = {
  9.       "/app/add": {
  10.         ctrlName: "addPageCtrl",
  11.         view: "scripts/todo/app/addPage.html",
  12.         ctrl: "scripts/todo/app/addPageCtrl.js"
  13.       },
  14.       "/app/list": {
  15.         ctrlName: "listPageCtrl",
  16.         view: "scripts/todo/app/listPage.html",
  17.         ctrl: "scripts/todo/app/listPageCtrl.js"
  18.       }
  19.     };
  20.     /**
  21.      * function used to create a JSON object for route configuration
  22.      * @param hash String
  23.      * @return JSON
  24.      */
  25.     var resolve = function (hash) {
  26.       var routeDef = {};
  27.       routeDef.url = hash;
  28.       routeDef.controller = paths[hash].ctrlName;
  29.       routeDef.templateUrl = paths[hash].view;
  30.       routeDef.resolve = {
  31.         loadCtrl: ["$q", function($q) {
  32.           var defer = $q.defer();
  33.           require([paths[hash].ctrl], function() {
  34.             defer.resolve();
  35.           });
  36.         }
  37.       };
  38.       return routeDef;
  39.     };
  40.     window.module = angular.module("todo", []);
  41.     module.config(["$provide", "$controllerProvider", "$stateProvider", "$urlRouterProvider",
  42.       function($provide, $controllerProvider, $stateProvider, $urlRouterProvider) {
  43.         module.register = {
  44.           service: $provide.service,
  45.           controller: $controllerProvider.register
  46.         };
  47.         for (var route in routes) {
  48.           if (routes.hasOwnProperty(route)) {
  49.             var routeObject = null;
  50.             if (routes[route] === "parent") {
  51.               routeObject = resolve(route);
  52.             } else {
  53.               routeObject = routes[route];
  54.             }
  55.             $stateProvider.state(route, routeObject);
  56.           }
  57.         }
  58.         $urlRouterProvider.otherwise("/app/list");
  59.       }
  60.     ]);
  61.   });
  62. }());

If you run application at its current state you will most probably get an error about missing file listPageCtrl.js. To resolve this we will create two dummy files "listPage.html" and listPageCtrl.js under path scripts/todo/app. It doesn't need to contain anything. For now we will create just an empty html file with Hello text. Also we need to create a listPageCtrl which will be empty too. Refer to below code

  1. (function() {
  2.   "use strict";
  3.   define([], function() {
  4.     var listPageCtrl = function($scope) {
  5.     };
  6.     if (typeof module !== "undefined" && module.register != null && typeof module.register.controller === "function") {
  7.       module.register.controller("listPageCtrl", ["$scope", listPageCtrl]);
  8.     }
  9.     // for sake of AMD
  10.     return listPageCtrl;
  11.   });
  12. }());

If we run the application now, we will see a blank page with Hello message on top of screen. Go ahead and give it a try.

Iteration 3 - Karma:

Now comes the most important part. Unit test cases are becoming a very important part of application development and writing good test cases during development phase helps in improving design and quality of application. There are many frameworks available to write unit test cases for Javascript code (Jasmine being one of the well known for Angular). For our TODO app we will be using Mocha along with Chai.

For karma setup we need to follow below tasks:

  • 1.  Open a command prompt at root folder
  • 2.  Run command npm install karma
  • 3.  Run command karma init my.conf.js
  • 4.  It will ask for testing framework to use. Select Mocha there
  • 5.  Next question will be about use of Require JS. Select Yes
  • 6.  It will ask for browsers. We can choose multiple browsers but Chrome will work for now
  • 7.  For next question specify scripts/*.js and then press enter. Add one more entry scripts/**/*.js and press enter to move to next question
  • 8.  Select enter to skip the next question
  • 9.  For question asking for generation of test-main.js. Select Yes
  • 10.  Select Yes for question asking to watch all the files. This will help us get continous feedback on change of code in any file.

At end of above steps, we will have two files created at root folder namely my.conf.js and test-main.js For more info related to configuration refer to this link. Now you might have noticed that there is one more folder that is created with name node_modules. This folder basically holds all the node dependencies required to run Karma but what if we delete this folder? From where we will get back these dependencies? Don't worrry we have a solution for that :)

Create a file name package.json in same folder where node_modules exist. For now we will create a empty file as below

  1. {
  2.   "name": "todo",
  3.   "description": "this is package json file to define node dependencies",
  4.   "license": "MIT"
  5. }

Now in the same command prompt we need to run below commands one by one

  1. npm install karma --save-dev
  2. npm install requirejs --save-dev
  3. npm install karma-mocha --save-dev
  4. npm install karma-chrome-launcher --save-dev
  5. npm install karma-requirejs --save-dev
  6. npm install angular --save-dev
  7. npm install angular-mocks --save-dev
  8. npm install karma-chai --save-dev

At end of these commands our package.json file will be updated to look like below. For more info on package.json file refer here

  1. {
  2.   "name": "todo",
  3.   "description": "this is package json file to define node dependencies",
  4.   "license": "MIT"
  5.   "devDependencies": {
  6.     "karma": "^0.13.2",
  7.     "requirejs": "^2.1.19",
  8.     "mocha": "^2.2.5",
  9.     "karma-mocha": "^0.2.0",
  10.     "karma-chrome-launcher": "^0.2.0",
  11.     "karma-requirejs": "^0.2.2",
  12.     "angular-mocks": "^1.4.3",
  13.     "angular": "^1.4.3",
  14.     "chai": "^3.2.0",
  15.     "karma-chai": "^0.1.0"
  16.   }
  17. }

Now its time to test setup. Run karma start my.conf.js in command prompt. This should open a chrome browser instance with URL http://localhost:9876/. Also in our command prompt we can see message "Executed 0 of 0" which is fine since we haven't written a single test case till now.

Before we start writing any test case we need to do one final configuration in my.conf.js and test-main.js which will ensure our test files are loaded and executed. Open my.conf.js and under framework array add chai as an additional framework as shown below

  1. frameworks: ['mocha', 'requirejs', 'chai']

Also under files array add two more entries to load all files for angular and angular-mocks. These files are downloaded using npm command and will be present under node_modules folder. End result will look as below:

  1. files: [
  2.   'test-main.js',
  3.   {pattern: 'scripts/*.js', included: false},
  4.   {pattern: 'scripts/**/*.js', included: false},
  5.   {pattern: 'node_modules/angular/*.js', included: false},
  6.   {pattern: 'node_modules/angular-mocks/*.js', included: false}
  7. ]

Since we are using RequireJs to load all the dependencies, we need to modify test-main.js which was generated for us earlier. We are doing two tasks in this file

  • 1.  Iterating over all the files and finding which are ending with either "test" or "spec"
  • 2.  Declaring default requireJs configuration

In require js configuration we setting baseUrl as /base which is a virtual folder created by Karma to host all the files under test. We need to change this to /base/scripts since all our source code is inside scripts folder.

  1. baseUrl: '/base/scripts'

Also if we notice then we will find that in the loop above we are replace /base from string before adding to allTestFiles array. This code also need to be modified and should be as below

  1. var normalizedTestModule = file.replace(/^\/base\/scripts\/|\.js$/g, '');
  2. allTestFiles.push(normalizedTestModule);

All test cases that we will write from now on will need angular-mocks as dependency (to know why click here). Angular Mocks internally needs angularjs to be loaded as dependency. Also we will need to load main.js so that our angular application is bootstraped and todo module is created.

Let's change the require js configuartion to achieve this. We need to add paths and shim json object to configure this. Final result will look as below:

  1. require.config({
  2.   baseUrl: '/base/scripts',
  3.   deps: allTestFiles,
  4.   paths: {
  5.     "main": "main",
  6.     "angular": "../node_modules/angular/angular.min",
  7.     "angular-mocks": "../node_modules/angular-mocks/angular-mocks"
  8.   },
  9.   shim: {
  10.     "main": {exports: "main", deps: ["angular"]},
  11.     "angular-mocks": {exports: "angular-mocks", deps: ["main"]}
  12.   },
  13.   callback: window.__karma__.start
  14. });

Iteration 4 - TDD:

To begin we will create a folder named test under scripts. This folder will hold all our unit test cases.

We will create a new file name listPageCtrlSpec.js at this location. We need to include todo/app/listPageCtrl as dependency since we want to test that controller and also we will include angular-mocks. It will look as below:

  1. (function() {
  2.   "use strict";
  3.   define(["todo/app/listPageCtrl", "angular-mocks"], function(listPageCtrl) {
  4.     describe("test suite for list page controller", function() {
  5.       var scope = null;
  6.       var controller = null;
  7.       beforeEach(module("todo"));
  8.       beforeEach(inject(function($rootScope, $controller) {
  9.         scope = $rootScope.$new();
  10.         controller = $controller;
  11.       }));
  12.     });
  13.   });
  14. }());

We have used beforeEach statement in the above code to perform two tasks

  • 1.  Mock our "todo" module
  • 2.  Inject $rootScope and $controller method to initialize controller

Now let's write our first test case. For our list page we need to fetch items from local storage and display it in UI as list view. Let's write a unit test case to read items from local storage. Test case will look like below:

  1. it ("should read items from local storage", function() {
  2.   // set dummy data
  3.   localStorage.setItem("todos", JSON.stringify([{task: "clean room", time: "19:00"}, {task: "sleep", time: "22:00"}]));
  4.   controller(listPageCtrl, {$scope: scope });
  5.   expect(scope.todos.length).to.equal(2);
  6.   expect(scope.todos[1].task).to.equal("sleep");
  7.   // clear data
  8.   localStorage.removeItem("todos");
  9. });

If we run test case now, it will give error "TypeError: Cannot read property 'length' of undefined" which is fine since we haven't implemented anything in our controller. Now lets go and implement a method in our controller to create this list.

In our previously created empty controller we will now create a method init and will write code to read the JSON array from local storage. Final result will look like below:

  1. (function() {
  2.   "use strict";
  3.   define([], function() {
  4.     var listPageCtrl = function($scope) {
  5.       var init = function() {
  6.         var items = localStorage.getItem("todos");
  7.         if (items != null) {
  8.           try {
  9.             $scope.todos = JSON.parse(items);
  10.           } catch (e) {
  11.             $scope.todos = [];
  12.           }
  13.         }
  14.       }
  15.       init();
  16.     };
  17.     if (typeof module !== "undefined" && module.register != null && typeof module.register.controller === "function") {
  18.       module.register.controller("listPageCtrl", ["$scope", listPageCtrl]);
  19.     }
  20.     // for sake of AMD
  21.     return listPageCtrl;
  22.   });
  23. }());

Now if we run the test case then we will see "Executed 1 of 1 SUCCESS". Congratulations we have successfully executed our first test case and implemented a method to read list of todo items

We should also write some HTML code in our listPage.html to print todo items in UI. It should look as below:

  1. <nav class="navbar navbar-inverse navbar-fixed-top">
  2.   <div class="container">
  3.     <div class="navbar-header">
  4.       <a class="navbar-brand" href="javascript:void(0);">To Do</a>
  5.     </div>
  6.     <div class="collapse navbar-collapse">
  7.       <ul class="nav navbar-nav"></ul>
  8.     </div>
  9.   </div>
  10. </nav>
  11. <div style="margin-top: 70px">
  12.   <ul class="list-group" ng-if="todos != null && todos.length > 0">
  13.     <li class="list-group-item" ng-repeat="todo in todos">{{ todo.task }}</li>
  14.   </ul>
  15.   <div class="alert alert-info" role="alert" ng-if="todos == null || todos.length === 0">Hurray!!! There are no pending items in To Do list</div>
  16.   <a type="button" class="btn btn-default" style="color:white;width:100%;background-color: #000000;border-color: #000000;" href="#/app/add">Add New</a>
  17. </div>

In this HTML we are iterating over our list to print each todo task. If list is not available or there are no items in it then we are printing a message with some help from ng-if. Also there is a button in bottom of the page which will allow us to add new todo item.

Go ahead and run the application at its current state. We will notice a nice UI with a header on top and a message along with button.
When we click on the button, currently nothing will happen though we can notice that the hash tag appended to URL changes to #/app/add

Its time for us to create a new controller (addPageCtrl.js) and html (addPage.html). We will create these files at path scripts/todo/app. For now we will keep our controller as empty file with code as below:

  1. (function() {
  2.   "use strict";
  3.   define([], function() {
  4.     var addPageCtrl = function($scope) {
  5.     };
  6.     if (typeof module !== "undefined" && module.register != null && typeof module.register.controller === "function") {
  7.       module.register.controller("addPageCtrl", ["$scope", addPageCtrl]);
  8.     }
  9.     // for sake of AMD
  10.     return addPageCtrl;
  11.   });
  12. }());

As per requirement we need to ensure that when used click on a button "Add To Do" in UI then we should add a new To Do Item. Let's first write a test case for that in our new file addPageCtrlSpec.js. Final result will be as below:

  1. (function() {
  2.   "use strict";
  3.   define(["todo/app/addPageCtrl", "angular-mocks"], function(addPageCtrl) {
  4.     describe("test suite for add page controller", function() {
  5.       var scope = null;
  6.       beforeEach(module("todo"));
  7.       beforeEach(inject(function($rootScope, $controller) {
  8.         scope = $rootScope.$new();
  9.         $controller(addPageCtrl, {$scope: scope});
  10.       }));
  11.       it ("should add an item to todos list", function() {
  12.         scope.task = "sleep";
  13.         scope.time = "21:00";
  14.         localStorage.removeItem("todos");
  15.         scope.onAddClick({});
  16.         var todos = [];
  17.         var items = localStorage.getItem("todos");
  18.         if (items != null) {
  19.           try {
  20.             todos = JSON.parse(items);
  21.           } catch (e) {}
  22.         }
  23.         expect(todos.length).to.equal(1);
  24.         expect(todos[0].task).to.equal("sleep");
  25.       });
  26.     });
  27.   });
  28. }());

At this point we will have a failing test case. To fix it we need to implement onAddClick method in addPageCtrl.js Lets write the code for that. End result will be like below

  1. var addPageCtrl = function($scope) {
  2.   $scope.onAddClick = function($event) {
  3.     var todos = [];
  4.     var items = localStorage.getItem("todos");
  5.     if (items != null) {
  6.       try {
  7.         todos = JSON.parse(items);
  8.       } catch (e) {}
  9.     }
  10.     todos.push({
  11.       task: $scope.task,
  12.       time: $scope.time
  13.     });
  14.     localStorage.setItem("todos", JSON.stringify(todos));
  15.   }
  16. };

With this all our test case should pass. We now need to write HTML for addPage.html to render UI. Final output will look like below. In this HTML we are ng-model directive to bind the value of HTML element with request scope variable. For e.g. ng-model="time" will bind the value of text box with $scope.time variable. We are also using ng-click directive which is used to bind the click event on any DOM element. In the below case when user clicks on "Add To Do" button, onAddClick method as defined in $scope will be triggered.

  1. <nav class="navbar navbar-inverse navbar-fixed-top">
  2.   <div class="container">
  3.     <div class="navbar-header">
  4.       <a class="navbar-brand" href="javascript:void(0);">To Do</a>
  5.     </div>
  6.     <div class="collapse navbar-collapse">
  7.       <ul class="nav navbar-nav"></ul>
  8.     </div>
  9.   </div>
  10. </nav>
  11. <div style="margin-top: 70px">
  12.   <div class="input-group" style="margin-bottom: 10px">
  13.     <span class="input-group-addon" id="basic-addon1">Task</span>
  14.     <input type="text" class="form-control" placeholder="Task" ng-model="task">
  15.   </divl>
  16.   <div class="input-group">
  17.     <span class="input-group-addon" id="basic-addon1">Time</span>
  18.     <input type="text" class="form-control" placeholder="Time" ng-model="time">
  19.   </divl>
  20.   <div style="margin-top: 10px">
  21.     <a type="button" class="btn btn-default" style="color:white;width:48%;background-color: #000000;border-color: #000000;margin-right:2%;" href="#/app/list">Show List</a>
  22.     <a type="button" class="btn btn-default" style="color:white;width:48%;background-color: #000000;border-color: #000000;" href="javascript:void(0);" ng-click="onAddClick($event);">Add To Do</a>
  23.   </div>
  24. </divl>

With this our application is finished. In the course of this development we learned how to create an application using angular js and manage page routing using ui-router. We also learned to manage dependencies using RequireJS and most importantly we learned to write test cases (for stability of application) which runs on Karma. Go ahead and run the application and enjoy benefits of home made To Do app. For complete source code click here

Feel free to comment below if any doubt or suggestions. Your feedback is valuable

  

Comments:

  • Sumit Sinha
    17 Mar 2016

    To start karma server -- karma start my.conf.js
  • Akash Verma
    Thanks for this wonderful information. It helped a lot :-)