117

I have a directive that has its own controller. See the below code:

var popdown = angular.module('xModules',[]);

popdown.directive('popdown', function () {
    var PopdownController = function ($scope) {
        this.scope = $scope;
    }

    PopdownController.prototype = {
        show:function (message, type) {
            this.scope.message = message;
            this.scope.type = type;
        },

        hide:function () {
            this.scope.message = '';
            this.scope.type = '';
        }
    }

    var linkFn = function (scope, lElement, attrs, controller) {

    };

    return {
        controller: PopdownController,
        link: linkFn,
        replace: true,
        templateUrl: './partials/modules/popdown.html'
    }

});

This is meant to be a notification system for errors/notifications/warnings. What I want to do is from another controller (not a directive one) to call the function show on this controller. And when I do that, I would also want my link function to detect that some properties changed and perform some animations.

Here is some code to exemplify what I'm asking for:

var app = angular.module('app', ['RestService']);

app.controller('IndexController', function($scope, RestService) {
    var result = RestService.query();

    if(result.error) {
        popdown.notify(error.message, 'error');
    }
});

So when calling show on the popdown directive controller, the link function should also be triggered and perform an animation. How could I achieve that?

Smi
  • 13,850
  • 9
  • 56
  • 64
user253530
  • 2,583
  • 13
  • 44
  • 61
  • Where are you placing the call to the `popdown` directive on the page - is it just in one place where other controllers are supposed to all have access to it, or are there several popdowns in different places? – satchmorun Feb 14 '13 at 21:00
  • my index.html has this :
    basically there is only 1 popdown instance as its meant to be globally available.
    – user253530 Feb 14 '13 at 21:12
  • 1
    I think you meant to write `popdown.show(...)` instead of `popdown.notify(...)` is that right? Otherwise the notify function is kind of confusing. – lanoxx Dec 08 '14 at 14:31
  • where does it come from `popdown.notify`? `.notifiy` method, I mean – Green Sep 12 '15 at 13:33

4 Answers4

166

This is an interesting question, and I started thinking about how I would implement something like this.

I came up with this (fiddle);

Basically, instead of trying to call a directive from a controller, I created a module to house all the popdown logic:

var PopdownModule = angular.module('Popdown', []);

I put two things in the module, a factory for the API which can be injected anywhere, and the directive for defining the behavior of the actual popdown element:

The factory just defines a couple of functions success and error and keeps track of a couple of variables:

PopdownModule.factory('PopdownAPI', function() {
    return {
        status: null,
        message: null,
        success: function(msg) {
            this.status = 'success';
            this.message = msg;
        },
        error: function(msg) {
            this.status = 'error';
            this.message = msg;
        },
        clear: function() {
            this.status = null;
            this.message = null;
        }
    }
});

The directive gets the API injected into its controller, and watches the api for changes (I'm using bootstrap css for convenience):

PopdownModule.directive('popdown', function() {
    return {
        restrict: 'E',
        scope: {},
        replace: true,
        controller: function($scope, PopdownAPI) {
            $scope.show = false;
            $scope.api = PopdownAPI;

            $scope.$watch('api.status', toggledisplay)
            $scope.$watch('api.message', toggledisplay)

            $scope.hide = function() {
                $scope.show = false;
                $scope.api.clear();
            };

            function toggledisplay() {
                $scope.show = !!($scope.api.status && $scope.api.message);               
            }
        },
        template: '<div class="alert alert-{{api.status}}" ng-show="show">' +
                  '  <button type="button" class="close" ng-click="hide()">&times;</button>' +
                  '  {{api.message}}' +
                  '</div>'
    }
})

Then I define an app module that depends on Popdown:

var app = angular.module('app', ['Popdown']);

app.controller('main', function($scope, PopdownAPI) {
    $scope.success = function(msg) { PopdownAPI.success(msg); }
    $scope.error   = function(msg) { PopdownAPI.error(msg); }
});

And the HTML looks like:

<html ng-app="app">
    <body ng-controller="main">
        <popdown></popdown>
        <a class="btn" ng-click="success('I am a success!')">Succeed</a>
        <a class="btn" ng-click="error('Alas, I am a failure!')">Fail</a>
    </body>
</html>

I'm not sure if it's completely ideal, but it seemed like a reasonable way to set up communication with a global-ish popdown directive.

Again, for reference, the fiddle.

satchmorun
  • 12,487
  • 2
  • 41
  • 27
  • 10
    +1 One should never call a function in a directive from outside the directive - it's bad practice. Using a service to manage global state that a directive reads is super common and this is the correct approach. More applications include notification queues and modal dialogs. – Josh David Miller Feb 14 '13 at 22:02
  • 2
    Excellent answer! You have answered this question and all my other questions about how to write good reusable modules that could just be dropped in any other project and used as is. Thank you so much! I've been looking for this information for the past 2 days and just couldn't find anything to answer my questions. – user253530 Feb 14 '13 at 22:05
  • 7
    Really exceptional answer! Such a useful example for those of us coming from jQuery and Backbone – Brandon Jun 06 '13 at 16:00
  • 11
    In this way is it possible to use this module to instantiate multiple directives in the same view? How can I call the success or error function of a particular instance of this directive? – ira Jan 08 '14 at 09:58
  • I don't think so. Factories are singletons. – TKrugg Apr 30 '14 at 13:32
  • 3
    @ira you could probably change the factory to keep a map (or list) of status and message objects and then use a name attribute on the directive to identify which item in the list you need. So instead of calling `success(msg)` in the html you would call `sucess(name, msg)` to select the directive with the correct name. – lanoxx Dec 08 '14 at 14:42
  • 5
    @JoshDavidMiller why do you consider it bad practice to call a method on a directive? If a directive encapsulates the some DOM logic as intended, surely it is quite natural to expose an API so that controllers that use it can invoke its methods as needed? – Paul Taylor Jan 05 '15 at 15:45
  • 1
    Because of separation of concerns. Your controller should manage the state to which the directive responds; if the directive changes the state, then the controller can respond in kind, and neither had to know the other existed. Do you have an example? – Josh David Miller Jan 07 '15 at 06:39
  • @JoshDavidMiller No. If this directive is reusable component, then directive's controller should manage directive's state. Not controller which just wants to use this directive. There is two parts of component: it's internal state and it's properties. It's outrer controller responsibility to mange directive properties, but i't directive responsibility to manage it's internal state and of course there are cases when you need some api to change it state. Popup component is singleton by nature. But there are cases when you need api for not singleton components. – Ruslan Stelmachenko Nov 26 '15 at 17:03
  • @djxak Perhaps I should have been clearer, because I think we largely agree. I didn't say the controller should manage the directive's state; I agree that would be absurd. I said the directive should manage the state to which the controller responds, meaning the "public api" it provides through properties. For example, a directive to display a user profile shouldn't access the "user service"; it should instead receive a user to display. That is the more generic, reusable form, as the calling controller can change it at will. – Josh David Miller Nov 29 '15 at 06:07
  • @JoshDavidMiller Oh, sorry. Yes, then I fully agree with this. But there are cases when reusable component needs some public api. Of course any api can be emulated with properties but it can be very ugly code. For example `shake` method which would play shake animation for 1s. It will be simple to call `directive1.shake(3)` instead of setting `shaking=3` from outer controller and then in directive set it to 0 after 3s. It's internal state which (without directive's api) we forced to manage from outside. – Ruslan Stelmachenko Nov 30 '15 at 15:16
  • @djxak Indeed. Without going too much into implementation details, these are often cases where directive+service combinations work well, so the service manages the state across directive instances and the controller can call methods on the injected service to which the directive responds. But there are many ways to proverbially skin a cat. I was attempting to give the most generic, "best practices" response - but engineers should exercise their good judgment in knowing when to deviate. – Josh David Miller Nov 30 '15 at 17:37
  • Because this question is kind common for different frameworks I've implemented the _Knockout's_ solution: [jsfiddle](https://jsfiddle.net/6dvsm73L/2/) – Hamid Behnam Apr 16 '16 at 03:00
  • @Ianoxx - following your suggestion, created this fiddle for multiple directives: http://jsfiddle.net/45y9Lt04/ – Florin Vistig Aug 02 '16 at 15:25
27

You can also use events to trigger the Popdown.

Here's a fiddle based on satchmorun's solution. It dispenses with the PopdownAPI, and the top-level controller instead $broadcasts 'success' and 'error' events down the scope chain:

$scope.success = function(msg) { $scope.$broadcast('success', msg); };
$scope.error   = function(msg) { $scope.$broadcast('error', msg); };

The Popdown module then registers handler functions for these events, e.g:

$scope.$on('success', function(event, msg) {
    $scope.status = 'success';
    $scope.message = msg;
    $scope.toggleDisplay();
});

This works, at least, and seems to me to be a nicely decoupled solution. I'll let others chime in if this is considered poor practice for some reason.

Aron
  • 1,552
  • 1
  • 13
  • 34
  • 1
    One drawback that I can think of is that in the selected answer you only need the PopdownAPI (easily available with DI). In this one you need access to the controller's scope to broadcast the message. Anyway, it looks very concise. – Julian Aug 28 '13 at 03:34
  • I like this better than the service-approach for simple use cases as it keeps the complexity down and is still loosely coupled – Patrick Oct 21 '14 at 21:40
11

You could also expose the directive's controller to the parent scope, like ngForm with name attribute does: http://docs.angularjs.org/api/ng.directive:ngForm

Here you could find a very basic example how it could be achieved http://plnkr.co/edit/Ps8OXrfpnePFvvdFgYJf?p=preview

In this example I have myDirective with dedicated controller with $clear method (sort of very simple public API for the directive). I can publish this controller to the parent scope and use call this method outside the directive.

luacassus
  • 6,540
  • 2
  • 40
  • 58
  • This requires a relationship between the controllers, right? Since OP wanted a message center, this may not be ideal to him. But it was very nice to learn your approach as well. It's useful in many situations and, like you said, angular itself uses it. – fasfsfgs Jul 19 '15 at 04:09
  • I a trying to follow an example provided by satchmorun. I am generating some html at the runtime, but I am not using directive's template. I am using directive's controller to specify a function to call from the added html but the function is not getting called. Basically, I have this directive: directives.directive('abcXyz', function ($compile { return { restrict: 'AE', require: 'ngModel', controller: function ($scope) { $scope.function1 = function () { .. }; }, my html is:" – Mark Aug 03 '15 at 00:51
  • This is the only elegant solution which can expose directive api if directive isn't a singleton! I still don't like using `$scope.$parent[alias]` because it's smells for me like using internal angular api. But still can't find more elegant solution for not-singleton directives. Other variants like broadcasting events or define empty object in parent controller for directive api smells even more. – Ruslan Stelmachenko Nov 26 '15 at 17:23
3

I got much better solution .

here is my directive , I have injected on object reference in directive and has extend that by adding invoke function in directive code .

app.directive('myDirective', function () {
    return {
        restrict: 'E',
        scope: {
        /*The object that passed from the cntroller*/
        objectToInject: '=',
        },
        templateUrl: 'templates/myTemplate.html',

        link: function ($scope, element, attrs) {
            /*This method will be called whet the 'objectToInject' value is changes*/
            $scope.$watch('objectToInject', function (value) {
                /*Checking if the given value is not undefined*/
                if(value){
                $scope.Obj = value;
                    /*Injecting the Method*/
                    $scope.Obj.invoke = function(){
                        //Do something
                    }
                }    
            });
        }
    };
});

Declaring the directive in the HTML with a parameter:

<my-directive object-to-inject="injectedObject"></ my-directive>

my Controller:

app.controller("myController", ['$scope', function ($scope) {
   // object must be empty initialize,so it can be appended
    $scope.injectedObject = {};

    // now i can directly calling invoke function from here 
     $scope.injectedObject.invoke();
}];
Ashwini Jindal
  • 811
  • 8
  • 15
  • This basically goes against the separation of concerns principles. You provide to the directive an object instantiated in a controller, and you delegate the responsibility of managing that object (ie creation of the invoke function) to the directive. In my opinion, NOT the better solution. – Florin Vistig Aug 02 '16 at 14:54