35

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?

Andres Valencia
  • 491
  • 1
  • 5
  • 8
  • You could add an attribute that is a callback function that the directive could call to inform the controller its private method, see http://stackoverflow.com/a/16908195/215945. I don't really recommend this, however. – Mark Rajcok Aug 30 '13 at 20:36
  • To anyone (like me) looking for a solution to this problem, consider using Angular events: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$broadcast $scope.$on and $scope.$broadcast – Matías Chomicki Nov 23 '17 at 19:15

5 Answers5

23

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();
Andrej Kaurin
  • 11,592
  • 13
  • 46
  • 54
  • 4
    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 "=?" – bennyl Jul 13 '14 at 12:49
  • I am not sure if I understand number 1...and will agree with number 2. – Andrej Kaurin Jul 14 '14 at 08:14
  • 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) – bennyl Jul 14 '14 at 12:26
  • 1
    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 – Vall3y Jun 04 '15 at 09:42
  • Yup, I removed watch part as it is really useless. – Andrej Kaurin Nov 04 '15 at 09:48
  • 2
    My concern with this is that the api is not necessarily defined when you try to use it. How would you ensure this? – Robert Hickman Dec 02 '15 at 19:42
  • Simple scope.$watch('myCoolApi', function(api) { if(api) { // api is ready here // you can also check for api method if you want } }); – Andrej Kaurin Dec 07 '15 at 10:42
  • I prefer using directive 'controller' instead of 'link' – CodeGems Oct 03 '16 at 02:18
6

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')

Vall3y
  • 1,181
  • 8
  • 18
5

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.

Max
  • 2,121
  • 3
  • 16
  • 20
  • 1
    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. – Andres Valencia Aug 30 '13 at 13:58
2

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
            });
        }
    }  
});
Paritosh
  • 11,144
  • 5
  • 56
  • 74
Benoit Tremblay
  • 698
  • 5
  • 17
  • 1
    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?" – Andres Valencia Aug 30 '13 at 13:14
  • 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. – Benoit Tremblay Aug 30 '13 at 13:19
  • 3
    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. – Chandermani Aug 30 '13 at 15:01
  • 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()]. – Trevor Apr 14 '14 at 16:36
1

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()]);
            });
        }
    };
})
kdbanman
  • 10,161
  • 10
  • 46
  • 78
  • Does the `.directive()` method go on the carousel element, or somewhere else? – kdbanman Jul 21 '15 at 18:28
  • @kdbanman [The module](https://docs.angularjs.org/guide/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);` – doug65536 Sep 12 '15 at 03:54