Juri Strumpflohner

RSS

Learning Angular: Verifying whether a function has been passed to my directive's isolated scope

Author profile pic
Juri Strumpflohner
Published
When you create isolated directives, you usually use the scope property to define your directive's external API. Some of the APIs properties might be mandatory while others are optional and based on that state, your directive might react differently. Sounds easy, but I stumbled upon a difference when you pass in functions vs. data objects. See yourself.

Problem

So I have some properties defined on my isolated directives using the scope property:

...
scope: {
    data: '=',
    clickFn: '&'
}

Not all of these properties may be required, so there can be the necessity for checking whether the user provided the directive’s property and hence react differently. With plain data properties, such as like data: '=' in the example before, it is quite easy. Simply use the ng-show (or ng-hide) directive in your template:

...
scope: {
    data: '='
},
template: '<div ng-show="data">Some content</div>',
...

The div won’t be shown if data is not defined. Why? Well, if data is not provided in the directive, then the $scope.data is undefined within your directive’s scope, thus evaluating to false and hence hiding the div.

Strangely, this is not the case when passing functions into a directive’s scope using the & notation. In that case, regardless of whether you provide a function on your directive or not, $scope.clickFn will always contain this:

function (locals) {
  return parentGet(parentScope, locals);
}

You can verify this by yourself. Simply implement the directive’s link function, debug it and inspect the $scope property.

Solution 1 - Ugly

The solution here is to inspect the iAttr parameter of your directive’s link function rather than the $scope object. Inspect it and then assign a variable to the scope which you can then use in your template. Something like this:

angular.module('someModule', [])
    .directive('myDirective', function() {
        return {
          restrict: 'E',
          scope: {
            clickFn: '&'
          },
          template: [
            '<div ng-show="isFn" style="border: 1px solid; padding:5px">',
                '<a href="#" ng-click="clickFn()">Click me</a>',
            '</div>'
            ].join(''),
          link: function($scope, iElem, iAttr){
            // use the iAttr to check whether the property is defined
            $scope.isFn = angular.isUndefined(iAttr.clickFn) === false;
          }
        };
      });

Solution 2

Define your function to be optional: &?. This way the function will only be defined if it actually has been passed in the HTML.

angular.module('someModule', [])
    .directive('myDirective', function() {
        return {
          restrict: 'E',
          scope: {
            clickFn: '&?'
          },
          template: [
            '<div ng-show="isFn" style="border: 1px solid; padding:5px">',
                '<a href="#" ng-click="clickFn()">Click me</a>',
            '</div>'
            ].join(''),
          controller: function($scope){
            
            if ($scope.clickFn) {
              // safely invoke clickFn()
            } else {
              // clickFn is not defined -> fallback
            }

          }
        };
      });

Background

Each function that is passed into the isolated scope of a directive is wrapped like this:

Here’s the source of parentGet

From a bird’s perspective, it seems that it is responsible for safely invoking the passed function with the local scope of the directive. But as said, I didn’t go into the details.