Blog

Ideas and insights from our team

An approach to Angular separation of concerns


Angular.js is a great JavaScript framework, it makes easy to develop Single Page Applications (SPAs) and removes most of the pain regarding data binding, requests and routing.
Unlike many MVW (Model View Whatever) frameworks and because the way JavaScript works, it does not force us to organize projects in a specific way nor it defines where each piece of code goes in your projects (where do we initialize model variables: controllers? services?).
I've read many blog posts talking about interesting design patterns and project architectures that define how to organize code and make a clean Angular project. So, just for fun, I decided to write a little app trying to follow the patterns which I liked the most. By the end, I had a simple app with some interesting concepts, especially regarding separation of concerns, so I decided to share them hoping to get some feedback. The code is available in my GitHub:

https://github.com/filipeximenes/django-angular-jogging

It's a Django app that allows users to enter and edit times for their jogging activities. The API is powered by Django REST Framework, but for this post I want to focus on the Angular part, so please just ignore the rest of the project.

The Architecture

For the architecture I decided to go with an "app driven" pattern, where each app is [almost] self contained and has its own modules (Controllers, Services, etc). This is how most people recommend organizing the code when working in real world projects. Django developers will find this pattern pretty familiar.

/main.module.js
/core
 /core.module.js
 /core.ctrl.js
 /core.service.js
/accounts
 /accounts.module.js
 /accounts.ctrl.js
 /accounts.service.js
/timings
 /timings.module.js
 /timings.ctrl.js
 /timings.service.js

Click here to see it in the project

The main module

The way it's organized, the main module (main.module.js) is responsible to defining dependencies, setting global configurations and routing. Take a look here.

App Modules

Each app has a module file, responsible to naming it and declaring the internal dependencies.

(function (){
  var app = angular.module('Jogging.timings',
                          ['Jogging.timings.ctrl',
                           'Jogging.timings.service']);
})();

This also makes it easy to import apps in the main module:

var app = angular.module('Jogging', [
                            ...

                            'Jogging.core',
                            'Jogging.accounts',
                            'Jogging.timings']);

Restangular

Before going to the main part of this article, just a brief note: Angular $http service is nice. I mean, its simple enough and does most of what is expected from it. On the other side, Restangular is just great! It allows us to set global configurations like the base url, and has very clean interface to play with web APIs. I really recommend taking a look at the project.

Controllers and Services

Now to the interesting parts! The main rule I tried to follow during the development of this app was:

Keep STATE and LOGIC inside services and make extremely thin controllers

This way, controllers are simply the interface between template and the services.

Lets take a look on how timings are managed:

TimingsFactory:

app.factory('TimingsFactory',
  ['Restangular', 'ConversionFactory',
    function (Restangular, ConversionFactory){
      var timingsResource = Restangular.all('timings');

      obj = {};
      obj.timings = [];

      obj.getTimingList = function (filters){
        return timingsResource.getList(filters).then(function(response) {
          obj.timings = response.data;
        });
      };

      obj.createTiming = function (data){
        data.time = ConversionFactory.fromFormattedToSeconds(createData.formattedTime);

        return timingsResource.post(data).then(function (response){
          obj.timings.push(response.data);
        });
      };

      obj.updateTiming = function (index, data){
        var timingResource = Restangular.one('timings', obj.timings[index].id);
        angular.extend(timingResource, data);

        return timingResource.put().then(function (response){
          angular.extend(obj.timings[index], response.data);
        });
      };

      obj.deleteTiming = function (index){
        var timingResource = Restangular.one('timings', obj.timings[index].id);

        timingResource.remove().then(function (response){
          obj.timings.splice(index, 1);
        });
      };

      return obj;
    }
  ]);

As we can see, the TimingsFactory provides obj.timings variable responsible for the state of the timings list and all functions that perform actions on it (in this case, a simple CRUD).

The controller will only add the TimingsFactory to the $scope:

TimingsCtrl:

app.controller('TimingsCtrl',
 ['$scope', 'TimingsFactory', 'ConversionFactory', 'ReportsFactory',
   function ($scope, TimingsFactory, ConversionFactory, ReportsFactory){
     $scope.timingsFactory = TimingsFactory;
     ...
   }
 ]);

And service functions are called directly from the template. For example:

The form to create a timing:

<form class="form-inline">
  <div class="form-group">
    <label for="time">Time: </label>
    <input
      data-ng-model="createData.formattedTime"
      name="time" type="text"
      class="form-control">
  </div>
  <div class="form-group">
    <label for="distance">Distance: </label>
    <input
      data-ng-model="createData.distance"
      name="distance" type="number"
      class="form-control">
  </div>
  <div class="form-group">
    <label for="date">Date: </label>
    <input
      data-ng-model="createData.date"
      name="date" type="date"
      class="form-control">
  </div>
  <button
    data-ng-click="timingsFactory.createTiming(createData); createData = {};"
    class="btn btn-default">add</button>
</form>

Filtering timings:

<form class="form-inline">
  <div class="form-group">
    <label for="startDate">From: </label>
    <input
      data-ng-model="filters.start_date"
      type="date" name="startDate"
      class="form-control">
  </div>
  <div class="form-group">
    <label for="endDate">To: </label>
    <input
      data-ng-model="filters.end_date"
      type="date" name="endDate"
      class="form-control">
  </div>
  <button
    data-ng-click="timingsFactory.getTimingList(filters);"
    class="btn btn-default">filter</button>
</form>

and it you want to see some more: The listing and inline performing updates in timings.

Separating concerns

The timings area was easy, all interactions only affected the state of the timings list which were already in the service. So, how to deal with service operations that also affect other $scope data. Lets look at the login process.

If user correctly enters login info, we expect it to be redirected to the /timings controller, otherwise, provide a visual feedback that there was a problem.

To do this, the AccountFactory will receive an object containing envents to be executed after the resquest was made.

The performLogin function:

this.performLogin = function (data){
  Restangular.all('login').post(data)
    .then(function (response){
      _this.setCredentials(response.data.token);
      if (events.onPerformLoginSuccess){
        return events.onPerformLoginSuccess(response);
      }
    }, function (response){
      if (events.onPerformLoginFailure){
        return events.onPerformLoginFailure(response);
      }
    });
};

and this is how AccountFactory is instantiated:

LoginController

app.controller('LoginController',
  ['$scope', '$location', 'AccountFactory',
    function ($scope, $location, AccountFactory){
      $scope.accountFactory = new AccountFactory({
        onPerformLoginSuccess: function (response){
          $location.path('/timings');
        },
        onPerformLoginFailure: function (response){
          $scope.loginFailure = true;
        }
      });
    }
]);

That's all for now

So here is what I have to show for now, hope you all liked. Please leave a comment with your opinions so this can be improved!

About Filipe Ximenes

Bike enthusiast, software developer and former director at Python Brasil Association. Likes open source and how people interact in open source communities.

Comments