Git Product home page Git Product logo

angular-extending-directive's Introduction

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.

angular-extending-directive's People

Contributors

thaiat avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.