Build a To-Do App Using Sails.js and AngularJS

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

AngularJS is an increasingly popular MV*/MVVM Javascript front-end framework that seamlessly integrates with the server-side MVC Node.js framework, Sails.js. Although AngularJS is well known and prevalently used, Sails.js is a more up-and-coming framework.

Sails.js is written in Node.js and utilizes Express as a web server. Additionally, Sails.js comes bundled with Waterline ORM simplifying the data layer by you only having to interchange adapters for most SQL or NoSQL databases.

One of my favorite features is the automatically generated REST API. This is very handy and allows you to create simple and well-designed APIs.

Lastly, it is compatible with many popular front-end frameworks, including AngularJS, Backbone, Ember, iOS, Android, and many more. If you’ve been deciding on which Javascript framework you want to learn – Sails.js is simple, secure, and most of all, fun!

Whether you’re a novice or veteran to AngularJS or Sails.js this blog post will illustrate how both frameworks interact with each other by building a to-do application.

To see the full source code of this project, check it out here.

Getting Started

Installing Dependencies

Before jumping into the code, we will need to install npm (which additionally installs Node.js) to utilize the necessary packages for this tutorial. With npm installed, we need to grab the Sails.js dependency by running:

$ npm install -g sails

Now let’s generate a Sails.js application by using the sails CLI, sails new todoApp. Hopefully, your directory structure looks like this:

│   Gruntfile.js
│   README.md
│   app.js
│   package.json
└─── api
    └───controllers
    └───models
    └─── policies
        │   sessionAuth.js
        │   responses
        │   badRequest.js
        │   forbidden.js
        │   notFound.js
        │   ok.js
        │   serverError.js
        │   services
    └─── assets
        │   favicon.ico
        │   images
        │   js
        │   robots.txt
        │   styles
        │   templates
    └─── images
    └─── js
        └─── dependencies
            │   sails.io.js
    └─── styles
        │   importer.less
    └─── templates
└─── config
    │   blueprints.js
    │   bootstrap.js
    │   connections.js
    │   cors.js
    │   csrf.js
    │   env
    │   globals.js
    │   http.js
    │   i18n.js
    │   local.js
    │   locales
    │   log.js
    │   models.js
    │   policies.js
    │   routes.js
    │   session.js
    │   sockets.js
    │   views.js
    └─── env
        │   development.js
        │   production.js
    └─── locales
        │   _README.md
        │   de.json
        │   en.json
        │   es.json
        │   fr.json

└─── node_modules
    └─── ejs
    └─── grunt
    └─── grunt-contrib-clean
    └─── grunt-contrib-coffee
    └─── grunt-contrib-concat
    └─── grunt-contrib-copy
    └─── grunt-contrib-cssmin
    └─── grunt-contrib-jst
    └─── grunt-contrib-less
    └─── grunt-contrib-uglify
    └─── grunt-contrib-watch
    └─── grunt-sails-linker
    └─── grunt-sync
    └─── include-all
    └─── rc
    └─── sails
    └─── sails-disk

└─── tasks
    │   README.md
    │   pipeline.js
    └─── config
        │   clean.js
        │   coffee.js
        │   concat.js
        │   copy.js
        │   cssmin.js
        │   jst.js
        │   less.js
        │   sails-linker.js
        │   sync.js
        │   uglify.js
        │   watch.js
    └─── register
        │   build.js
        │   buildProd.js
        │   compileAssets.js
        │   default.js
        │   linkAssets.js
        │   linkAssetsBuild.js
        │   linkAssetsBuildProd.js
        │   prod.js
        │   syncAssets.js
└─── views
        │   403.ejs
        │   404.ejs
        │   500.ejs
        │   homepage.ejs
        │   layout.ejs

Great! Now we can start our server with sails lift and see our landing page by visiting http://localhost:1337.

sails-new-app

Additionally, we need to update our package.json and create a bower.json file to configure the project to our needs.

package.json

Let’s update the package.json file to have the following packages. Optionally, you can edit the name, author, and many other properties of this file to fit your needs.

{
  "name": "todoApp",
  "author": "Scotch",
  "description": "Sails/Angular Todo Applcation",
  "main": "app.js",
  "dependencies": {
    "bower": "^1.4.1",
    "ejs": "~0.8.4",
    "forever": "^0.11.1",
    "grunt": "0.4.2",
    "grunt-contrib-clean": "~0.5.0",
    "grunt-contrib-coffee": "~0.10.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-copy": "~0.5.0",
    "grunt-contrib-cssmin": "~0.9.0",
    "grunt-contrib-jst": "~0.6.0",
    "grunt-contrib-less": "0.11.1",
    "grunt-contrib-uglify": "~0.4.0",
    "grunt-contrib-watch": "~0.5.3",
    "grunt-sails-linker": "~0.9.5",
    "grunt-sync": "~0.0.4",
    "include-all": "~0.1.3",
    "q": "^1.4.1",
    "rc": "~0.5.0",
    "sails": "~0.11.0",
    "sails-disk": "~0.10.0"
  }
}

To install these dependencies, run npm install. Now, we need to customize our front-end configuration. Under the assets directory, run bower init to generate the bower.json file, update it with the following packages and install them with bower install:

bower.json

{
  "name": "todoAngularApp",
  "dependencies": {
    "angular-bootstrap": "~0.11.0",
    "angular-moment": "~0.7.1",
    "angular-route": "~1.2.17",
    "angular": "1.2.19",
    "angular-mocks": "~1.2.21",
    "jquery": "~2.1.3",
    "bootstrap": "~3.3.5"
  }
}

One last thing we need to set up is in the tasks/pipeline.js. Pipeline.js tells our program where our dependencies are and which to load.

pipeline.js

var cssFilesToInject = [
'bower_components/bootswatch/dist/css/bootstrap.css',
'styles/**/*.css'
];
var jsFilesToInject = [
  'js/dependencies/sails.io.js',
  '/bower_components/jquery/dist/jquery.js',
  '/bower_components/angular/angular.js',
  '/bower_components/angular-route/angular-route.js',
  '/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
  '/bower_components/bootstrap/dist/js/boostrap.js',
  'js/dependencies/**/*.js',

  'js/**/*.js'
  ];

var templateFilesToInject = [
'templates/*.html'
];

module.exports.cssFilesToInject = cssFilesToInject.map(function(path) {
  return '.tmp/public/' + path;
});
module.exports.jsFilesToInject = jsFilesToInject.map(function(path) {
  return '.tmp/public/' + path;
});
module.exports.templateFilesToInject = templateFilesToInject.map(function(path) {
  return 'assets/' + path;
});

Now with the configuration set up, we can dive into the coding!

Front-end

Layout.ejs

Typically, we need to add a ng-app tag in our HTML, but this needs to be in our views/layout.ejs file. The layout.ejs is where our script and stylesheet tags are and the templating structure. Let’s modify the HTML tag to look like this: <html ng-app="todoApp">.

app.js

Now this is where the actual AngularJS programming comes in. Create assets/js/app.js. The app.js is going to be our primary controller as well as instantiate our angular module. Also, we need to include a method for retrieving all the todos (on page load), adding and remove a todo.

'use strict';

var todoApp = angular.module('todoApp', ['ngRoute', 'ui.bootstrap']);
todoApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.when('/', {
      templateUrl: '/templates/todo.html',
      controller: 'TodoCtrl'
    }).otherwise({
      redirectTo: '/',
      caseInsensitiveMatch: true
    })
  }]);

todoApp.controller('TodoCtrl', ['$scope', '$rootScope', 'TodoService', function($scope, $rootScope, TodoService) {
  $scope.formData = {};
  $scope.todos = [];

  TodoService.getTodos().then(function(response) {
    $scope.todos = response;
  });

  $scope.addTodo = function() {
    TodoService.addTodo($scope.formData).then(function(response) {
      $scope.todos.push($scope.formData)
      $scope.formData = {};
    });
  }

  $scope.removeTodo = function(todo) {
    TodoService.removeTodo(todo).then(function(response) {
      $scope.todos.splice($scope.todos.indexOf(todo), 1)
    });
  }
}]);

TodoService

Notice that we include a TodoService that we haven’t created yet. The service will communicate to our backend via a REST API we will create. Create assets/js/service/TodoService.js with the following code:

todoApp.service('TodoService', function($http, $q) {
  return {
    'getTodos': function() {
      var defer = $q.defer();
      $http.get('/todo/getTodos').success(function(resp){
        defer.resolve(resp);
      }).error( function(err) {
        defer.reject(err);
      });
      return defer.promise;
    },
    'addTodo': function(todo) {
      var defer = $q.defer();
      $http.post('/todo/addTodo', todo).success(function(resp){
        defer.resolve(resp);
      }).error( function(err) {
        defer.reject(err);
      });
      return defer.promise;
    },
    'removeTodo': function(todo) {
      var defer = $q.defer();
      $http.post('/todo/removeTodo', todo).success(function(resp){
        defer.resolve(resp);
      }).error( function(err) {
        defer.reject(err);
      });
      return defer.promise;
    }
}});
  

Template

Last thing we need for our front-end is the HTML template that our client will see and interact with. Here is assets/templates/todo.html that we referenced in app.js

<div class="container" ng-controller="TodoCtrl as todo">
    <div class="jumbotron">
        <h1 align="center">Todo Application</h1>
        <br>
        <div id="todo-form" class="row">
            <div class="col-sm-8 col-sm-offset-2 text-center">
                
            </div>
        </div>
    </div>
    <div id="todo-list" class="row">
        <div class="col-sm-4 col-sm-offset-4">
            <div class="checkbox" ng-repeat="singleTodo in todos">
                <label>
                    <input type="checkbox" ng-click="todo.removeTodo(singleTodo)">
                    {{ singleTodo.value }}
                </label>
            </div>
        </div>
    </div>
</div>

Data Flow Between the Front and Back End

One of the most challenging concepts new Sails.js developers has is determining how data flows between the front and back end. Let’s break down this process starting with the front-end using our application as an example.

Let’s say the user creates a todo from the view on the front-end. The logic that controls this part is located in the controller ($scope.addTodo function). Notice how this calls the service which uses the $http service to make an HTTP POST request to the URL http://localhost:1337/todo/addTodo

This is where Sails.js comes into play (further described below). The controller recognizes the addTodo request and in the TodoController, it communicates with the TodoService with the given todo information. Next, the service interacts with the todo model defined in Todo.js.

After updating the model, any errors or callback functions travel upstream to where the user can eventually see their newly created todo! This picture accurately sums up the communication between both, the front and back end.

frontend-to-backend-sails-angular

Back-end

Firstly, we need to create a model that will store our todos. This object will only hold the todo value and will be stored in the Waterline ORM. All the back-end code will be in the api directory. Lastly, the data flows from the controller to service to model and back up.

We can use the sails cli to create a model and controller skeleton. Let’s do this by sails generate api Todo.

Here is the model located at api/models/Todo.js:

Model

module.exports = {
  attributes: {
    value: {
      'type': 'text'
    }
  }
};

Controller

Now we need to have a controller that our service in the front-end can communicate with through the Sails.js generated API. Note that each of the functions must be the same name as the front-end service functions. Here is the controller located at api/controllers/TodoController.js:

module.exports = {
    getTodos: function(req, res) {
        TodoService.getTodos(function(todos) {
            res.json(todos);
        });
    },
    addTodo: function(req, res) {
        var todoVal = (req.body.value) ? req.body.value : undefined
        TodoService.addTodo(todoVal, function(success) {
            res.json(success);
        });
    },
    removeTodo: function(req, res) {
       var todoVal = (req.body.value) ? req.body.value : undefined
        TodoService.removeTodo(todoVal, function(success) {
            res.json(success);
        });
    };
};

Service

Lastly, we have the service that is the middleware between the controller and model. In this service, we use the Waterline ORM syntax to use CRUD operations on the model. Here is the service located at api/services/TodoService.js:

module.exports = {
  getTodos: function(next) {
    Todo.find().exec(function(err, todos) {
      if(err) throw err;
      next(todos);
    });
  },
  addTodo: function(todoVal, next) {
    Todo.create({value: todoVal}).exec(function(err, todo) {
      if(err) throw err;
      next(todo);
    });
  },
  removeTodo: function(todoVal, next) {
    Todo.destroy({value: todoVal}).exec(function(err, todo) {
      if(err) throw err;
      next(todo);
    });
  }
};

Lift Off!

Now that we’ve finished the code let’s take a look at our application! Once again, we can run our server by sails lift.

sails-angular-todo-application

And now by adding some todos, we got a nice list going! We can also remove them by checking them off.

sails-angular-todo-app-with-todos

Conclusion

Sails.js and AngularJS supply extraordinary tools to implement SPAs. Additionally, using Sails.js will help create robust applications for larger applications such as enterprise applications.

Hopefully, this small project has demystified developing applications in Sails.js and AngularJS.

Devan Patel

I'm an avid Javascript developer which I write about here. Feel free to reach out to me on my Twitter or check out the projects I work on at my Github!