David Cai | Blog

Angular Best Practice Recap

Sat Aug 1, 2015

Best practice

I attended a great Angular training provided by OasisDigital the last week. Learned so many tips and best practices from Bill Odom and his team. While my memory is still fresh, I’d like to document the stuff I learned. So again, I use my blog as study notes. While I’m on the topic of Angular best practices, I also like to bring in some advices from John Papa, Shai Reznik, and other wisdoms of the community. This article basically is a recap of what I heard and learned.


PREFER FACTORY OVER SERVICE

A service is a simplified version of a factory. Services are a constructor functions. However, I’ve seen some service code that returns objects. The author of the code probably mistook services with factories. To clear the confusion, let’s favor factories over services. I personally prefer naming a factory as xyzService instead of xyzFactory.

angular
  .module('myApp')
  .factory('fooService', fooService)
;

function fooService() {
  return {
    getFoo: function() { ... },
    getBar: function() { ... }
  };
}


ASSIGN THIRD-PARTY GLOBALS TO VALUES

Although I can use $window to reference globals of third-party libraries, I like to assign globals to values, and inject them. This makes function arguments a clear contract of what dependencies my code requires.

angular
  .module('myApp')
  .value('_', _) // lodash
  .value('d3', d3) // D3
;

...

function MainController(_, d3) { // Injection
  _.forEach( ... );
  d3.select( ... );
}

For low-level libraries (e.g., lodash), I can even delete lodash’s global ‘’ from the window object, so ‘’ will be available only through injection:

angular
  .module('myApp')
  .factory('_', function() {
    var _ = window._;
    delete window._; // Delete _ from window

    return _;
  })
;


STICK WITH $HTTP

I feel $http gives me more flexibilities than $resource. And $http service’s promise interface is much nicer. Better to stick with $http than mixing $http and $resource which unnecessarily complicate my code.


WRAP REST IN SERVICES

Encapsulate REST requests as methods in services, so users of the service don’t have to know the interface of your REST APIs, and the choice of REST libraries ($http, $resource, etc.).

angular
  .module('myApp')
  .factory('accountService', accountService)
;

function accountService($http, $log) {
  return {
    getAccounts: function(userId) {
      return $http.get('/api/accounts/' + userId)
        .then(function(response) {
          return response.data;
        })
        .catch(function(error) {
          $log.error(error);
          return error;
        })
      ;
    }
  };
}


UI ROUTER RESOLVE PATTERN

I often run to the scenarios where I need to hold off displaying UI until certain data is ready. The resolve property of uiRouter is designed to tackle this common problem.

Assign a name-to-function map to the resolve property. If the function returns a value, the returned value is treated as dependency, and the value is injected into the controller; if the function returns a promise, the promise is resolved before the controller is instantiated, and the resolved value is injected into the controller.

$stateProvider
  .state('home', {
    ... ,

    // Resolve items and users before routing to the 'home' state
    resolve: {
      items: function() {
        // Returns an array value
        return [
          { name: 'The Settlers of Catan', price: 51.46 },
          { name: 'Mansions of Madness', price: 56.52 }
        ];
      },
      users: function(userService) {
        // Returns a promise
        return userService.getUsers();
      }
    },

    // items and users are injected to the controller
    controller: function($scope, $log, items, users) {
      $log.log(items);
      $log.log(users);
    }
  });


ISOLATE DIRECTIVE SCOPE

Think Angular directives as re-usable functions, and its scope as the argument list of a function. In most cases, a directive shouldn’t assume the presence of data in the ancestor scopes or elements in the DOM. The scope (and the directive’s DOM attributes) should be the only place where a directive retrieves external information.

Pass information to directives via element attributes:

<div data-browse-happy-banner
  data-ie-version="8"
  data-on-dismiss="bannerDismissed()"></div>

Then retrieve these information from the scope:

angular
  .module('myApp')
  .directive('browseHappyBanner', browseHappyBanner)
;

function browseHappyBanner() {
  return {
    restrict: 'A',
    scope: { // Isolated scope
      ieVersion: '@', // Pass ieVersion from data-ie-version attribute
      onDismiss: '&' // Pass onDismiss callback from data-on-dismiss attribute
    },
    template: '<div> ... <a data-ng-click="onClose()"></a></div>',
    ... ,
    link: function(scope) {
      // Retrieve ieVersion from scope
      var iev = parseInt(scope.ieVersion, 10);
      ...
      scope.onClose = function() {
        // Invoke onDismiss callback from the scope
        scope.onDismiss();  
      };
    }
  };
}


CLEAN TEMPLATE

If snippets in a template look like program, then you are doing it wrong. Templates need to be declarative, easy to understand for non-programmers. The best practice is to imagine your template is written by a web designer rather than a developer.

Will a web designer understand the following template?

<div data-ng-class="{
  'error': ((bFormSubmitted || PersonalInfoForm.DOBmonth.$dirty) && PersonalInfoForm.DOBmonth.$invalid) ||
            ((bFormSubmitted || PersonalInfoForm.DOBday.$dirty) && PersonalInfoForm.DOBday.$invalid) ||
            ((bFormSubmitted || PersonalInfoForm.DOByear.$dirty) && PersonalInfoForm.DOByear.$invalid ) || errorMapResult.birthdate,
  'success': (PersonalInfoForm.DOBmonth.$dirty && !PersonalInfoForm.DOBmonth.$invalid) &&
              (PersonalInfoForm.DOBday.$dirty && !PersonalInfoForm.DOBday.$invalid) &&
              (PersonalInfoForm.DOByear.$dirty && !PersonalInfoForm.DOByear.$invalid )}">
  ...
</div>

Guess no. Even developer might have hard time to understand this. Move the logic to controller or service, so the template becomes clean:

<div data-ng-class="{ 'error': hasError, 'success': isSuccessful }">
  ...
</div>


THIN CONTROLLER

Make controller thin. Move logic to services and directives. This makes controller much easy to test.


LIGHT FILTER

Filter tends to be called multiple times. So if you have a filter that does a lot of heavy lifting, it will drag down the performance of the entire app. Try to simplify the logic in filter. Use cache or memoization to improve the performance.


angular
  .module("myApp")
  .filter("heavyLifting", heavyLifting) // A filter does heavy lifting stuff
;

function heavyLifting() {

  var doHeavyLifting = function() { ... }; // Complicate process

  // Use lodash's memoize function to cache the returned result for a specific input.
  // Future calls with the same input will return the cached result.
  return _.memoize(function(input) {
    return doHeavyLifting(input);
  });
}

More details on filter and memoization can be found here


NAMED FUNCTIONS

Use named functions whenever you can. When error occurs inside the named function, the function name will appear in the stack trace, which is really helpful for debugging.

// Named function showToggle
$timeout(function showToggle() {
  angular.element('.toggle').css('display', 'block');
}, 1000);

// Named function greet
var greet = function greet(name) {
  console.log('Hello ' + name);
};

// Named controller
angular.module('myApp').controller('MyController', MyController);
function MyController() { ... }


SCOPE JS CODE IN IIFE

This best practice is not limited to Angular. IIFE (Immediately-Invoked Function Expression) is often used to create scopes in which the variables and functions will not be leaked to outer scopes.

;(function() {
  ...
  var app = angular.module('myApp');
  ...
})();

console.log(app) // not defined


PYRAMID TESTING

Again this advice is not limited to Angular.

Write more unit tests. Treat unit tests as front-line defense. Use e2e protractor tests as high-level tests for verifying critical happy and error paths.

Martin Fowler discussed Test pyramid in his blog.

Test Pyramid


NG-MODEL DOT RULE

Misko Hevery in his presentation on Angular best practices coined the dot rule. My over-simplified version is to bind objects instead of primitives to $scope, and always initialize the bound object:

function MainController($scope) {
  $scope.something = {}; // Always initialize the bound objects

  $scope.person = {
    name: 'David Cai'
  }; // Bind objects to $scope instead of $scope.name = 'David Cai';
}

In the HTML template:

<div ng-controller="MainController">
  <input ng-model="person.name">
</div>


ONE COMPONENT PER FILE

Limit one Angular component (directive, service, controller, etc.) per JavaScript file. Make it easy to locate components, and keep files shorter.

angular
  .module('myApp')
  .directive('myAwesomeWidget', function() { ... })

  // Don't.
  // We already defined a directive in this file.
  // Move the following directive to another file.
  .directive('myOtherAwesomeWidget', function() { ... })
;


GROUP FILES BY FEATURES

There are two schools of thoughts when it comes to organizing Angular files - grouping by types, and grouping by features. I prefer the latter for big projects. Much easier for navigation.

common
  - util.filter.js
  - util.filter.spec.js
action-panel
  - action-panel.directive.js
  - action-panel.directive.spec.js
  - action-panel.html
  - action-panel.scss


INCLUDE INTENTION IN FILE NAMES

.js can mean a lot of things in Angular apps. It makes the intention much clear when the file names contain .controller, .directive, .spec, .service, etc.

Shai Reznik recommends using .ctrl for controllers, .drv for directives, .srv for services., .tpl for templates, and .test for unit tests. I prefer full names, e.g. .controller. I also feel that HTML files don’t need .tpl or .template, since in most cases, HTML files are templates. No need to explicitly call them out as templates.


LINT JAVASCRIPT

Use jshint, JSCS, or even better - eslint to lint javascript code. Integrate lint into the build process. Use editor plugins to bring lint warnings and errors right in front of developers’s eyes. I’ve been considering to create GIT hook to block git pushes if lint reports any warnings or errors.


USE NG-ANNOTATE

Angular replies on the names of function arguments for dependency injection. However, JS minification will shorten argument names, which ruined Angular DI. To work around this issue, we either use the DI array syntax:

// DI array syntax
app.controller('MainController', ['$scope', '$log', function($scope, $log) {
  ...
}]);

… or assign dependencies to the $inject property:

// $inject syntax
MainController.$inject = ['$scope', '$log'];
function MainController($scope, $log) {
  ...
}

These workarounds look like patchwork to me. Not to mention that both are error prone where you have to maintain the dependency order in arrays.

Here comes ngAnnotate - an Angular plugin that automatically inserts the DI array syntax in source code:

// ngAnnotate will replace function($scope, $log) { ... } with
// ['$scope', '$log', function($scope, $log) { ... }]
app.controller('MainController', function($scope, $log) {
  ...
});

Although ngAnnotate is quite smart to figure out where to annotate DI syntax, in some cases, ngAnnotate might miss the DI annotation for a function. You can prepend a function with /*@ngInject*/ to explicitly state that the function should get annotated.


LEVERAGE TASK RUNNERS

Lint and ngAnnotate can be integrated in the build process by task runners such as Gulp.

Use Gulp to automate:



« »

comments powered by Disqus