Giter VIP home page Giter VIP logo

angularjs-styleguide's Introduction

AngularJS styleguide

Opinionated AngularJS styleguide for teams by @toddmotto

This styleguide comprises of my experience with Angular, several talks and working in teams, building and structuring Angular applications.

John Papa and I have discussed in depth styling patterns for Angular's structure, syntax, conventions and everything else! Thanks to those discussions, I've learned some great tips from John that have helped shape this guide. We've both created our own take of a styleguide, I urge you to check his out to compare thoughts.

See the original article

Table of Contents

  1. Modules
  2. Controllers
  3. Services
  4. Factory
  5. Directives
  6. Filters
  7. Routing resolves
  8. Publish and subscribe events
  9. Performance
  10. Angular wrapper references
  11. Comment standards
  12. Minification and annotation

Modules

  • Definitions: Declare modules without a variable using the setter and getter syntax

    // bad
    var app = angular.module('app', []);
    app.controller();
    app.factory();
    
    // good
    angular
      .module('app', [])
      .controller()
      .factory();
  • Note: Using angular.module('app', []); sets a module, whereas angular.module('app'); gets the module. Only set once and get for all other instances.

  • Methods: Pass functions into module methods rather than assign as a callback

    // bad
    angular
      .module('app', [])
      .controller('MainCtrl', function MainCtrl () {
    
      })
      .service('SomeService', function SomeService () {
    
      });
    
    // good
    function MainCtrl () {
    
    }
    function SomeService () {
    
    }
    angular
      .module('app', [])
      .controller('MainCtrl', MainCtrl)
      .service('SomeService', SomeService);
  • This aids with readability and reduces the volume of code "wrapped" inside the Angular framework

  • IIFE scoping: To avoid polluting the global scope with our function declarations which get passed into Angular, ensure build tasks wrap the concatenated files inside an IIFE

    (function () {
    
      angular
        .module('app', []);
      
      // MainCtrl.js
      function MainCtrl () {
    
      }
      
      angular
        .module('app')
        .controller('MainCtrl', MainCtrl);
      
      // SomeService.js
      function SomeService () {
    
      }
      
      angular
        .module('app')
        .service('SomeService', SomeService);
        
      // ...
        
    })();

Back to top

Controllers

  • controllerAs syntax: Controllers are classes, so use the controllerAs syntax at all times

    <!-- bad -->
    <div ng-controller="MainCtrl">
      {{ someObject }}
    </div>
    
    <!-- good -->
    <div ng-controller="MainCtrl as main">
      {{ main.someObject }}
    </div>
  • In the DOM we get a variable per controller, which aids nested controller methods avoiding any $parent calls

  • The controllerAs syntax uses this inside controllers which gets bound to $scope

    // bad
    function MainCtrl ($scope) {
      $scope.someObject = {};
      $scope.doSomething = function () {
    
      };
    }
    
    // good
    function MainCtrl () {
      this.someObject = {};
      this.doSomething = function () {
    
      };
    }
  • Only use $scope in controllerAs when necessary, for example publishing and subscribing events using $emit, $broadcast, $on or using $watch, try to limit the use of these, however and treat $scope uses as special

  • Inheritance: Use prototypal inheritance when extending controller classes

    function BaseCtrl () {
      this.doSomething = function () {
    
      };
    }
    BaseCtrl.prototype.someObject = {};
    BaseCtrl.prototype.sharedSomething = function () {
    
    };
    
    AnotherCtrl.prototype = Object.create(BaseCtrl.prototype);
    
    function AnotherCtrl () {
      this.anotherSomething = function () {
    
      };
    }
  • Use Object.create with a polyfill for browser support

  • Zero-logic: No logic inside a controller, delegate to services

    // bad
    function MainCtrl () {
      this.doSomething = function () {
    
      };
    }
    
    // good
    function MainCtrl (SomeService) {
      this.doSomething = SomeService.doSomething;
    }
  • Think "skinny controller, fat service"

Back to top

Services

  • Services are classes and are instantiated with the new keyword, use this for public methods and variables

    function SomeService () {
      this.someMethod = function () {
    
      };
    }

Back to top

Factory

  • Singletons: Factories are singletons, return a host Object inside each Factory to avoid primitive binding issues

    // bad
    function AnotherService () {
      var someValue = '';
      var someMethod = function () {
    
      };
      return {
        someValue: someValue,
        someMethod: someMethod
      };
    }
    
    // good
    function AnotherService () {
      var AnotherService = {};
      AnotherService.someValue = '';
      AnotherService.someMethod = function () {
    
      };
      return AnotherService;
    }
  • This way bindings are mirrored across the host Object, primitive values cannot update alone using the revealing module pattern

Back to top

Directives

  • Declaration restrictions: Only use custom element and custom attribute methods for declaring your Directives ({ restrict: 'EA' }) depending on the Directive's role

    <!-- bad -->
    
    <!-- directive: my-directive -->
    <div class="my-directive"></div>
    
    <!-- good -->
    
    <my-directive></my-directive>
    <div my-directive></div>
  • Comment and class name declarations are confusing and should be avoided. Comments do not play nicely with older versions of IE, using an attribute is the safest method for browser coverage.

  • Templating: Use Array.join('') for clean templating

    // bad
    function someDirective () {
      return {
        template: '<div class="some-directive">' +
          '<h1>My directive</h1>' +
        '</div>'
      };
    }
    
    // good
    function someDirective () {
      return {
        template: [
          '<div class="some-directive">',
            '<h1>My directive</h1>',
          '</div>'
        ].join('')
      };
    }
  • DOM manipulation: Only takes place inside Directives, never a controller/service

    // bad
    function UploadCtrl () {
      $('.dragzone').on('dragend', function () {
        // handle drop functionality
      });
    }
    angular
      .module('app')
      .controller('UploadCtrl', UploadCtrl);
    
    // good
    function dragUpload () {
      return {
        restrict: 'EA',
        link: function (scope, element, attrs) {
          element.on('dragend', function () {
            // handle drop functionality
          });
        }
      };
    }
    angular
      .module('app')
      .directive('dragUpload', dragUpload);
  • Naming conventions: Never ng-* prefix custom directives, they might conflict future native directives

    // bad
    // <div ng-upload></div>
    function ngUpload () {
      return {};
    }
    angular
      .module('app')
      .directive('ngUpload', ngUpload);
    
    // good
    // <div drag-upload></div>
    function dragUpload () {
      return {};
    }
    angular
      .module('app')
      .directive('dragUpload', dragUpload);
  • Directives and Filters are the only providers that we have the first letter as lowercase, this is due to strict naming conventions in Directives due to the way Angular translates camelCase to hyphenated, so dragUpload will become <div drag-upload></div> when used on an element.

  • controllerAs: Use the controllerAs syntax inside Directives also

    // bad
    function dragUpload () {
      return {
        controller: function ($scope) {
    
        }
      };
    }
    angular
      .module('app')
      .directive('dragUpload', dragUpload);
    
    // good
    function dragUpload () {
      return {
        controllerAs: 'dragUpload',
        controller: function () {
    
        }
      };
    }
    angular
      .module('app')
      .directive('dragUpload', dragUpload);

Back to top

Filters

  • Global filters: Create global filters only using angular.filter() never use local filters inside Controllers/Services

    // bad
    function SomeCtrl () {
      this.startsWithLetterA = function (items) {
        return items.filter(function (item) {
          return /$a/i.test(item.name);
        });
      };
    }
    angular
      .module('app')
      .controller('SomeCtrl', SomeCtrl);
    
    // good
    function startsWithLetterA () {
      return function (items) {
        return items.filter(function (item) {
          return /$a/i.test(item.name);
        });
      };
    }
    angular
      .module('app')
      .filter('startsWithLetterA', startsWithLetterA);
  • This enhances testing and reusability

Back to top

Routing resolves

  • Promises: Resolve dependencies of a Controller in the $routeProvider (or $stateProvider for ui-router) not the Controller itself

    // bad
    function MainCtrl (SomeService) {
      var _this = this;
      // unresolved
      _this.something;
      // resolved asynchronously
      SomeService.doSomething().then(function (response) {
        _this.something = response;
      });
    }
    angular
      .module('app')
      .controller('MainCtrl', MainCtrl);
    
    // good
    function config ($routeProvider) {
      $routeProvider
      .when('/', {
        templateUrl: 'views/main.html',
        resolve: {
          // resolve here
        }
      });
    }
    angular
      .module('app')
      .config(config);
  • Controller.resolve property: Never bind logic to the router itself, reference a resolve property for each Controller to couple the logic

    // bad
    function MainCtrl (SomeService) {
      this.something = SomeService.something;
    }
    
    function config ($routeProvider) {
      $routeProvider
      .when('/', {
        templateUrl: 'views/main.html',
        controllerAs: 'main',
        controller: 'MainCtrl'
        resolve: {
          doSomething: function () {
            return SomeService.doSomething();
          }
        }
      });
    }
    
    // good
    function MainCtrl (SomeService) {
      this.something = SomeService.something;
    }
    
    MainCtrl.resolve = {
      doSomething: function () {
        return SomeService.doSomething();
      }
    };
    
    function config ($routeProvider) {
      $routeProvider
      .when('/', {
        templateUrl: 'views/main.html',
        controllerAs: 'main',
        controller: 'MainCtrl'
        resolve: MainCtrl.resolve
      });
    }
  • This keeps resolve dependencies inside the same file as the Controller and the router free from logic

Back to top

Publish and subscribe events

  • $scope: use the $emit and $broadcast methods to trigger events to direct relationship scopes only

    // up the $scope
    $scope.$emit('customEvent', data);
    
    // down the $scope
    $scope.$broadcast('customEvent', data);
  • $rootScope: use only $emit as an application-wide event bus and remember to unbind listeners

    // all $rootScope.$on listeners
    $rootScope.$emit('customEvent', data);
  • Hint: $rootScope.$on listeners are different to $scope.$on listeners and will always persist so they need destroying when the relevant $scope fires the $destroy event

    // call the closure
    var unbind = $rootScope.$on('customEvent'[, callback]);
    $scope.$on('$destroy', unbind);
  • For multiple $rootScope listeners, use an Object literal and loop each one on the $destroy event to unbind all automatically

    var rootListeners = {
      'customEvent1': $rootScope.$on('customEvent1'[, callback]),
      'customEvent2': $rootScope.$on('customEvent2'[, callback]),
      'customEvent3': $rootScope.$on('customEvent3'[, callback])
    };
    for (var unbind in rootListeners) {
      $scope.$on('$destroy', rootListeners[unbind]);
    }

Back to top

Performance

  • One-time binding syntax: Use the one-time binding syntax {{ ::value }} where it makes sense, in newer versions of Angular (v1.3.0-beta.10+)

    // bad
    <h1>{{ vm.title }}</h1>
    
    // good
    <h1>{{ ::vm.title }}</h1>

    Why? : Binding once removes the $$watchers count after the undefined variable becomes resolved, thus reducing performance in each dirty-check

  • Consider $scope.$digest: Use $scope.$digest over $scope.$apply where it makes sense, child scopes will update only

    $scope.$digest();

    Why? : $scope.$apply will call $rootScope.$digest, which causes the entire application $$watchers to dirty-check again, using $scope.$digest will dirty check current and child scopes from the initiated $scope

Back to top

Angular wrapper references

  • $document and $window: Use $document and $window at all times to aid testing and Angular references

    // bad
    function dragUpload () {
      return {
        link: function (scope, element, attrs) {
          document.addEventListener('click', function () {
    
          });
        }
      };
    }
    
    // good
    function dragUpload () {
      return {
        link: function (scope, element, attrs, $document) {
          $document.addEventListener('click', function () {
    
          });
        }
      };
    }
  • $timeout and $interval: Use $timeout and $interval over their native counterparts to keep Angular's two way data binding up to date

    // bad
    function dragUpload () {
      return {
        link: function (scope, element, attrs) {
          setTimeout(function () {
            //
          }, 1000);
        }
      };
    }
    
    // good
    function dragUpload ($timeout) {
      return {
        link: function (scope, element, attrs) {
          $timeout(function () {
            //
          }, 1000);
        }
      };
    }

Back to top

Comment standards

  • jsDoc: Use jsDoc syntax to document function names, description, params and returns

    /**
     * @name SomeService
     * @desc Main application Controller
     */
    function SomeService (SomeService) {
    
      /**
       * @name doSomething
       * @desc Does something awesome
       * @param {Number} x First number to do something with
       * @param {Number} y Second number to do something with
       * @returns {Number}
       */
      this.doSomething = function (x, y) {
        return x * y;
      };
    
    }
    angular
      .module('app')
      .service('SomeService', SomeService);

Back to top

Minification and annotation

  • ng-annotate: Use ng-annotate for Gulp as ng-min is deprecated and comment functions that need automated dependency injection using /** @ngInject */

    /**
     * @ngInject
     */
    function MainCtrl (SomeService) {
      this.doSomething = SomeService.doSomething;
    }
    angular
      .module('app')
      .controller('MainCtrl', MainCtrl);
  • Which produces the following output with the $inject annotation

    /**
     * @ngInject
     */
    function MainCtrl (SomeService) {
      this.doSomething = SomeService.doSomething;
    }
    MainCtrl.$inject = ['SomeService'];
    angular
      .module('app')
      .controller('MainCtrl', MainCtrl);

Back to top

Angular docs

For anything else, API reference, check the Angular documentation.

Contributing

Open an issue first to discuss potential changes/additions.

License

(The MIT License)

Copyright (c) 2014 Todd Motto

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

angularjs-styleguide's People

Contributors

stryju avatar toddmotto avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.