barbarian meets coding

WebDev, UX & a Pinch of Fantasy

AngularJS Wiki

This article is part of my personal wiki where I write personal notes while I am learning new technologies. You are welcome to use it for your own learning!

Angular.js is an opinionated client-side MVC framework specially suited to develop highly interactive web application. Angular:

  • is open source,
  • very comprehensive (offers support for two-way data binding, DI of services, networking, routing, etc)
  • fully testable (see karma)
  • lets you extend HTML [with new attributes and elements] through the use of directives.
  • supports future web technologies like web components and object.observe

Introduction to Angular

Angular Architecture

  • Two way data binding
  • Dirty checking
  • Dependency Injection

Angular Components

  • Models
  • Services
  • Controllers
  • Views(Directives)

Getting Started with Angular: The Angular Seed

The Angular team provides a template project known as the Angular seed to be used as a starting point for a new Angular.js application. This project is an ideal starting point to begin using Angular.

AngularJS Modules

A module in AngularJS is used to group different parts of an Angular app like controllers, services, directives, etc. Modules represent independent parts of the application that can make use of other modules to access new functionality. You can compare AngularJS modules with .NET assemblies.

You can define a module by using the angular.module function. For instance, an Angular app has typically one root module used to bootstrap the app:

1
var myApp = angular.module("myApp", /* dependencies */ []);

You can add AngularJS components to a module by using different methods within the module object. For instance, to add a controller to a module you use the app.controller method:

1
2
3
4
// add controller
myApp.controller('MyController', function($scope){});
// add service
myApp.service('MyService', function($service){});

The ng-app Directive

The ng-app directive is the entry point to your AngularJS application and should point to the boostrap module:

1
2
3
4
<html ng-app="myApp">
<head>...</head>
<body>...</body>
</html>

AngularJS Controllers and the Scope

Controllers in AngularJS work much in the same way that they work in any MVC framework. They work as an intermediary between the domain model and the views. They perform operations on data of the domain model and prepare it for a given view, they create a scope to expose data to the view. The view in its turn can bind to any piece of data exposed via the scope object.

The ng-controller Directive

The ng-controller directive lets you bind a view to a given controller, that is, a fragment of HTML code to a controller and its scope. For instance:

1
2
3
4
5
6
myApp.controller('MyController', function($scope){
  $scope.barbarian = {
     name: "conan",
     avatar: "img/conan.jpg"
  };
});
1
2
3
4
5
6
<div ng-controller="MyController">
  <h1></h1>
  <!-- note the use of ng-src to delay the request for an image until the binding is resolved -->
  <!-- when the binding is resolved then the src will be filled with the image url -->
  <img ng-src="">
</div>

The ng-repeat directive

You can use the ng-repeat directive to repeat a given html template for every item within an array. For instance:

1
2
3
4
5
6
7
myApp.controller('MyController', function($scope){
  $scope.barbarian = {
     name: "conan",
     avatar: "img/conan.jpg",
     inventory: [{name: "sword"}, {name: "shield"}, {name: "potion of health"}]
  };
});
1
2
3
4
5
6
7
8
9
10
11
12
<div ng-controller="MyController">
  <h1></h1>
  <!-- note the use of ng-src to delay the request for an image until the binding is resolved -->
  <!-- when the binding is resolved then the src will be filled with the image url -->
  <img ng-src="">
  <h2>Inventory</h2>
  <ul>
    <li ng-repeat="inv in barbarian.inventory">
      
    </li>
  </ul>
</div>

Handling Events

AngularJS provides numerous directives to handle DOM events. For instance:

  • ng-click to handle a click event
1
2
3
4
5
6
7
8
9
10
11
12
myApp.controller('MyController', function($scope){
  $scope.barbarian = {
     name: "conan",
     avatar: "img/conan.jpg",
     inventory: [{name: "sword"}, {name: "shield"}, {name: "potion of health"}]
  };

  $scope.sellItem = function(item){
    var idx = $scope.barbarian.inventory.indexOf(item);
    if (idx > 0) $scope.barbarian.inventory.splice(idx, 1);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
<div ng-controller="MyController">
  <h1></h1>
  <!-- note the use of ng-src to delay the request for an image until the binding is resolved -->
  <!-- when the binding is resolved then the src will be filled with the image url -->
  <img ng-src="">
  <h2>Inventory</h2>
  <ul>
    <li ng-repeat="item in barbarian.inventory">
      
      <button ng-click="sellItem(item)"></button>
    </li>
  </ul>
</div>

AngularJS Built-in Directives

Directives are the way Angular extends HTML with new behavior and functionality. You can create directives that are either elements, attributes, CSS classes and even comments.

  • Event built-in directives let you handle DOM events
    • ngClick, ngDblClick
    • ngMousedown, ngMouseup, ngMouseover, etc
    • ngChange (requires the ngModel directive within the same element)
  • ngApp represents the entry point of your Angular app
  • ngBind tells angular to replace the text content of an HTML element with the value of a given expression f.i. ng-bind="barbarian.name". Typically you don’t use ng-bind="expression" directly but `. It is preferable to useng-bindinstead of ```` if a template is momentarily displayed by the browser in its raw state before Angular compile it; sinceng-bindis an attribute it is not visible while a page is loading. (An alternative solution to this would be to useng-cloak`)
  • ngBindTemplate tells angular to replace the text content of an HTML element with the interpolation of a given template f.i. ng-bind-template=" is fierce and weights "
  • ngBindHtml (part of the angular-sanitize module) evaluates an expression and inserts the resulting HTML into the element in a secure way (sanitized using the $sanitize service). For instance: ng-bind-html="snippetWithUnsafeHtml"
  • ngBindHtmlUnsafe like above but without the sanitization step so you can use it on markup that you trust
  • ngShow and ngHide let you show and hide parts of the DOM based on expressions
  • ngCloak allows you to hide portions of your page (or the whole page) while angular is loading to avoid showing portions of the DOM that haven’t been evaluated by Angular yet.
  • ngStyle lets you style elements dynamically via objects that describe CSS properties.
  • *ng-class lets you add classes dynamically by binding an expression that represents all classes to be added
  • ngClassEven and ngClassOdd work with ng-repeat to provide different styles to odd/even elements of an array when rendered in the DOM.

Tips About How to Use Controllers

Don’t manipulate the DOM in controllers, use directives for that, a controller should limit itself to being an intermediary between the domain model and the view. Just populate the $scope and expose the data you need.

AngularJS Services

A service in Angular is used as a way to encapsulate reusable business logic that can then be injected as a collaborator in controllers or other services.

Using services within your Angular applications will result in a clearer, more organized application with smaller, reusable and testable components.

Creating Your Own Services

You can create a service using the service or the factory functions. Note that, because services are singletons is important that they are stateless to avoid coupling different parts of your application with shared state. The recommended ways to declare and register services looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// service
angular
    .module('app')
    .service('logger', logger);

// returns a constructor function
// Angular will transparently call *new*
// on this so you don't have to
function logger() {
  this.logError = function(msg) {
    /* */
  };
}

// factory
angular
    .module('app')
    .factory('logger', logger);

// returns a factory function
function logger() {
    return {
        logError: function(msg) {
          /* */
        }
   };
}

Once registered like this a service can be used by other angular components such as controllers or other services:

1
2
3
app.controller('MyController', function MyController($scope, logger){
    // you can start using the logger service right here
})

Angular Built-in Services

Angular comes with a host of built-in services which can be easily identified by the $ character that precedes them like $scope. This section will briefly describe some of these built-in services

The $http and $q Services

You can use the $http service to make HTTP requests to any server via the browser XMLHttpRequest object and JSONP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app.factory('someService', function($http, $log)){
  return {
    get: function(onSuccessCallback, onErrorCallback){
        // you can use $http directly as a method
        $http({method: 'GET', url: '/data/someData.json'})
          .success(function(data, status, headers, config){
            $log.info(data, status, headers, config);
            onSuccessCallback(data)
          })
          .error(function(data, status, headers, config){
            $log.warn(data, status, headers, config);
            onErrorCallback();
          })
    }
  }
}

// when using this service from other 
// component you would need to pass callbacks
someService.get(/*sucess*/ function(data) {
    // do something with data
}, /* error */ function (){
    // oh no! There was an error!
})

We can also leverage the $q service to avoid the need of using callbacks when handling http requests or any other asynchronous operation. The $q service allows you to create promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
app.factory('someService', function($http, $q)){
  return {
    get: function(){
        var deferred = $q.defer();
        // you can use $http directly as a method
        $http({method: 'GET', url: '/data/someData.json'})
          .success(function(data, status, headers, config){
            deferred.resolve(data);
          })
          .error(function(data, status, headers, config){
            deferred.reject(status);
          })
        // return a promise
        return deferred.promise;
    }
  }
}

// we can use this service from another component
// without the need of callbacks by taking
// advantage of the returned promise:
someService.get()
  .then(function(data){
     // use something with data FTW!!!
  })
  .error(function(statusCode){
     // oh no! An error!!!
});

Note how we used the also built in $log service to log information in a similar fashion to console.log in the browser.

For more detailed information about these services refer to the AngularJS documentation: * $http * $log * $q

The $resource Service for RESTful Services

You can use the resource service to consume RESTful APIs. In order to start using it within your application, include the ngResource (angular-resource) module within your application and use it as detailed in the example below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.factory('someResfulServiceFacade', function($resource)){
  return {
    get: function(id){
      return $resource('/api/resource/:id', {id: '@id'})
        .get({id: id});
    }
  }
}

// and you can use it like this:
var resource = someRestfulServiceFacade.get(1);
// this is async and works sort of like a promise 

// or like an actual promise like this
// note how we explicitely get a promise via $promise
someRestfulServiceFacade.get()
    .$promise
    .then(/* success */ function(resource){// this resource object has a Resource type that wraps the server response}
          /* error */ function(response){});

The $resource service provides a simple API to support all RESTful related HTTP verbs via get(GET), save(POST), query(complex GET) and delete(DELETE), etc. See AngularJS documentation for more information

Control Page Scrolling with $anchorScroll Service

The $anchorScroll service allows you send the page scroll within a page to the current hash on the browser URL ($location.hash). It works like this:

1
2
3
4
5
app.controller('MyController', function($scope, $anchorScroll){
  $scope.scrollToAnchor = function() {
    $anchorScroll(); // scrolls to anchor set in browser url ($location.hash)
  };
});

For more informarion refer to the AngularJS docs;

The $cacheFactory Service

The $cacheFactory service allows you to construct caches and provides access to them. I assume you can use it as a way to share information within an AngularJS application. You can use it like this:

1
2
3
4
5
6
app.controller('MyController', function($scope, $cacheFactory){
  var myCache = $cacheFactory('myCache');
  $scope.addToCache = function(key, value){ myCache.put(key,value);};
  $scope.readFromCache = function(key){ myCache.get(key);};
  $scope.getCacheStats = function(){ myCache.info();};
})

You can find more information on the docs.

Binding on the Fly With The $compile Service

The $compile service is used heavilily internally by AngularJS to look for directives within a page and process them. Additionally, Angular exposes this service so you can use it with custom directives or whenever you want to bind markup to data on-the-fly. For instance:

1
2
3
4
5
6
7
8
9
10
app.controller('MyController', function($scope, $compile) {

    $scope.name = "Jaime";
    $scope.appendHelloWorld = function(){
        // 1. compile template
        // 2. link template to scope
        // 3. append to body
        return $compile('<p>Hello World </p>')($scope).appendTo(angular.element('body'));
    }
})

In better words, $compile lets you compile HTML markup or DOM elemens into a template, that can be later linked(bound) to a scope and displayed to the user.

For more information about $compile, visit Angular documentation;

The $parse Service

The $parse service like the $compile service is used internally by AngularJS. You can use $parse to turn any javascript expression into a function that can be then called within a given context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// parsing a simple expression
var addNumbers = $parse('1 + 1');
console.log(fn())
// => 2

// parsing a expression and evaluating it within a context
var getName = $parse('person.name');
var context1 = { person : { name: 'Jaime'}};
var context2 = { person : { name: 'Conan'}};

console.log(getName(context1));
// => 'Jaime'
console.log(getName(context2));
// => 'Conan'

// the second argument represents the 'local' context
// which takes precedence over the other context
console.log(getName(context2, context1));
// => 'Jaime'

// you can also assign to an expression (if it is supported)
var setName = getName.assign;
setName(context1, 'Not Jaime');
console.log(context1.person.name);
// => 'Not Jaime'

To learn more about the $parse service look at the Angular docs.

The $locale Service

You can use the $locale service to define formats to localize dates and numbers. If you have a date bound to an element with a format:

1
<h1></h1>
1
2
$scope.myDate = Date.now();
$scope.myFormat = $locale.DATETIME_FORMATS.shortDate;

In order to use other locales for other countries you will need to import the different localization modules.

To learn more about this service and the different options it provides go look into the documentation for the service and for internationalization

The $timeout Service

The $timeout service is very similar to JavaScript’s setTimeout function, but with the different that AngularJS can track changes produced by the timeout callback (otherwise the UI will not be refreshed until another part of the $scope changes):

1
2
3
4
5
6
7
8
var promise = $timeout(function(){
  // do something
  $scope.name = 'Jaime';
}, 2000);

// you can use the promise to cancel the execution of the
// timeout callback
$timeout.cancel(promise);

The $exceptionHandler Service

The $exceptionHandler service provides you a hook to handle AngularJS exceptions. We can override it to take control of exceptions that occurr in AngularJS ourselves:

1
2
3
4
5
app.factory('$exceptionHandler', function(){
  return function(exception){
    console.log("exception handled:", exception);
  };
});

For more information about this service look into the docs.

The $filter Service

The $filter service allows you access any filters that you yourself have defined and all built-in filters in AngularJS. So for intance if we have the following filter somewhere within our app:

1
2
3
4
5
6
7
8
9
10
11
app.filter('sizes', function(){
  return function(size){
    var sizes = {
      'S': 'Small',
      'M': 'Medium',
      'L': 'Large',
      'XL': 'Extra Large'
    };
    return sizes[size];
  }
})

You can get a hand of the filter by using the $filter directive and use the filter programmatically:

1
2
3
4
5
6
7
8
9
10
11
12
app.controller('MyController', function($scope, $filter){
  var sizes = $filter('sizes');
  console.log(sizes('S'));
  // => 'Small'
});

// or you can directly inject the sizes filter by using the 
// following convention <filterName>Filter
app.controller('MyController', function($scope, sizesFilter){
  console.log(sizesFilter('S'));
  // => 'Small'
});

For more information about the $filter service go to the AngularJS docs

Handle Your Cookies with the $cookieStore service

The $cookieStore service – within the ngCookies module – lets you easily manage cookies within your web application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#1. Remember to add module dependencies
var app = angular.module('myApp', ['ngCookies']);

#2. Use it!
app.controller('MyCookiesController', function($scope, $cookieStore){
  $scope.person = {name: 'Jaime'};
  $scope.savePersonToCookie = function(person){
    $cookieStore.put('person', person);
  };
  $scope.setPersonFromCookie = function(){
    var person = $cookieStore.get('person');
    console.log(person);
    $scope.person = person;
  };
  $scope.removeCookie = function(){
    $cookieStore.remove('person');
  };
});

You can find more information about the $cookieStore in the AngularJS docs.

Other Services

  • $interpolate: compiles a string with markup (stuff with moustaches my) into an interpolation function.
  • $interpolateProvider: used to configure interpolation. You can change interpolation characters for instance from moustaches to brackets. Useful to fix conflicts between libraries.
  • $log: Used for diagnostic logging and debugging. Provides methods log, info, warn and error.
  • $rootScope: Used behind the scenes. There is one single root scope per angular application. It is used as a prototype for creating any $scope of the application. More info on scopes.
  • DOM-related Services
  • $window
  • $document
  • $rootElement: provides access to the element that is decorated with the ng-app directive.

AngularJS Routing

AngularJS routing allows you to provide a way to navigate within the different views of your single page application. Using the routing APIs you will be able to define URL routes that will map to the views within your app.

In order to start using routing you will need to have an html ng-view element that will act as a placeholder for the views:

1
2
3
4
5
<body>
...
<ng-view></ng-view>
...
</body>

The routing system not only allows you to map URLs to views but compose your entire single page application by composing controllers with templates to produce views. You will need to add ngRoute module (angular-route.js) and configure your routes like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var myApp = angular.module('myApp', ['ngRoute'])
  .config(function($routeProvider)){
    $routeProvider.when('/speakers', {
      templateUrl: 'templates/speakers.html',
      controller: 'SpeakersCtrl'
    });
    $routeProvider.when('/sessions', {
      templateUrl: 'templates/sessions.html',
      controller: 'SessionsCtrl'
    });
    // You can also add routes with parameters
    $routeProvider.when(/sessions/:sessionId, {
      templateUrl: 'template/session.html' ,
      controller: 'SessionCtrl'
    })
    // and add the default route to redirect the user to
    // if a route is not found
    $routeProvider.otherwise({redirectTo:'/sessions'});

  });

After configuring a route, you will be able to reach it via an a tag as illustrated below:

1
2
3
4
5
<a href="#/speakers">speakers</a>
<a href="#/sessions">sessions</a>

...
<a href="#/session/11">My Awesome Session</a>

Additionally, the routing engine within AngularJS will keep your browser history as the user would expect (whenever you navigate within your app and then click on the back button, the browser goes to the previous view within your Angular app instead of getting out of your site).

In the previous example I used a :sessionId in one of the routes that can be used as a parameter. In order to extract this route parameter from your route within a controller you can use the $routeParams service:

1
2
3
4
myApp.controller('SessionCtrl', function($scope, sessionsRepo, $routeParams){
  $scope.session = sessionsRepo.getSession($routeParams.sessionId);
  // etc
});

The $route Service

In addition to what we have seen so far, we can define custom parameters within our map of routes:

1
2
3
4
5
6
7
8
9
10
11
12
var myApp = angular.module('myApp', ['ngRoute'])
  .config(function($routeProvider)){
    $routeProvider.when('/speakers', {
      templateUrl: 'templates/speakers.html',
      controller: 'SpeakersCtrl'
    });
    $routeProvider.when('/speaker/:speakerId', {
      greetings: 'hello world',
      templateUrl: 'templates/speakers.html',
      controller: 'SpeakerCtrl'
    });
  });

These and other parameters from the route can be accessed in any controller with the help of the $route service:

1
2
3
4
5
6
7
8
9
10
myApp.controller('SpeakerCtrl', function($scope, $route){
  $scope.greeting = $route.current.greetings;
  // to get params from the query string -> /speaker/1/?showBio=true
  console.log($route.current.params.showBio); // => 'true'
  // it also works with route params :param
  console.log($route.current.params.speakerId); // => '1'
  // We can also use pathParams
  // but this only works for parameters that are path of the route
  console.log($route.current.pathParams.speakerId); // => '1' 
});

Reloading a View

Sometimes you’ll want to reload a view without reloading your entire app. If you want to do that the $route service provides the reload method that you can use for this very purpose.

Enabling HTML5 Routing

You can configure your routing to avoid the need of using the hashtag # by using the $locationProvider service:

1
2
3
4
5
6
7
8
9
var myApp = angular.module('myApp', ['ngRoute'])
  .config(function($routeProvider, $locationProvider)){
    $routeProvider.when('/speakers', {
      templateUrl: 'templates/speakers.html',
      controller: 'SpeakersCtrl'
    });
    // routes...
    $locationProvider.html5Mode(true);
  });

This will allow you to use normal urls instead of hashtag prefixed urls for deep linking within your app:

1
<a href="/sessions">Sessions</a>

AngularJS will use this approach within browsers that support HTML5 routes and will include the hashtag with browsers that do not support this style of routing. Important: It is worthy of note that if you save a url with a deep link and then try to navigate to it directly (instead of reaching it from within the angular app via a link tag) the browser will make a GET request to your server with that very url (e.g. myapp.com/sessions), in that case you will need to configure your server to return the root html file instead of the partial template.

You can find more information about this service in AngularJS documentation.

Dynamic Templates

Instead of specifying a url for a template you can pass any string within the template property. This allows you to generate templates on the fly:

1
2
3
4
5
6
7
8
var myApp = angular.module('myApp', ['ngRoute'])
  .config(function($routeProvider)){
    $routeProvider.when('/speakers', {
      // you could generate this on the fly via a service
      template: '<h1>Speakers</h1>',
      controller: 'SpeakersCtrl'
    });
  });

Avoid Partial View Load With Long Running Requests

If you experience a problem when loading a view because the associated JSON data that is going to be bound to a template takes a while, and thus an unpopulated view appears to the user you can use the resolve routing option. If so, the resolve option must contain a function that returns a promise. AngularJS will wait for the promise to be completed before rendering the view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myApp = angular.module('myApp', ['ngRoute'], function($routeProvider){
  $routeProvider.when('/session/:sessionId', {
    templateUrl: 'templates/session.html',
    controller: 'SessionCtrl',
    resolve: {
      session : function($route, sessionsRepo)  {
        return sessionsRepo.get($route.current.params.sessionId).$promise;
      }
    }
  });
});

// the data can be accessed in the controller via the $route service:
myApp.controller('SessionCtrl', function($scope, $route){
  // ... 
  $scope.session = $route.current.locals.session;
});

The $location Service

The $location service allows you to navigate within your angular app in a programmatic fashion, that is, from code instead of using link tags. The $location service provides access to the URL that is currently being displayed in the browser a is linked to it so that changes done via the $location service are also reflected in the browser URL.

1
2
3
4
5
6
7
8
9
myApp.controller('someController', function($scope, $location){
  // go to sessions
  $location.url('/sessions');
  // provides bunch of info
  console.log('absUrl: ', $location.absUrl()); // absolute url
  console.log('url: ', $location.url()); // relative url
  console.log('protocol: ', $location.protocol());
  // port, host, path, search, hash, etc...
});

The $location service also allows you to switch to another url without putting it in the browser history. You can do that by calling the $location.replace() method before changing the url.

For more information regarding the $location service refer to the AngularJS docs.

Creating Custom Directives

AngularJS allows you to create custom HTML tags with an associated look and feel and behavior via custom directives. Additionally you can use directive to create custom evens (like ng-click) or to observe and react to changes in the model. This allows you to create a domain specific language over HTML and be more intentional about the specific domain of the web application that you are developing. AngularJS directives are heavily inspired by the Web components standard that is under development.

You can create a new directive by using the directive method. Look closely at the different examples because they use different options and provide a little advice as to when and why:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
myApp.directive('myDirective', function($compile){
  // return the directive definition object
  return {
    restrict: 'E', // restrict directive to 'E'lements (by default it is 'A'ttribute, it can also be 'C' for class, 'M' for comment, etc)
    link: function (scope, element, attrs, controller){
      var markup = "<input type='text' ng-model='name'> <br/>";
      var compiledMarkup = $compile(markup); // compile markup
      var boundMarkup = compiledMarkup($scope); // apply scope to markup
      angular.element(element).html(boundMarkup);
    }
  };
});

// this is equivalent to:

myApp.directive('myDirective', function($compile){
  // return the directive definition object
  return {
    restrict: 'E', // restrict directive to 'E'lements (by default it is 'A'ttribute, it can also be 'C' for class, 'M' for comment, etc)
    template: "<input type='text' ng-model='name'> <br/>"
  };
});

// you can also point the template to a URL like this:
myApp.directive('myDirective', function($compile){
  // return the directive definition object
  return {
    restrict: 'E',
    templateUrl: '/templates/directives/myDirective.html'
  };
});

// you can use the replace property 
// to generate valid HTML (replace the whole directive element for valid markup)
myApp.directive('myDirective', function($compile){
  // return the directive definition object
  return {
    restrict: 'E',
    replace: true,
    templateUrl: '/templates/directives/myDirective.html'
  };
});

You can then use it in your html:

1
<my-directive/>

As you can anticipate, this allows you to create multiple directives to build a very intentional domain specific markup:

1
2
3
4
5
6
7
8
9
<h1>Sessions</h1>
<ul ng-repeat="session in sessions">
  <session></session>
</ul>
...
<h1>Speakers</h1>
<ul ng-repeat="speaker in speakers">
  <speaker></speaker>
</ul>

Using a Specific Scope Within a Directive

To this point we have been using the same $scope as the one in which the html directive is placed. This is not what you want to do since it will make the directive not reusable, that is, the directive depends on outer scope it has no control over. Moreover, it may couple multiple directives together (changing binding sources in one directive may affect other directives). Instead we can define a separate scope for the directive:

1
2
3
4
5
6
7
8
9
myApp.directive('myDirective', function($compile){
  // return the directive definition object
  return {
    restrict: 'E',
    templateUrl: '/templates/directives/myDirective.html',
    scope: { // now the directive has its own isolated scope
    }
  };
});

Once you isolate the scope of a directive it becomes necessary to be able to pass data to the directive in some way, you can do this via html attributes that can be bound to the scope declared within the directive. For instance, if we have an element directive with an session attribute:

1
<session-details session="session" another-parameter="cheese"/>

We can bind that argument to the directive scope by declaring the session variable in the scope property of the directive definition object with a value of =session (or = if we are using the same name in the scope and in the attribute):

1
2
3
4
5
6
7
8
9
10
11
myApp.directive('sessionDetails', function($compile){
  // return the directive definition object
  return {
    restrict: 'E',
    templateUrl: '/templates/directives/sessionDetails.html',
    scope: {
      session: '=session' // we can also use the shorcut '=' since it is the same name 'session'
      anotherParameter: '=anotherParameter' // note how another-parameter becomes anotherParameter
    }
  };
});

In addition to binding properties with '=' to our scope we can use '&' and '@' as described in the example below:

1
2
3
4
5
<upvote upvote="session.upvote()" downvote="session.downvote()" count=""/>

<!-- if we were to use count with '='
<upvote upvote="session.upvote()" downvote="session.downvote()" count="session.voteCount"/>
-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
myApp.directive('upvote', function(){
  return {
    restrict: 'E',
    template: '/templates/directives/upvote.html',
    scope: {
      // the '&' means that we expect to bind this scope property
      // to a function that we will execute within the context
      // of the **outer** scope instead of the directive scope
      upvote: '&',
      downvote: '&',
      // the '@' means that we expect to bind this scope property
      // to a string. If we want to bind this property of the inner
      // scope with a property of the outer scope then we will need
      // to evaluate it in the html attribute e.g. 
      count: '@',
      // count: '=' // this would bind the property value from outer to inner scope
    }
  };
});

Handling Events With Directives

In addition to create directives to create our own custom HTML elements with a specific view and behavior, we can create directives that handle events and inject behavior in other existing elements.

1
...
1
...

Transclusion

Transclusion is the act of embedding html within a directive and injecting this html within the template of the directive. For instance:

1
2
3
4
<collapsible title="">
  <h6></h6>
  <p></p>
</collapsible>

Note how that inner html gets injected in the body of the directive template by using the ng-transclude directive:

1
2
3
4
5
6
7
8
9
10
11
myApp.directive('collapsible', function(){
  restrict: 'E',
  replace: true,
  template: '<div><h4 class="title" ng-click="toggleVisibility"></h4><div ng-transclude ng-show="visible"></div></div>',
  transclude: true
  scope: { 'title': '@'},
  controller: function($scope) {
     $scope.visible = true;
     $scope.toggleVisibility = function(){$scope.visible = !$scope.visible;};
  }
});

It is very important to note that transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside. So the transcluded content will use the outer scope and not the scope within the directive (if it has one).

The transclude option changes the way scopes are nested. It makes it so that the contents of a transcluded directive have whatever scope is outside the directive, rather than whatever scope is on the inside. In doing so, it gives the contents access to the outside scope.

This behavior makes sense for a directive that wraps some content, because otherwise you’d have to pass in each model you wanted to use separately. If you have to pass in each model that you want to use, then you can’t really have arbitrary contents, can you?

For more information about transclusion visit the AngularJS documentation for ngTransclude and the directives documentation.

Tips and Tricks

Avoiding Clipping behavior When a Page First Renders

Some times you will notice that the first time the page renders for your angular app you will get a clipping/flicker behavior. In order to avoid that you can use the ng-cloak directive:

The ngCloak directive is used to prevent the Angular html template from being briefly displayed by the browser in its raw (uncompiled) form while your application is loading. Use this directive to avoid the undesirable flicker effect caused by the html template display.

1
<div ng-cloak>...</div>

You can find more information about ng-cloak in the AngularJS docs.

Writing Clean Code in AngularJS

Separation of Concerns

Separating concerns within your application will make it easier to maintain and extend, you’ll be able to reuse more code and it will be easier to test (since it is easier to test parts of code that are independent from each other). In summary, separation of concerns lets you minimize the complexity of your application by creating components that do one thing and one thing only.

AngularJS is built with separation of concerns in mind but you can still violate the separation of concerns principle and unnecesarily increase the complexity of your applications. In order to follow the SoC principle you should:

  • Use controllers…

More Best Practices

Reference

Comments