Thursday, 26 June 2014

Watching Attribute Value with $observe

Below is an example of how to watch for an attribute's value and do something every time it has changed. To do this we would use $observe function.

<!-- origin markup in the view -->
enter a colour: <input class="observe-test" data-ng-model="inputValue" title="{{inputValue}}"/> 
<br />
value typed: <span></span>

// directive codes
myApp.directive('observeTest', function () {
    return {
        restrict: 'C',
        link: function (scope, element, attrs) {
            attrs.$observe('title', function (newValue) {
              attrs.$set('style', 'color:' + newValue);
              element.parent().find('span').text(newValue);
            })
        }
    }
});

See the example in Plunker

Monday, 23 June 2014

More Advanced AngularJS Directive

Isolate Scope
scope option is useful for creating directive's internal variables or functions from outer values or functions.
<!-- origin markup in the view -->
<scopetest a-value="controllerData" the-value="{{controllerData}}" show-value="showPopup()"></scopetest>

// these codes are inside the controller
$scope.controllerData = 'Oii';
$scope.showPopup = function () {
    alert($scope.controllerData);
}

// directive codes
myApp.directive('scopetest', function () {  // would work as well if I had used myApp.directive('showValue' . . . restrict: 'A'
    return {
        restrict: 'E',
        template: "<p>controllerData: {{controllerData}}</p> <p>data: {{data}}</p> <p>theData: {{theData}}</p> <p>sameData: {{sameData}}</p> <button type='button' ng-click='showData()'>Click Me!</button>",
        scope: {
            data: "=aValue",
            theData: "@theValue",
            sameData: "&aValue",    // doesn't work. Seems only work for method.
            showData: "&showValue"
        }
    }
})

See the example in Plunker

In this example we use '=' to pass the value of an outer variable specified in an attribute in the origin markup. I.e., data: "=aValue" will create an internal variable called 'data' and get the value of the outer variable (an AngularJs $scope variable) specified in the 'a-value' attribute in the origin markup. The attribute name referred to follows the same matching pattern as the matching pattern of a directive name (i.e, 'aValue' matches 'a-value' in the origin markup).

'@' is used if we want to copy the literal value (copy the text) of an attribute in the origin markup. If we want to pass a value of an AngularJs variable then we need to use {{ . . . }} to get the value first before passing it to the directive. I.e., <scopetest the-value="{{controllerData}}" />

'&' is used to pass a function of an attribute in the origin markup.

If the attribute name being referred to is the same as the internal variable name then when defining the internal variable, we don't need to specify the attribute name after the symbol. E.g., aValue: "="; is the same as aValue: "=aValue";

When we are using isolate scope, external variables will not be available inside the directive. Only the variables that are declared inside scope: { . . . } will be available.


Transclude
This option is used to pass the matched origin markup and its inner content into a directive template. The parent-to-be element in the template needs to be decorated with ng-transclude attribute. All the inner content of the parent-to-be element will be overwritten with the passed markup. The directive also needs to set transclude: true to activate this option.

<!-- origin markup in the view -->
<trancsludetest>
  <div>
 This is a content from origin markup. 
 <br />
 Try to print a value: {{value}}
  </div>
</trancsludetest>

// code inside controller
$scope.value = 'controller value';

// directive codes
myApp.directive('transcludetest', function () {
  return {
    restrict: 'E',
    transclude: true,
    template: '<div class="fancy-class" style="border: 2px solid black; padding: 2px"><div class="another--class" style="border: 1px dashed blue" ng-transclude>content inside this div will be ignored</div></div>',
    link: function (scope, element) {
      scope.value = 'directive value';
    },
    //scope: {}   // if this is used then only external values are considered
  };
})

See the example in Plunker


Priority and Terminal
priority option is useful for directives (more than one) that are defined in a single DOM element. The option is used to determine the order of how they are going to be applied, especially for their link and compile functions. The compile and pre-link functions are executed from the greatest number while post-link functions are executed from the smallest number.

If not defined, the priority default value is 0. If the directives have same priority values then it seems that they are executed in alphabetical order.

If terminal option is used then directives that have lower priorities will be disregarded.

<!-- origin markup in the view -->
<div second-priority first-priority></div>

// directive codes
myApp.directive('secondPriority', function () {
    return {
        restrict: 'A',
        link: {
          pre: function (scope, element, attrs) {
            alert('preLink - two');
            element.append("<br/>preLink - two");
          },
          post: function (scope, element, attrs) {
            alert('postLink - two');
            element.append("<br/>postLink - two");
          }
        },
        priority: 1
    }
})
.directive('firstPriority', function () {
    return {
        restrict: 'A',
        link: {
                pre: function (scope, element, attrs) {
                  alert('preLink - one');
                  element.append("preLink - one");
                },
                post: function (scope, element, attrs) {
                  alert('postLink - one');
                  element.append("<br/>postLink - one");
                }
        },
        priority: 2,
        //terminal: true
    }
});

See the example in Plunker


Require
This option is used to pass other directive's controller into the link function of the directive where the option is specified. The other directive could be a sibling directive or a directive in one of parent elements.
The require option can take a single string or an array of strings of directive names to be found.
The string name can be:
- not prefixed - to find a sibling directive
- prefixed with '?' - return 'null' if the intended sibling directive is not found
- prefixed with '^' - find the directive in one of parent elements
- prefixed with '^?' - return 'null' if the intended directive is not found in any parent elements

<!-- origin markup in the view -->
<div parent-directive>
  <div>
 <div>
   <div sibling-directive require-test></div>
 </div>
  </div>
</div>

// directive codes
myApp.directive('parentDirective', function () {
    return {
      restrict: 'A',
      controller: function ($scope) {
        this.parentData = "<br />parentDirective data.. ";
        this.parentFunction = function(param) {
            return param + "<br />   - Hi there, this is parentDirective";
        };
      }
    };
})
.directive('siblingDirective', function () {
    return {
      restrict: 'A',
      require: '^parentDirective',
      link: function (scope, element, attrs, controller)
      {
        element.append(controller.parentData);
        var test = controller.parentFunction('<br />Hello this is siblingDirective');
        element.append(test);
      },
      controller: function ($scope) {
        this.siblingFunction = function(param) {
            return param + "<br />   - Hi there, this is your sibling directive";
        };
      }
    };
})
.directive('requireTest', function () {
    return {
        restrict: 'A',
        require: ['?^parentDirective','siblingDirective'],
        link: function (scope, element, attrs, controller)
        {
          element.append(controller[0].parentData);
          var test = controller[0].parentFunction('<br />Hello this is requireTest');
          element.append(test);
          test = controller[1].siblingFunction('<br />Hello this is requireTest');
          element.append(test);
        }
    };
});

See the example in Plunker