angularjs-directive scope - Accessing attributes from an AngularJS directive




custom directives (3)

My AngularJS template contains some custom HTML syntax like:

<su-label tooltip="{{field.su_documentation}}">{{field.su_name}}</su-label>

I created a directive to process it:

.directive('suLabel', function() {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    scope: {
      title: '@tooltip'
    },
    template: '<label><a href="#" rel="tooltip" title="{{title}}" data-placement="right" ng-transclude></a></label>',
    link: function(scope, element, attrs) {
      if (attrs.tooltip) {
        element.addClass('tooltip-title');
      }
    },
  }
})

Everything works fine, at the exception of the attrs.tooltip expression, which always returns undefined, even though the tooltip attribute is visible from Google Chrome's JavaScript console when doing a console.log(attrs).

Any suggestion?

UPDATE: A solution was offered by Artem. It consisted in doing this:

link: function(scope, element, attrs) {
  attrs.$observe('tooltip', function(value) {
    if (value) {
      element.addClass('tooltip-title');
    }
  });
}

AngularJS + = bliss


Answers

See section Attributes from documentation on directives.

observing interpolated attributes: Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.


Although using '@' is more appropriate than using '=' for your particular scenario, sometimes I use '=' so that I don't have to remember to use attrs.$observe():

<su-label tooltip="field.su_documentation">{{field.su_name}}</su-label>

Directive:

myApp.directive('suLabel', function() {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: {
            title: '=tooltip'
        },
        template: '<label><a href="#" rel="tooltip" title="{{title}}" data-placement="right" ng-transclude></a></label>',
        link: function(scope, element, attrs) {
            if (scope.title) {
                element.addClass('tooltip-title');
            }
        },
    }
});

Fiddle.

With '=' we get two-way databinding, so care must be taken to ensure scope.title is not accidentally modified in the directive. The advantage is that during the linking phase, the local scope property (scope.title) is defined.


There are three ways scope can be added in the directive:

  1. Parent scope: This is the default scope inheritance.

The directive and its parent(controller/directive inside which it lies) scope is same. So any changes made to the scope variables inside directive are reflected in the parent controller as well. You don't need to specify this as it is the default.

  1. Child scope: directive creates a child scope which inherits from the parent scope if you specify the scope variable of the directive as true.

Here, if you change the scope variables inside directive, it won't reflect in the parent scope, but if you change the property of a scope variable, that is reflected in the parent scope, as you actually modified the scope variable of the parent.

Example,

app.directive("myDirective", function(){

    return {
        restrict: "EA",
        scope: true,
        link: function(element, scope, attrs){
            scope.somvar = "new value"; //doesnot reflect in the parent scope
            scope.someObj.someProp = "new value"; //reflects as someObj is of parent, we modified that but did not override.
        }
    };
});
  1. Isolated scope: This is used when you want to create the scope that does not inherit from the controller scope.

This happens when you are creating plugins as this makes the directive generic since it can be placed in any HTML and does not gets affected by its parent scope.

Now, if you don't want any interaction with the parent scope, then you can just specify scope as an empty object. like,

scope: {} //this does not interact with the parent scope in any way

Mostly this is not the case as we need some interaction with the parent scope, so we want some of the values/ changes to pass through. For this reason, we use:

1. "@"   (  Text binding / one-way binding )
2. "="   ( Direct model binding / two-way binding )
3. "&"   ( Behaviour binding / Method binding  )

@ means that the changes from the controller scope will be reflected in the directive scope but if you modify the value in the directive scope, the controller scope variable will not get affected.

@ always expects the mapped attribute to be an expression. This is very important; because to make the “@” prefix work, we need to wrap the attribute value inside {{}}.

= is bidirectional so if you change the variable in directive scope, the controller scope variable gets affected as well

& is used to bind controller scope method so that if needed we can call it from the directive

The advantage here is that the name of the variable need not be same in controller scope and directive scope.

Example, the directive scope has a variable "dirVar" which syncs with variable "contVar" of the controller scope. This gives a lot of power and generalization to the directive as one controller can sync with variable v1 while another controller using the same directive can ask dirVar to sync with variable v2.

Below is the example of usage:

The directive and controller are:

 var app = angular.module("app", []);
 app.controller("MainCtrl", function( $scope ){
    $scope.name = "Harry";
    $scope.color = "#333333";
    $scope.reverseName = function(){
     $scope.name = $scope.name.split("").reverse().join("");
    };
    $scope.randomColor = function(){
        $scope.color = '#'+Math.floor(Math.random()*16777215).toString(16);
    };
});
app.directive("myDirective", function(){
    return {
        restrict: "EA",
        scope: {
            name: "@",
            color: "=",
            reverse: "&"
        },
        link: function(element, scope, attrs){
           //do something like
           $scope.reverse(); 
          //calling the controllers function
        }
    };
});

And the html(note the differnce for @ and =):

<div my-directive
  class="directive"
  name="{{name}}"
  reverse="reverseName()"
  color="color" >
</div>

Here is a link to the blog which describes it nicely.







angularjs angularjs-directive