TDD + ES6 + AngularJs (Part I of II)

AngularJs

Recently I have had some time to play with ECMAScript 6 (ES6) a.k.a ECMAScript 2015 and I've developed a small todo-app using AngularJs 1.x and the next generation of the JavaScript language specification.

TODO App

ECMAScript 6 adds really useful features to the language like:

  • Classes, a proper inheritance system, and in general, a more OO approach
  • Arrow functions
  • String interpolation
  • Destructuring
  • Default parameter values, spread operators
  • let & const hoisting
  • Module system
  • Native promises
  • Tail call optimizations
  • And much more...

For more details about what this new language version brings to the table, see Features of ES6.

Also since I am a big fan of TDD & BDD, I have applied it to the development of this small todo-app, and I have managed to have the tests code working with ES6 and a coverage test plugin integrated nicely with all of it.

It is well known that the amount of tools available on the JavaScript landscape can be overwhelming, particularly for non "frontend developers" like me, so here you can find the main set of tools that I used to develop this sample application:

  • Gulp as the default task runner.
  • Babel as the JavaScript compiler to transpile ES6 code to ES5 standard.
  • NPM to manage all the necessary development tools.
  • Bower to manage frontend dependencies.
  • ESLint to ensure code follows a good set of established coding standards.
  • Karma as the default test runner.
  • Jasmine as the default unit/integration testing framework.
  • Chai and Sinon to have more advanced assertion and mocking capabilities.
  • Protractor for writing end to end browser tests.

This is how a controller in ES6 looks like

const TITLE = 'todos';
const HELP_TEXT = 'What needs to be done?';

class TodoController {
  /* @ngInject */
  constructor(todoService, log) {
    this.todoService = todoService;
    this.log = log;
    this.title = TITLE;
    this.helpText = HELP_TEXT;
    this.newTodo = {};
  }

  loadTodos() {
    return this.todoService.loadTodos().catch(() => this.log.error('Error loading todos'));
  }

  add() {
    return this.todoService.add(this.newTodo).then(() => this._resetNewTodo());
  }

  toggleCompleted(todo) {
    return this.todoService.toggleCompleted(todo);
  }

  delete(todo) {
    return this.todoService.delete(todo);
  }

  get todos() {
    return this.todoService.todos;
  }

  _resetNewTodo() {
    this.newTodo = {};
  }
}

Important things to notice about the controller:

  1. The use of ES6 classes, constructor, methods, getters and arrow functions from the new standard.

  2. The use of @ngInject to auto inject dependencies in an angular component, if you are wondering how this magic happens ng-annotate is the dependency that does this trick. No more $inject arrays and ugly syntax to achieve the same goal.

  3. Next, I have some dependencies like todoService, a log collaborator and properties that define the model for the view (title, helpText and newTodo). Note that everything related to the domain model, lives outside the TodoController (in the todoService), this makes the controller to act more like a coordinator of actions that happen in the view and also brings some advantages in regards to reusability, if you are interested in learning more about this matter I encourage you to read this blog post in which you can pick up some ideas Rethinking AngularJS Controllers.

  4. Where is the $scope property? Well, there is no $scope property, the old fashioned angular $scope property has been replaced with the this property of the controller, AngularJs 2.x is going to kill the $scope property among many other things, so it is better to use the recommended guidelines to make the transition from AngularJs 1.x to AngularJs 2.x smoother and avoid the use of $scope, start creating Web components, etc. More on this topic here Preparing for the future of AngularJS.

Now lets take a look at the directive

class TodoDirective {
  constructor() {
    return {
      restrict: 'E',
      scope: {},
      templateUrl: 'app/components/todo.html',
      controller: TodoController,
      controllerAs: 'todoController',
      bindToController: true
    };
  }
}

As you can see I am using the recommendations to design components from the AngularJs team like using a new isolated scope, and the controllerAs and bindToController conventions.

Now let's take a look at the TodoService

import _ from 'lodash';
import Todo from '~/src/app/model/todo';

const TODO = 'todo';

class TodoService {
  /* @ngInject */
  constructor(restService) {
    this.restService = restService;
    this.todos = [];
  }

  loadTodos() {
    return this.restService.list(TODO).then((todos) => this._setTodos(todos));
  }

  add(newTodo) {
    const todo = new Todo(newTodo);
    this.todos = _.union(this.todos, [todo]);
    return this.restService.add(TODO, todo);
  }

  toggleCompleted(todo) {
    const updatedTodo = todo.toggleCompleted();
    this._update(todo, updatedTodo);
    return this.restService.update(TODO, updatedTodo);
  }

  delete(todo) {
    this.todos = _.without(this.todos, todo);
    return this.restService.delete(TODO, todo);
  }

  _update(todo, newTodo) {
    const index = _.findIndex(this.todos, todo);
    this.todos = _.insert(this.todos, newTodo, index);
  }

  _setTodos(todos) {
    this.todos = _.map(todos, (todo) => new Todo(todo));
  }
}

export default TodoService;

Here I am using the new ES6 module system in combination with the babel-root-import gulp plugin to import lodash and the Todo domain model class. You might be wondering why I am importing lodash and not injecting it as a dependency of the service. For me lodash is more like an extension of the language and you don't mock lodash in your unit tests, it should have a different treatment in comparison to a dependency. The same could apply to $q when you work with promises, but this is my personal point of view so follow the approach that makes more sense to you. Another advantage of importing the Todo domain model and not treat it as a dependency is that the domain model is less coupled and not polluted by references to the framework as you will see later.

The responsibility of this service consists in updating the state of the system and in order to do that, it needs to coordinate the calls to the backend to reflect those changes in the server side, this could have been done raising events and splitting the responsibility of updating the state of the systems in two components, one that updates the state on the client side and another one that reacting to events, updates the state on the server side, this alternative might be better in terms of following a Single Responsibility Principle, but since this is a simple example, I am making some concessions.

And finally the Todo domain model

class Todo {
  constructor(args) {
    this.id = args.id;
    this.content = args.content;
    this.completed = args.completed;
  }

  toggleCompleted() {
    return new Todo({
      id: this.id,
      content: this.content,
      completed: !this.completed
    });
  }
}

export default Todo;

The model holds data and behaviour, take into account that it is not polluted by anything related to the underlying framework, and in addition it is following a more functional approach to avoid mutating the state of the Todo instance by returning a new Todo that is either completed or not when a call to the toggleCompleted method is done.

To avoid AngularJs changing the state of the model directly (due to the two way data binding) I had to make these changes on the view

 <input class="toggle" type="checkbox" ng-model="todo.completed" 
   ng-change="todoController.toggleCompleted(todo)" />
 <input class="toggle" type="checkbox" ng-checked="todo.completed" 
   ng-click="todoController.toggleCompleted(todo)" />

Using ng-checked instead of ng-model and ng-click instead of ng-change.

You can find the code of the full app in these github repositories todo-app-frontend, todo-app-backend and follow me on twitter @doktor500