How to expose a public API from a directive that is a reusable component?

13,574

Solution 1

Does this work for you?

angular.directive('extLabel', function() {
    return {
        restrict: 'E',
        scope: {
            api: '='
        },
        link: function(scope, iElement, iAttrs) {
            scope.api = {
                    doSomething: function() { },
                    doMore: function() { }
                };
        }
    };
});

From containing parent

<ext:label api="myCoolApi"></ext:label>

And in controller

$scope.myCoolApi.doSomething();
$scope.myCoolApi.doMore();

Solution 2

I like Andrej's and use this pattern regularly, but I would like to suggest some changes to it

angular.directive('extLabel', function {
    return {
        scope: {
            api: '=?',
            configObj: '='
        },
        // A controller, and not a link function. From my understanding, 
        // try to use the link function for things that require post link actions 
        // (for example DOM manipulation on the directive)
        controller: ['$scope', function($scope) {

          // Assign the api just once
          $scope.api = {
            changeLabel: changeLabel
          };

          function changeLabel = function(newLabel) {
            $scope.configObj.label = newLabel;
          }
        }]
    }
});



<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label api="label2api" name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>

In controller of course label2api.changeLabel('label')

Solution 3

I faced this problem when writing a directive to instantiate a dygraph chart in my Angular applications. Although most of the work can be done by data-binding, some parts of the API require access to the dygraph object itself. I solved it by $emit()ing an event:

'use strict';
angular.module('dygraphs', []);

angular.module('dygraphs').directive('mrhDygraph', function ($parse, $q) {
    return {
        restrict: 'A',
        replace: true,
        scope: {data: '=', initialOptions: '@', options: '='},
        link: function (scope, element, attrs) {
            var dataArrived = $q.defer();
            dataArrived.promise.then(function (graphData) {
                scope.graph = new Dygraph(element[0], graphData, $parse(scope.initialOptions)(scope.$parent));
                return graphData.length - 1;
            }).then(function(lastPoint) {
                scope.graph.setSelection(lastPoint);
                scope.$emit('dygraphCreated', element[0].id, scope.graph);
            });
            var removeInitialDataWatch = scope.$watch('data', function (newValue, oldValue, scope) {
                if ((newValue !== oldValue) && (newValue.length > 0)) {
                    dataArrived.resolve(newValue);
                    removeInitialDataWatch();
                    scope.$watch('data', function (newValue, oldValue, scope) {
                        if ((newValue !== oldValue) && (newValue.length > 0)) {
                            var selection = scope.graph.getSelection();
                            if (selection > 0) {
                                scope.graph.clearSelection(selection);
                            }
                            scope.graph.updateOptions({'file': newValue});
                            if ((selection >= 0) && (selection < newValue.length)) {
                                scope.graph.setSelection(selection);
                            }
                        }
                    }, true);
                    scope.$watch('options', function (newValue, oldValue, scope) {
                        if (newValue !== undefined) {
                            scope.graph.updateOptions(newValue);
                        }
                    }, true);
                }
            }, true);
        }
    };
});

The parameters of the dygraphCreated event include the element id as well as the dygraph object, allowing multiple dygraphs to be used within the same scope.

Solution 4

In my opinion, a parent shouldn't access a children scope. How would you know which one to use and which one to not use. A controller should access his own scope or his parent scopes only. It breaks the encapsulation otherwise.

If you want to change your label, all you really need to do is change the label1/label2/label3 variable value. With the data-binding enabled, it should work. Within your directive, you can $watch it if you need some logic everytime it changes.

angular.directive('extLabel', function {
    return {
        scope: {
            name: '@',
            configObj: '='
        },
        link: function(scope, iElement, iAttrs) {
            scope.$watch("configObj", function() {
                // Do whatever you need to do when it changes
            });
        }
    }  
});

Solution 5

Use these directives on the element that you want to go prev and next:

<carousel>
 <slide>
   <button class="action" carousel-next> Next </button>
   <button class="action" carousel-prev> Back </button>
 </slide>
</carousel>

.directive('carouselNext', function () {
       return {
        restrict: 'A',
        scope: {},
        require: ['^carousel'],
        link: function (scope, element, attrs, controllers) {
            var carousel = controllers[0];
            function howIsNext() {
                if ((carousel.indexOfSlide(carousel.currentSlide) + 1) === carousel.slides.length) {
                    return 0;
                } else {
                    return carousel.indexOfSlide(carousel.currentSlide) + 1;
                }
            }
            element.bind('click', function () {
                carousel.select(carousel.slides[howIsNext()]);
            });
        }
    };
})

.directive('carouselPrev', function () {
    return {
        restrict: 'A',
        scope: {},
        require: ['^carousel'],
        link: function (scope, element, attrs, controllers) {
            var carousel = controllers[0];
            function howIsPrev() {
                if (carousel.indexOfSlide(carousel.currentSlide) === 0) {
                    return carousel.slides.length;
                } else {
                    return carousel.indexOfSlide(carousel.currentSlide) - 1;
                }
            }
            element.bind('click', function () {
                carousel.select(carousel.slides[howIsPrev()]);
            });
        }
    };
})
Share:
13,574
Admin
Author by

Admin

Updated on June 24, 2022

Comments

  • Admin
    Admin about 2 years

    Having a directive in angular that is a reusable component, what is the best practice to expose a public API that can be accessed from the controller? So when there are multiple instances of the component you can have access from the controller

    angular.directive('extLabel', function {
        return {
            scope: {
                name: '@',
                configObj: '='
            },
            link: function(scope, iElement, iAttrs) {
                // this could be and exposed method
                scope.changeLabel = function(newLabel) {
                    scope.configObj.label = newLabel;
                }
            }
        }
    });
    

    Then when having:

    <ext-label name="extlabel1" config-obj="label1"></ext-label>
    <ext-label name="extlabel2" config-obj="label2"></ext-label>
    <ext-label name="extlabel3" config-obj="label3"></ext-label>
    

    How can I get the access the scope.changeLabel of extLabel2 in a controller?

    Does it make sense?

  • Admin
    Admin almost 11 years
    The "label" example was just a simple abstraction. That's the point of my question: -"Is there a way to expose an API from a reusable component that can be accessed from the controller?"
  • Benoit Tremblay
    Benoit Tremblay almost 11 years
    You really shouldn't create your directive the same way as a controller. Use $watch and events to interact with your directive, not the scope.
  • Admin
    Admin almost 11 years
    Yes, I understand that this approach is not the most aligned with the MVC principles because it couples the controller with the directive but the need to send parameters (options) to the component from the controller makes it a but more natural.
  • Chandermani
    Chandermani almost 11 years
    I believe the other way around is more acceptable. Parent can access child, keeping child generic. If child accesses parent then it cannot be reused across different parent elements.
  • Trevor
    Trevor about 10 years
    Yes, parent should access a child scope directly, but in my mind, the OP's request is like creating a component class and having another class call a method on that component class [e.g. Bank class calls Account.deposit()].
  • bennyl
    bennyl almost 10 years
    This is a great way, some notes though - 1. you must unsubscribe the watch for the api in the directive or you will find yourself in an infinite loop. 2. since one not always want to access the api you should define the api as "=?"
  • Andrej Kaurin
    Andrej Kaurin almost 10 years
    I am not sure if I understand number 1...and will agree with number 2.
  • bennyl
    bennyl almost 10 years
    if you make a change to the result of the watched expression inside the watch function then the watch function will get called again (which will make the change again) and this can cause an infinite loop (in my case angular notice that and stopped after the 11th try with an exception)
  • Vall3y
    Vall3y about 9 years
    Why use $watch, when you could just assign it regularly? Also you probably want to do it in a directive controller instead of a link function, although it won't matter in many examples
  • kdbanman
    kdbanman almost 9 years
    Does the .directive() method go on the carousel element, or somewhere else?
  • doug65536
    doug65536 almost 9 years
    @kdbanman The module. The module is created with angular.module(name, [ dependencies... ]). An existing module can be appended to by fetching the existing module with angular.module(name);
  • Andrej Kaurin
    Andrej Kaurin over 8 years
    Yup, I removed watch part as it is really useless.
  • Robert Hickman
    Robert Hickman over 8 years
    My concern with this is that the api is not necessarily defined when you try to use it. How would you ensure this?
  • Andrej Kaurin
    Andrej Kaurin over 8 years
    Simple scope.$watch('myCoolApi', function(api) { if(api) { // api is ready here // you can also check for api method if you want } });
  • CodeGems
    CodeGems over 7 years
    I prefer using directive 'controller' instead of 'link'