ruk·si

📐 Angular 1.X
Guide

Updated at 2016-06-17 02:30

Most of the examples here can just be copy pasted to a JSFiddle.

General

Where should I use which component:

  • Modules: a namespace for services, controllers, directives and filters. You should have one main application module with multiple submodules.
  • Controllers: data layering, data processing, presentation logic and data fetching using services. Controllers can be standalone using module.controller() syntax or embedded; e.g. into directives and routes with controller: function() {} syntax. Controllers become fat and you move stuff to services.
  • Directives: handle DOM manipulation and DOM interaction. All DOM related functionality should still be limited to link function though.
  • Filters: reusable logic for data formatting and filtering. Add currency, format date time, capitalize string, show only specific items in a list etc.
  • Services: libraries accessible through Angular dependency injection. HTTP API calls, data cache, centralized/shared data, login mechanism, wrapping third-party non-UI libraries etc. All functionality that doesn't belong to any of the above usually end up in services. It's better to have multiple small services than one big one.
Usually reusable: Modules, Services, Directives, Filters
Usually one-offs: Controllers
angular.module('myApp', []); // [] is the list of dependencies
angular.module('myApp');     // Without it, it returns the module.

// these definitions are usually in separate files
angular.module('myApp').value('a', ...);
angular.module('myApp').service('a', ....);
angular.module('myApp').factory('a', ....);
angular.module('myApp').controller('a', ...)
angular.module('myApp').directive('a', ...).
angular.module('myApp').filter('a', ...);

Keep all files under 100 lines. Yes, everything can be neatly split into under 100 line files.

One entity per file. Each standalone controller, directive, filter, etc. should have their own file.

Group files by feature they create, not by type.

ng/
  main-module/
    main-module.js
  feature-module-x/
    feature-module-x.js
    services/
      user-service.js
      product-service.js
    components/
      common/
        ajax-lookup.js
      profile/
        profile-ctrl.js
        profile.js
        profile.html
        profile-small.js
        profile-small.html
      product/
        product-ctrl.js
        product.js
        product.html
    tests/
      services/
        user-service.test.js
        product-service.test.js
      components/
        common/
          ajax-lookup.test.js
        profile/
          profile-ctrl.test.js
          profile.test.js
          profile-small.test.js
        product/
          product-ctrl.single.test.js
          product-ctrl.multiple.test.js
          product-ctrl.http.test.js
          product.test.js
  • Everything under ng/ should be publicly accessible / minified together.
  • Controller names should be upper first camel case and should end in Ctrl e.g. ProfileCtrl.
  • Directive names should be lower first camel case e.g. profileImage.
  • Files should be in lower-snake-case.
  • Tests should be in similar directory structure.
  • Multiple test files can test the same entity.

You create all Angular components inside a module.

Angular will scan everything inside ng-app for Angular directives. You cloud leave it without a value but usually you want to namespace your Angular components under a module

<body ng-app>
  <span>{{1 + 2}}</span>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
</body>
<body ng-app="myApp">
  <span>{{1 + 2}}</span>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', []);
  </script>
</body>

angular has a lot of helper methods built in. But still it is a good idea to add a functional library like lodash or underscore on top of it.

angular
  .forEach(collection, function)
  .fromJson(obj)
  .toJson(obj)
  .copy(obj) // deep clone, used to duplicate data while unbinding the copy
  .extend(destination, source, source) // shallow, start with {} for copy
  .merge(destination, source, source)  // deep, start with {} for copy
  .isDefined/isUndefined
  .isArray/isDate/isFunction/isNumber/isString/isObject/isElement
  .element(ele) // apply jQuery or jQuery lite wrapper

Scopes are glue objects that are passed between views and controllers. Scope is a JavaScript object that contains methods and variables which partials (HTML) can access.

  • ng-app initializes the root scope.
  • Each controller defines a new scope.
  • Each directive can live in the parent scope or create an isolated scope.
  • Normal scopes are chained by their prototypes; isolated scopes are not.
  • Variables in a scope are usually called models.
  • Controllers attach methods and variables to a scope.

Views

View is a projection of a scope through a HTML template.

Use {{}} with optional ng-cloak when possible. Angular has two ways to show a variable in a view; ng-bind attribute and {{var}} syntax. {{}} is tidier but causes the original version to flash for users if ng-cloak (and the CSS) are not set.

<body ng-app>
  <style> [ng-cloak] { display: none; } </style>
  <input type="text" ng-model="name" placeholder="Enter your name">

  <!-- Notice how they appear when you refresh the page. -->
  <h1>Hello <span ng-bind="name"></span></h1>
  <h1>Hello {{name}}</h1>
  <h1 ng-cloak>Hello {{name}}</h1>

  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', []);
  </script>
</body>

One-time bindings are defined with :: prefix. The value is read once and the variables is unbound so it won't change. You can unbind variables, object with bound variables or arrays of bound variables.

<body ng-app="myApp">
  <div ng-controller="OneTimeCtrl as otc">
    <input type="text" ng-model="otc.name">
    <div>{{otc.name}}</div>
    <div>{{::otc.name}}</div>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', [])
      .controller('OneTimeCtrl', [function() {
        this.name = 'Ruksi';
      }]);
  </script>
</body>

ng-model binds an input to a JavaScript object in angular. Variable named name in the Angular scope will have a two-way binding with the input.

<body ng-app>
  <input type="text" ng-model="name" placeholder="Enter your name">
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
</body>

All directives (non-service components) are bound to a scope. By default they are in $rootScope if you don't specify a directive inside a controller or create a directive with an isolated scope.

<body ng-app="myApp">

  <!-- root scope, see that they change at the same time -->
  <div><input type="text" ng-model="name"> <span>{{name}}</span></div>
  <div><input type="text" ng-model="name"> <span>{{name}}</span></div>

  <!-- controller scope, old syntax, note that it changes independently -->
  <div ng-controller="OldCtrl">
    <input type="text" ng-model="name"> <span>{{name}}</span>
  </div>

  <!-- controller scope, new syntax, better if you use nested controllers -->
  <div ng-controller="NewCtrl as nc">
    <input type="text" ng-model="nc.name"> <span>{{nc.name}}</span>
  </div>

  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>

    // Normally, all of these should be in separate js files.
    angular.module('myApp', []);

    angular.module('myApp')
      .controller('OldCtrl', ['$scope', function($scope) {
        $scope.name = 'Ruksi';
      }])

    angular.module('myApp')
      .controller('NewCtrl', [function() {
        this.name = 'Ruksi';
      }]);

  </script>
</body>

Controllers

  • Everything that must be accessible from HTML {{}} expressions go to this or $scope in the controller
  • Anything else goes to local variables in controller

Controllers should never touch the DOM. Controllers shouldn't reference anything in the DOM so no jQuery selectors in controllers. Referencing the DOM makes controllers harder to test. You can have presentation logic like defining functions for CSS class definitions but those values should be utilized through directives like ngClass.

Let the models and values to drive the UI. Don't do it by hand, use all kinds of listeners as much as possible.

Avoid deep $watch. By default $watch checks are shallow comparisons but you can make them deep by setting the third argument as true. But deep comparisons are expensive. Create an additional boolean value and use that to signal that the complex value has changed.

Use $broadcast and $emit in controller scopes. Counterpart for $scope.$on() event listeners.

function ExampleCtrl($scope) {
  $scope.$on('event-name', function handler() {
    //body
  });
}

function ExampleCtrl($scope) {
  $scope.$emit('event-name', { foo: 'bar' });
}

Avoid adding event listeners to $rootScope. Only services should be are allowed to do that as they don't have their own scope. A controller can be destroyed but the event listeners is still left to the $rootScope. Only $emit events in the $rootScope, avoid broadcasting.

Use $scope.$apply when doing anything non-Angular but asynchronous. All asynchronous Angular services like $http and $timeout do the $apply automatically. For non-Angular components, you need to do it manually.

Use CSS class objects. They are easier to test than reading rendered HTML.

<div ng-controller="TodoCtrl as ctrl">
  <div ng-class="ctrl.getNoteClass(note.done)">
      {{note.label}}
  </div>
</div>
<script type="text/javascript">
  angular
    .module('myApp', [])
    .controller('TodoCtrl', ['$scope', function($scope) {
      $scope.getNoteClass = function(status) {
        return { done: status, pending: !status };
      };
  }]);
</script>

Use full-form dependency injection. Minimizing the first style won't work.

// BAD
.controller('MyCtrl', [function($scope) { .. }]);

// GOOD
.controller('MyCtrl', ['$scope', function($scope) { .. }]);

Directives

Most DOM manipulation should happen in link, template manipulation in compile. Link function is called when Angular binds data ($scope) to the HTML, compile is called to create the link function. You should never use link and compile together.

Receive data by isolated scope scope: {}, child scope scope: true or services. You should never need to use the controller scope or inherit from it. Explicitly specifying what you want through HTML attributes is a lot cleaner.

Isolated scope binding symbols:

  • = passing a bound JSON object or literal from surrounding scope.
  • @ passing a string with potential Angular expressions {{}}.
  • & passing a function from surrounding scope.

Clean events on $scope.$on('destroy', ...). If you add events to DOM or JavaScript external to the directive itself, remember to remove them when the directive isolated scope is destroyed. If you are for some reason inheriting parent scope, use $element $destroy event instead.

Use transclude if directive content is specified by the user.

transclude: true = specify ng-transclude inside the template.
                   Transcluded HTML also has access to the parent scope,
                   even if the directive HTML is in isolated scope.
transclude: 'element' = replace the entire element, not just the ng-transclude
                        part. Transcluded content is found in $transclude
                        parameter in link function. This is used
                        for more complex directives like ng-repeat.

Use require for requiring other directives. require can be used for specifying dependencies on it self or that parent chain has it.

Use ng-cloak directive or class to hide soon-to-be-compiled markup. Or use ng-bind directive, depending on the case.

ng-click

ng-click is used for acting on clicks.

<body ng-app="myApp">
  <div ng-controller="CountCtrl as cc">
    <button ng-click="cc.increment()">+</button><span>{{cc.count}}</span>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', [])
      .controller('CountCtrl', [function() {
        this.count = 0;
        this.increment = function() { this.count++; }.bind(this);
        // older $scope syntax would work as well
      }])
  </script>
</body>

ng-repeat

ng-repeat is used for rendering a collection of something. It also provides helper values for styling.

<body ng-app>
  <div ng-repeat="name in ['ruksi', 'muksi', 'boksi']">
    <b>{{name}}</b>
    <div>First: {{$first}}</div>
    <div>Middle: {{$middle}}</div>
    <div>Last: {{$last}}</div>
    <div>Index: {{$index}}</div>
    <div>Even: {{$even}}</div>
    <div>Odd: {{$odd}}</div>
    <br/><br/>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
</body>

Looping over objects is also done with ng-repeat.

<body ng-app>
  <div ng-repeat="(letter, number) in {a: 1, b: 2, c: 3}">
    {{letter}} => {{number}}
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
</body>

You can also create multipart-ng-repeat.

<body ng-app>
  <table>
    <tr ng-repeat-start="note in [1,2,3,4]">
      <td>Start: {{note}}</td>
    </tr>
    <tr>
      <td>Between!</td>
    </tr>
    <tr ng-repeat-end>
      <td>End: {{note}}</td>
    </tr>
  </table>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
</body>

track by changes how Angular tracks changes in repeat collections. This allows e.g. having duplicates in repeats and boosting performance. track by $index is a special case.

<body ng-app="myApp">
  <div ng-controller="HashCtrl as hc">
    <div ng-repeat="note in hc.notes track by note.id">
      <div>{{note.id}}</div>
      <div>{{note.text}}</div>
    </div>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', [])
      .controller('HashCtrl', [function() {
        this.notes = [
          { id: 1, text: 'First Note' },
          { id: 2, text: 'Second Note' },
          { id: 3, text: 'Finished Third Note' }
          // { id: 1, text: 'Oops' } // This would throw an error.
        ];
      }]);
  </script>
</body>

Use ng-repeat helper variables. They are specially good with in styling.

<div ng-repeat="item in items">
  <div>First Element: {{ $first }}</div>
  <div>Middle Element: {{ $middle }}</div>
  <div>Last Element: {{ $last }}</div>
  <div>Index of Element: {{ $index }}</div>
  <div>At Even Position: {{ $even }}</div>
  <div>At Odd Position: {{ $odd }}</div>
</div>

Understand how ng-repeats track by works. ng-repeat generates and caches a DOM element for each item in the collection. track by allows you to change the cache key, stored in $$hashKey variable.

<div ng-repeat="item in items track by item.id">

ng-form

Use ng-form helper CSS classes when doing client-side validation.

State         Applied CSS Class on Element
$invalid      ng-invalid
$valid        ng-valid
$pristine     ng-pristine
$dirty        ng-dirty
required      ng-valid-required, ng-invalid-required
min           ng-valid-min,      ng-invalid-min,      etc.

Angular has a pretty good set of basic validation directives and decent error message management.

<body ng-app="myApp">
  <div ng-controller="ValidationCtrl as vc">
    <form ng-submit="vc.submit()" name="myForm">
      <input type="text"
             name="username"
             ng-model="vc.user.username"
             ng-model-options="{ debounce: 500 }"
             ng-minlength="4"
             required>
      <span ng-show="myForm.username.$error.required">
        This is a required field
      </span>
      <span ng-show="myForm.username.$error.minlength">
        Minimum length required is 4
      </span>
      <input type="password"
             ng-model="vc.user.password"
             required>
      <input type="submit"
             value="Submit"
             ng-disabled="myForm.$invalid">
    </form>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', [])
      .controller('ValidationCtrl', [function() {
        this.submit = function() {
          console.log('User clicked submit with ', this.user);
        }.bind(this);
      }])
  </script>
</body>

ng-repeat also allows creating checkboxes with ease.

<body ng-app="myApp">
  <div ng-controller="CheckboxCtrl as cc">
    <div ng-repeat="animal in cc.animals">
      <label>{{animal.name}}
      <input type="checkbox"
             ng-model="animal.selected"
             ng-true-value="'YES'"
             ng-false-value="'NO'"
      ></label>
    </div>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', [])
      .controller('CheckboxCtrl', [function() {
        this.animals = [
         {name: 'Dog', selected: 'YES'},
         {name: 'Cat', selected: 'NO'},
         {name: 'Owl', selected: 'NO'}
       ];
      }])
  </script>
</body>

ng-options allows creating dropdown menus.

<body ng-app="myApp">
  <div ng-controller="DropdownCtrl as dc">
    <div>
    <select ng-model="dc.selectedCountryId"
            ng-options="c.id as c.name for c in dc.countries">
    </select>
      Selected Country ID : {{dc.selectedCountryId}}
    </div>
  </div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"></script>
  <script>
    angular.module('myApp', [])
      .controller('DropdownCtrl', [function() {
        this.countries = [
         {id: 111, name: 'Finland'},
         {id: 222, name: 'Sweden'},
         {id: 333, name: 'Norway'}
       ];
       this.selectedCountryId = 222;
      }])
  </script>
</body>

Filters

Filters should be fast. Heavy processing and DOM manipulation should never be done as filters will be executed multiple times per second.

You can dependency inject filters inside controllers and services. Good for optimization and creating filters based on existing filters.

Use filter filter. filter is versatile filter that allows you to choose a subset of items in an array.

<!-- string, looks for a truthy key in item -->
<li ng-repeat="item in items | filter:shown">
<li ng-repeat="item in items | filter:!shown">

<!-- object, looks for specific keys in item -->
<li ng-repeat="item in items | filter:{size:'M'}">
<li ng-repeat="item in items | filter:{size:'S',priority:10}">

<!-- function, evaluate a function against the item -->
<li ng-repeat="item in items | filter:ctrl.showReady()">

Services

Angular has three types of services; factory, service and provider.

  • factory returns the service as is, good for defining "classes" and objects with helper functions that don't share state.
  • service calls new on the service before returning it, good for services that have internal state.
  • provider is service that requires startup configuration.
// Factory is a singleton as is.
angular.module('myApp').factory('MyService', [function() {
  var service = {}
  service.doStuff = function() { ... }
  return service
};
// Service is initialized with `new` and then served as a singleton.
angular.module('myApp').service('MyService', [
  function MyService() {
    this.doStuff = function() { ... };
  }
]);
// Provider singleton must be configured before use.
angular.module('myApp')
.provider('ItemService', function() {
  var haveDefaultItems = true;
  this.disableDefaultItems = function() { haveDefaultItems = false; };
  this.$get = [function() {
    var optItems = [];
    if (haveDefaultItems) {
      optItems = [1, 2, 3];
    }
    return new ItemService(optItems);
  }];
})
.config(['ItemServiceProvider', function(ItemServiceProvider) {
      var shouldHaveDefaults = false;
      if (!shouldHaveDefaults) {
        ItemServiceProvider.disableDefaultItems();
      }
  }])

Use standard Angular services as much as possible. They have great mocks available in the mock library.

$window = wrapper for the global window object.
$location = browser bar URL and router parameters.
            $location.path('/wherever')
            $location.search({id: 223})
$http = AJAX calls.
        $httpBackend.expectGET('/api/note').respond([{id: 1, label: 'Mock'}]);
        Expect variants enforce order and that was called.
        When variants only act as mock endpoints.
$timeout/$interval = setTimeout and setInterval

Use $timeout/$interval services instead of setTimeout/setInterval. The default ones don't allow Angular to keep track when to check for data updates.

Remove any $timeout/$interval services on $scope.$on('destroy', ...). Otherwise they get left in the background potentially causing errors.

Always use $http for your AJAX calls. Angular can't keep track of any other XHR calls so it would know when to check for updated models.

Always group and wrap $http calls in a service. ngResource service or custom service. Makes it easier to test and changed; e.g. adding validation layer later.

$http calls can be given timeout promise as an argument that acts as cancel when rejected. Useful for canceling searches and such.

Use HttpInterceptors for any shared HTTP request behavior. Authentication, logging, login request redirect, transform XML to JSON etc.

Use spies to mock services on controller tests.

spyOn(MyService, 'index').andReturn([{id:1}, {id:2}]);
...
expect(myService.list.callCount).toEqual(1);

Router

Use pre-route checks to check for access. They are defined using resolve parameter object to route definition. If any of the checks return a failed promise, Angular won't load the route.

Use ui-router for more complex layouts. Basic ngRouter only supports one view.

Modules

  • ngResource: for creating REST API wrappers.
  • ngCookies: make cookie values work like objects.
  • ngSanitize: sanitizes user input for e.g. live preview.
  • ngTouch: improved support for touch devices e.g. gestures.
  • ngAnimate: animations for common transitions.

Testing

Best testing runner for Angular is karma and you should pair it up with karma-jasmine and karma-chrome-launcher.

Controller test example:

describe('MyCtrl', function() {
  beforeEach(module('notesApp'));

  var ctrl;
  beforeEach(inject(function($controller) {
    ctrl = $controller('MyCtrl');
  }));

  it('should have highlight items based on state', function() {
    expect(ctrl.isComplete).toBeFalsy();
  });

});

Sources