Skip to content

arjunasuresh3/angular-extending-directive

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Working example : http://thaiat.github.io/angular-extending-directive/

Original post is available at http://thaiat.github.io/blog/2014/03/10/extending-an-existing-directive-in-angularjs/

Let's say you have a third party angularjs directive that you want to extend or simply access the api defined by its controller.
We could use require but that means that we have to put the 2 directives on the same element, or that the extended directive should be contained inside the first one (looks weird), because require will look up the chain of html.

Well... this is not always possible as we do not have control on the code defining the first directive. It could restrict its usage to 'E', meaning that our extended directive cannot be anymore restricted to 'E'.

How can we easily do that, and in a more natural way meaning the extended directive should wrap the first directive ?

Let's first start with some simple index.html

<html>
    <head>
        <title>angular-extending-directive</title>
        <style type="text/css">
       .circle {
            border: 1px black solid;
            text-align: center;
            padding: 4px;
            font-size: 40px;
            border-radius: 50px;
            -moz-border-radius: 50px;
            border-radius: 50%;
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            -khtml-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
            cursor: pointer;
        }
         .counter {
            width: 50px;
            height: 50px;
            background-color: green;         
        }
        </style>
        <script type="text/javascript" src="bower_components/angular/angular.js"></script>
        <script type="text/javascript" src="scripts/app.js"></script>
    </head>
    <body ng-app="myApp">
        <h1>counter</h1> 
        <counter></counter>

    </body>
</html>

We are defining a counter directive with some minimal css.

Our counter directive should display a number, starting at 0, and increment it each time we click on it. Most of the chances are that this third party directive will use an isolate scope.

Again this is pretty simple, and here is the code.

var myApp = angular.module('myApp', []);
myApp.directive('counter', function () {
  return {
    scope: {},
    controller: function ($scope, $element, $attrs, $transclude) {
      $scope.value = 0;
      $scope.increment = function () {
        $scope.value += 1;

      };
    },

    link: function (scope, iElm, iAttrs, controller) {
      iElm.on('click', function (e) {
        e.stopPropagation();
        scope.$apply(function () {
          console.log('click counter');
          scope.increment();
        });
      });
    },
    restrict: 'E',
    template: '<div class="circle counter">{{value}}</div>'
  };
});

The directive presents a circle with a number in it. Each time you click it will increment the number.

Now we want to use this existing directive but extend its behavior. For the matter of the explanation, let's create a new directive called wrappercounter that wraps it into a wider circle, and when will click on it, it should log a console message.

The html for this new directive should be:

<h1>wrapcounter</h1> 
<wrapcounter>
  <counter></counter>
</wrapcounter>

and to please our eyes we can add the following css

.wrapcounter {
    width: 100px;
    height: 100px;
    background-color: red;
    box-pack: center;
    box-align: center;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    cursor: pointer;
}

Depending on how the first directive counter was written we have 3 ways to achieve this.

##1st way : element.isolateScope If the first directive controller uses $scope (like above) we have to retrieve the inner element, and the api declared by the scope will be available through element.isolateScope().

myApp.directive('wrapcounter', function () {
  return {
    restrict: 'E',
    transclude: true,
    template: '<div class="circle wrapcounter" ng-transclude></div>',
    link: function (scope, iElm, iAttrs, controller) {
      // retrieve the inner directive element
      var counter = iElm.find('counter')[0];

      var innerScope = angular.element(counter).isolateScope();
      
      iElm.on('click', function (e) {
        e.stopPropagation();
        scope.$apply(function () {
          // decorating the increment function with a console log.
          console.log('click wrapper');
          // accessing the inner directive api
          innerScope.increment();

        });
      });
    }
  };

});

##2nd way : element.controller(name) This only works if the controller of the first directive uses the this (or controllerAs) syntax. Let's modify the first directive

myApp.directive('counter', function () {
  return {
    scope: {},
    controller: function ($scope, $element, $attrs, $transclude) {
      var vm = this;
      vm.value = 0;
      vm.increment = function () {
        vm.value += 1;
      };
    },
    controllerAs: 'vm',
    link: function (scope, iElm, iAttrs, controller) {
      iElm.on('click', function (e) {
        e.stopPropagation();
        scope.$apply(function () {
          console.log('click counter');
          scope.vm.increment();
        });
      });
    },
    restrict: 'E',
    template: '<div class="circle counter">{{vm.value}}</div>'
  };
});

And now the second directive

myApp.directive('wrapcounter', function () {
  return {
    restrict: 'E',
    transclude: true,
    template: '<div class="circle wrapcounter" ng-transclude></div>',
    link: function (scope, iElm, iAttrs, controller) {
      // retrieve the inner directive element
      var counter = iElm.find('counter')[0];
      
      // retrieve the inner controller
      var innerController = angular.element(counter).controller('counter');
      
      iElm.on('click', function (e) {
        e.stopPropagation();
        scope.$apply(function () {
          // decorating the increment function with a console log.
          console.log('click wrapper');
          // accessing the inner directive api
          innerController.increment();

        });
      });
    }
  };

});

###3rd way : element.data Looking at the angluarjs source code we see this piece of code:

if (!hasElementTranscludeDirective) {
     $element.data('$' + directive.name + 'Controller', controllerInstance);
}

So we can use this to get access to the controller:

myApp.directive('wrapcounter', function () {
  return {
    restrict: 'E',
    transclude: true,
    template: '<div class="circle wrapcounter" ng-transclude></div>',
    link: function (scope, iElm, iAttrs, controller) {
      // retrieve the inner directive element
      var counter = iElm.find('counter')[0];

      // retrieve the inner controller
      var innerController = angular.element(counter).data('$' + 'counter' + 'Controller');
      
      iElm.on('click', function (e) {
        e.stopPropagation();
        scope.$apply(function () {
          // decorating the increment function with a console log.
          console.log('click wrapper');
          // accessing the inner directive api
          innerController.increment();

        });
      });
    }
  };

});

As you see in any case we have access to the api defined by the inner directive. We could change it, decorate or override existing functions, add our own functions if some are missing from the inner directive, etc...

Hope this makes sense...

Git repo : https://github.com/thaiat/angular-extending-directive/tree/master/scripts

Happy coding.

Avi

PS: Big thanks to Nir Kaufman that helped me figure it out.

About

How to extend an existing directive in angularjs

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published