Friday, 23 May 2014

AngularJS Directives, Using Isolated Scope with Attributes

Directives in AngularJS are very powerful, but it takes some time to understand what processes lie behind them.
While creating directives, AngularJS allows you to create an isolated scope with some custom bindings to the parent scope. These bindings are specified by the attribute defined in HTML and the definition of the scope property in the directive definition object.
There are 3 types of binding options which are defined as prefixes in the scope property. The prefix is followed by the attribute name of HTML element. These types are as follows
  1. Text Binding (Prefix: @)
  2. One-way Binding (Prefix: &)
  3. Two-way Binding (Prefix: =)
Let's see how these are defined in directives, and I'll go into details one by one.
JS:
angular.module("myApp",[])  
  .directive("myDirective", function () {
    return {
      restrict: "A",
      scope: {
        text: "@myText",
        twoWayBind: "=myTwoWayBind",
        oneWayBind: "&myOneWayBind"
      }
    };
  }).controller("myController", function ($scope) {
    $scope.foo = {name: "Umur"};
    $scope.bar = "qwe";
});
HTML:
<div ng-controller="myController">  
  <div my-directive
    my-text="hello {{ bar }}"
    my-two-way-bind="foo"
    my-one-way-bind="bar">
  </div>
</div>  

1. Text Binding

Text bindings are prefixed with @, and they are always strings. Whatever you write as attribute value, it will be parsed and returned as strings. Which means anything inside curly braces {{ }}, will reflect the value.
So, if we were to define $scope.username = "Umur", and define the attribute like , the value in the directive scope is going to be hello Umur. This value would be updated in each digest cycle.

2. One-way Binding

One-way bindings are prefixed with & and they can be of any type. They are going be defined as getter functions in the directive scope. See it with example:
Controller:
/* more code */
$scope.someObject = { name:'Umur', id:1 };
/* more code */
HTML:
<my-directive my-attribute="someObject" />
Directive:
{
  scope: {myValue: "&myAttribute"},
  link: function (scope, iElm, iAttrs) {
    var x = scope.myValue();
    // x == {name:"Umur", id:1}
  }
}
Since they are getter functions, they are read-only, any changes to the value will not propagated to higher scopes.

3. Two-way Bindings

Two-way bindings are prefixed by = and can be of any type. These work like actual bindings, any changes to a bound value will be reflected in everywhere.
Let's see it by example:
Controller:
/* more code */
$scope.someObject = { name:'Umur', id:1 };
/* more code */
HTML:
<my-directive my-attribute="someObject" />  
Directive:
{
  scope: {myValue: "=myAtrribute" },
  link: function (scope, iElm, iAttrs) {
    var x = scope.myValue.name;
    // x == "Umur";
    scope.myValue.name = "Kieslowski";
    // if we go to parent scope (controller's scope, in this example)
    // $scope.someObject.name == "Kieslowski";
    // would be true
  }
}

Summary with Code

JS:
angular.module("myApp", [])  
  .directive("myDirective", function () {
    return {
      restrict: "A",
      scope: {
        text: "@myText",
        twoWayBind: "=myTwoWayBind",
        oneWayBind: "&myOneWayBind"
      }
    };
}).controller("myController", function ($scope) {
  $scope.foo = {name: "Umur"};
  $scope.bar = "qwe";
});
HTML:
<div ng-controller="myController">  
  <div my-directive 
       my-text="hello {{ bar }}" 
       my-two-way-bind="foo" 
       my-one-way-bind="bar">
  </div>
</div>  
Results:
/* Directive scope */

in: $scope.text  
out: "hello qwe"  
// this would automatically update the changes of value in digest
// this is always string as dom attributes values are always strings

in: $scope.twoWayBind  
out: {name:"Umur"}  
// this would automatically update the changes of value in digest
// changes in this will be reflected in parent scope

// in directive's scope
in: $scope.twoWayBind.name = "John"

//in parent scope
in: $scope.foo.name  
out: "John"

in: $scope.oneWayBind() // notice the function call, this binding is read only  
out: "qwe"  
// any changes here will not reflect in parent, as this only a getter

Advanced Angular: $parse

If you want to step up in your AngularJS knowledge, $parse is one of the most important services that you should know about. It is used in most of the directives, and opens up your imagination to a new set of possibilities.
So, what does it do? Let's start with a place we all well know: ngClick.
ngClick directive, takes an expression, and executes the expression when the directive element is clicked. So, how does it work internally? Yep, you guessed it: with $parse.
$parse takes an expression, and returns you a function. When you call the returned function with context (more on that later) as first argument. It will execute the expression with the given context.
It will execute the expression with the given context.
Context is a pure javascript object, as far as $parse is concerned. Everything in the expression will be run on this object.
Everything in the expression will be run on this object.
Let's see it with an example:
function MyService($parse) {  
    var context = {
        author: { name: 'Umur'},
        title: '$parse Service',
        doSomething: function (something) {
            alert(something);
        }
    };
    var parsedAuthorNameFn = $parse('author.name');
    var parsedTitleFn = $parse('title');
    var parsedDoSomethingFn = $parse('doSomething(author.name)');

    var authorName = parsedAuthorNameFn(context);
    // = 'Umur'
    var parsedTitle = parsedTitleFn(context);
    // = '$parse Service'
    var parsedDoSomething = parsedDoSomethingFn(context);
    // shows you an alert 'Umur'
}
So this is very cool, we can evaluate strings with a context safely. Let's write a very basic myClick directive.
angular.module('my-module', [])  
    .directive('myClick', function ($parse) {
        return {
            link: function (scope, elm, attrs) {
                var onClick = $parse(attrs.myClick);
                elm.on('click', function (e){
                    // The event originated outside of angular,
                    // We need to call $apply
                    scope.$apply(function () {
                        onClick(scope);
                    });
                });
            }
        }
    });
See, the pure javascript object turns out to the our scope!
This works, but if you look at the docs of ngClick, it lets us to pass $event object to the function. How does that happen? It is because the parsed function acceptes an optional second argument for additional context.
We have access to event object in the click callback, and we can just pass this through.
angular.module('my-module', [])  
    .directive('myClick', function ($parse) {
        return {
            link: function (scope, elm, attrs) {
                var onClick = $parse(attrs.myClick);
                elm.on('click', function (e){
                    // The event originated outside of angular,
                    // We need to call $apply
                    scope.$apply(function () {
                        onClick(scope, {$event: e});
                    });
                });
            }
        }
    });
And the usage:
link  
That's it. You can now make your own directives with $parse.

Bonus

If you don't need to pass additional context, you can save some bytes and remove code of the code. Here is a way to do it cooler. How does it work exercise it left to the reader. Please leave a comment if you think you've found the answer!
angular.module('my-module', [])  
    .directive('myClick', function ($parse) {
        return {
            link: function (scope, elm, attrs) {
                var onClick = $parse(attrs.myClick);
                elm.on('click', function (e) {
                    scope.$apply(onClick);
                });
            }
        }
    });