475

What's the correct way to communicate between controllers?

I'm currently using a horrible fudge involving window:

function StockSubgroupCtrl($scope, $http) {
    $scope.subgroups = [];
    $scope.handleSubgroupsLoaded = function(data, status) {
        $scope.subgroups = data;
    }
    $scope.fetch = function(prod_grp) {
        $http.get('/api/stock/groups/' + prod_grp + '/subgroups/').success($scope.handleSubgroupsLoaded);
    }
    window.fetchStockSubgroups = $scope.fetch;
}

function StockGroupCtrl($scope, $http) {
    ...
    $scope.select = function(prod_grp) {
        $scope.selectedGroup = prod_grp;
        window.fetchStockSubgroups(prod_grp);
    }
}
Shashank Agrawal
  • 25,161
  • 11
  • 89
  • 121
fadedbee
  • 42,671
  • 44
  • 178
  • 308
  • 36
    Totally moot, but in Angular, you should always use $window instead of the native JS window object. This way you can stub it out in your tests :) – Dan M Nov 26 '12 at 02:01
  • 1
    Please see the comment in the answer below from me with regard to this issue. $broadcast is no longer more expensive than $emit. See the jsperf link I referenced there. – zumalifeguard May 07 '14 at 20:24

19 Answers19

460

Edit: The issue addressed in this answer have been resolved in angular.js version 1.2.7. $broadcast now avoids bubbling over unregistered scopes and runs just as fast as $emit. $broadcast performances are identical to $emit with angular 1.2.16

So, now you can:

  • use $broadcast from the $rootScope
  • listen using $on from the local $scope that needs to know about the event

Original Answer Below

I highly advise not to use $rootScope.$broadcast + $scope.$on but rather $rootScope.$emit+ $rootScope.$on. The former can cause serious performance problems as raised by @numan. That is because the event will bubble down through all scopes.

However, the latter (using $rootScope.$emit + $rootScope.$on) does not suffer from this and can therefore be used as a fast communication channel!

From the angular documentation of $emit:

Dispatches an event name upwards through the scope hierarchy notifying the registered

Since there is no scope above $rootScope, there is no bubbling happening. It is totally safe to use $rootScope.$emit()/ $rootScope.$on() as an EventBus.

However, there is one gotcha when using it from within Controllers. If you directly bind to $rootScope.$on() from within a controller, you'll have to clean up the binding yourself when your local $scope gets destroyed. This is because controllers (in contrast to services) can get instantiated multiple times over the lifetime of an application which would result into bindings summing up eventually creating memory leaks all over the place :)

To unregister, just listen on your $scope's $destroy event and then call the function that was returned by $rootScope.$on.

angular
    .module('MyApp')
    .controller('MyController', ['$scope', '$rootScope', function MyController($scope, $rootScope) {

            var unbind = $rootScope.$on('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });

            $scope.$on('$destroy', unbind);
        }
    ]);

I would say, that's not really an angular specific thing as it applies to other EventBus implementations as well, that you have to clean up resources.

However, you can make your life easier for those cases. For instance, you could monkey patch $rootScope and give it a $onRootScope that subscribes to events emitted on the $rootScope but also directly cleans up the handler when the local $scope gets destroyed.

The cleanest way to monkey patch the $rootScope to provide such $onRootScope method would be through a decorator (a run block will probably do it just fine as well but pssst, don't tell anybody)

To make sure the $onRootScope property doesn't show up unexpected when enumerating over $scope we use Object.defineProperty() and set enumerable to false. Keep in mind that you might need an ES5 shim.

angular
    .module('MyApp')
    .config(['$provide', function($provide){
        $provide.decorator('$rootScope', ['$delegate', function($delegate){

            Object.defineProperty($delegate.constructor.prototype, '$onRootScope', {
                value: function(name, listener){
                    var unsubscribe = $delegate.$on(name, listener);
                    this.$on('$destroy', unsubscribe);

                    return unsubscribe;
                },
                enumerable: false
            });


            return $delegate;
        }]);
    }]);

With this method in place the controller code from above can be simplified to:

angular
    .module('MyApp')
    .controller('MyController', ['$scope', function MyController($scope) {

            $scope.$onRootScope('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });
        }
    ]);

So as a final outcome of all this I highly advise you to use $rootScope.$emit + $scope.$onRootScope.

Btw, I'm trying to convince the angular team to address the problem within angular core. There's a discussion going on here: https://github.com/angular/angular.js/issues/4574

Here is a jsperf that shows how much of a perf impact $broadcastbrings to the table in a decent scenario with just 100 $scope's.

http://jsperf.com/rootscope-emit-vs-rootscope-broadcast

jsperf results

Fabio Bonfante
  • 5,128
  • 1
  • 32
  • 37
Christoph
  • 26,519
  • 28
  • 95
  • 133
  • I'm trying to do your 2nd option, but I'm getting an error: Uncaught TypeError: Cannot redefine property: $onRootScope right where I'm doing the Object.defineProperty.... – Scott Nov 06 '13 at 21:53
  • Maybe I screwed something up when I pasted it here. I use it in production and it works great. I'll take a look tomorrow :) – Christoph Nov 07 '13 at 22:10
  • @Scott I pasted it over but the code already was correct and is exactly what we use in production. Can you double check, that you don't have a typo on your site? Can I see your code somewhere to help troubleshooting? – Christoph Nov 08 '13 at 12:09
  • @Christoph is there a good way of doing the decorator in IE8, as it doesn't support Object.defineProperty on non-DOM objects? – joshschreuder Jan 07 '14 at 01:12
  • @Christoph I don't believe an ES5 shim will work due to this: http://stackoverflow.com/a/10285151/324817. Applying the function directly to `$delegate.constructor.prototype` works but are there any unintended side-effects of not using `enumerable = false`? – joshschreuder Jan 07 '14 at 01:18
  • yep, the side effect is that the function name may show in unexpected situations like the select directive iterating over all scope properties to show it's options etc. Luckily the function may land in native Angular with Angular 1.3 which means the `defineProperty` won't be needed anymore. – Christoph Jan 07 '14 at 15:12
  • This is the best solution to this problem I have seen, thank you. I don't understand how $scope.$onRootScope works though... it seems you are calling the monkey-patched $rootScope as a method on $scope. Am I reading that wrong? How does $scope get access to $onRootScope? `$scope.$onRootScope('someComponent.someCrazyEvent', function(){ console.log('foo'); });` – tengen Jan 14 '14 at 19:10
  • That's because we are patching it directly on the `Scope` prototype which means it will be available on all `$scope` objects as well. This has the benefit that we know *which* scope it's coming from so that we can unregister on `$destroy`. – Christoph Jan 15 '14 at 08:54
  • @Christoph: We need to support IE 8 in our project. So the only solution seems to be using $rootScope.$broadcast + $scope.$on since IE 8 does not support Object.defineProperty, am I right? – Michael Hunziker Feb 10 '14 at 14:44
  • Well, if you don't use `Object.defineProperty` you will run into problems in certain situations. Without directly patching AngularJS, I see no other way. However, the feature is being discussed for AngularJS 1.3 and if you are ok with using a patched AngularJS you can use this PR: https://github.com/angular/angular.js/pull/5507 – Christoph Feb 10 '14 at 20:30
  • 59
    This was a very clever solution to the problem, but it is no longer needed. The latest version of Angular (1.2.16), and probably earlier, has this problem fixed. Now $broadcast will not visit every descendant controller for not reason. It will only visit those who are actually listening for the event. I updated the jsperf referenced above to demonstrate that the problem is now fixed: http://jsperf.com/rootscope-emit-vs-rootscope-broadcast/27 – zumalifeguard May 07 '14 at 20:22
  • Does this solution work even if the controllers are on different pages? – Jess Jun 27 '14 at 15:51
  • This response is no longer correct. Take a look at the latest results: http://jsperf.com/rootscope-emit-vs-rootscope-broadcast/36 – zumalifeguard Jul 19 '14 at 02:03
  • Handy thing to note: You can unbind from broadcasted/emitted events in Angular like so: http://stackoverflow.com/a/14898795/2173353. Seems to be working, but I am not 100% sure yet to tell you the truth. – user2173353 Jul 28 '14 at 12:20
  • @user2173353 sure thing, that's exactly how we do the unsubscribing in the snippet provided by this answer as well. In fact, I was the one who proposed to implement this unsubscribe technique in 2011 ;-) https://github.com/angular/angular.js/issues/542 – Christoph Jul 28 '14 at 12:24
  • @Christoph Oups! Missed that. I thought `console.log('foo');` was where the unbinding would take place. I should get more available sprint time, so I could read more carefully ! :) – user2173353 Jul 28 '14 at 12:43
  • Clean and brief post about Event Bus with $rootscope: http://www.johanilsson.com/2014/04/angularjs-eventbus/ – pdorgambide May 02 '15 at 11:46
109

The top answer here was a work around from an Angular problem which no longer exists (at least in versions >1.2.16 and "probably earlier") as @zumalifeguard has mentioned. But I'm left reading all these answers without an actual solution.

It seems to me that the answer now should be

  • use $broadcast from the $rootScope
  • listen using $on from the local $scope that needs to know about the event

So to publish

// EXAMPLE PUBLISHER
angular.module('test').controller('CtrlPublish', ['$rootScope', '$scope',
function ($rootScope, $scope) {

  $rootScope.$broadcast('topic', 'message');

}]);

And subscribe

// EXAMPLE SUBSCRIBER
angular.module('test').controller('ctrlSubscribe', ['$scope',
function ($scope) {

  $scope.$on('topic', function (event, arg) { 
    $scope.receiver = 'got your ' + arg;
  });

}]);

Plunkers

If you register the listener on the local $scope, it will be destroyed automatically by $destroy itself when the associated controller is removed.

Community
  • 1
  • 1
poshest
  • 4,157
  • 2
  • 26
  • 37
  • 1
    Do you know if this same pattern can be used with the `controllerAs` syntax? I was able to use `$rootScope` in the subscriber to listen for the event, but I was just curious if there was a different pattern. – edhedges Sep 30 '14 at 14:07
  • 3
    @edhedges I guess you could inject the `$scope` explicitly. [John Papa writes](http://www.johnpapa.net/do-you-like-your-angular-controllers-with-or-without-sugar/) about events being one "exception" to his usual rule of keeping `$scope` "out" of his controllers (I use quotes because as he mentions `Controller As` still has `$scope`, it's just under the bonnet). – poshest Sep 30 '14 at 15:45
  • By under the bonnet do you mean that you can still get at it via injection? – edhedges Sep 30 '14 at 16:25
  • 2
    @edhedges I updated my answer with a `controller as` syntax alternative as requested. I hope that's what you meant. – poshest Oct 01 '14 at 06:51
  • @dsdsdsdsd well, eg, if your controller is nested inside another element with an `ng-if` which evaluates to false (from true previously), the controller will be destroyed by Angular – poshest Mar 06 '16 at 12:58
  • I expected that controllers would persist for the lifetime of the application ... once a controller is gone, then so is controls ?? ... even if some state evaluated to false, wouldn't control still need to exist? ... (rhetorical question ... I'm still learning angular) – dsdsdsdsd Mar 06 '16 at 17:46
  • 3
    @dsdsdsdsd, services/factories/providers will stay around forever. There are always one and only one of them (singletons) in an Angular app. Controllers on the other hand, are tied to functionality: components/directives/ng-controller, which can be repeated (like objects made from a class) and they come and go as necessary. Why do you want a control and its controller to keep existing when you don't need it anymore? That's the very definition of a memory leak. – poshest Mar 15 '16 at 10:37
54

Using $rootScope.$broadcast and $scope.$on for a PubSub communication.

Also, see this post: AngularJS – Communicating Between Controllers

Scott Rippey
  • 15,614
  • 5
  • 70
  • 85
Renan Tomal Fernandes
  • 10,978
  • 4
  • 48
  • 31
42

Since defineProperty has browser compatibility issue, I think we can think about using a service.

angular.module('myservice', [], function($provide) {
    $provide.factory('msgBus', ['$rootScope', function($rootScope) {
        var msgBus = {};
        msgBus.emitMsg = function(msg) {
        $rootScope.$emit(msg);
        };
        msgBus.onMsg = function(msg, scope, func) {
            var unbind = $rootScope.$on(msg, func);
            scope.$on('$destroy', unbind);
        };
        return msgBus;
    }]);
});

and use it in controller like this:

  • controller 1

    function($scope, msgBus) {
        $scope.sendmsg = function() {
            msgBus.emitMsg('somemsg')
        }
    }
    
  • controller 2

    function($scope, msgBus) {
        msgBus.onMsg('somemsg', $scope, function() {
            // your logic
        });
    }
    
Singo
  • 521
  • 4
  • 3
  • 7
    +1 for the automatic unsubscription when the scope gets destroyed. – Federico Nafria Mar 20 '14 at 03:12
  • 6
    I like this solution. 2 changes I made: (1) allow the user to pass in 'data' to the emit message (2) make the passing of 'scope' optional so this can be used in singleton services as well as controllers. You can see those changes implemented here: https://gist.github.com/turtlemonvh/10686980/038e8b023f32b98325363513bf2a7245470eaf80 – turtlemonvh Apr 14 '14 at 22:27
20

GridLinked posted a PubSub solution which seems to be designed pretty well. The service can be found, here.

Also a diagram of their service:

Messaging Service

Ryan Schumacher
  • 1,816
  • 2
  • 21
  • 33
15

Actually using emit and broadcast is inefficient because the event bubbles up and down the scope hierarchy which can easily degrade into performance bottlement for a complex application.

I would suggest to use a service. Here is how I recently implemented it in one of my projects - https://gist.github.com/3384419.

Basic idea - register a pubsub/event bus as a service. Then inject that eventbus where ever you need to subscribe or publish events/topics.

numan salati
  • 19,394
  • 9
  • 63
  • 66
  • 7
    And when a controller is not needed anymore, how you automatic unsubscribe it? If you don't do this, because of closure the controller will never be removed from memory and you will still be sensing messages to it. To avoid this you will need to remove then manually. Using $on this will not occur. – Renan Tomal Fernandes Aug 18 '12 at 04:45
  • 1
    thats a fair point. i think it can be solved by how you architect your application. in my case, i have a single page app so its a more mangeable problem. having said that, i think this would be much cleaner if angular had component lifecycle hooks where you could wire/unwire things like this. – numan salati Aug 18 '12 at 16:49
  • 6
    I just leave this here as nobody stated it before. Using the rootScope as an EventBus is **not** inefficient as `$rootScope.$emit()` only bubbles upwards. However, as there is no scope above the `$rootScope` there is nothing to be afraid of. So if you are just using `$rootScope.$emit()` and `$rootScope.$on()` you will have a fast system wide EventBus. – Christoph Oct 21 '13 at 13:55
  • 1
    The only thing you need to be aware of is that if you use `$rootScope.$on()` inside your controller, you will need to clean up the event binding as otherwise they will sum up as it's creating a new one each time the controller is instantiated and they don't get automatically destroyed for you since you are binding to `$rootScope` directly. – Christoph Oct 21 '13 at 14:19
  • The latest version of Angular (1.2.16), and probably earlier, has this problem fixed. Now $broadcast will not visit every descendant controller for not reason. It will only visit those who are actually listening for the event. I updated the jsperf referenced above to demonstrate that the problem is now fixed: http://jsperf.com/rootscope-emit-vs-rootscope-broadcast/27 – zumalifeguard May 07 '14 at 20:23
  • $rootScope broadcast is no longer a problem. See http://jsperf.com/rootscope-emit-vs-rootscope-broadcast/36 – zumalifeguard Jul 19 '14 at 02:04
14

Using get and set methods within a service you can passing messages between controllers very easily.

var myApp = angular.module("myApp",[]);

myApp.factory('myFactoryService',function(){


    var data="";

    return{
        setData:function(str){
            data = str;
        },

        getData:function(){
            return data;
        }
    }


})


myApp.controller('FirstController',function($scope,myFactoryService){
    myFactoryService.setData("Im am set in first controller");
});



myApp.controller('SecondController',function($scope,myFactoryService){
    $scope.rslt = myFactoryService.getData();
});

in HTML HTML you can check like this

<div ng-controller='FirstController'>  
</div>

<div ng-controller='SecondController'>
    {{rslt}}
</div>
Qiang
  • 1,468
  • 15
  • 18
Load Reconn
  • 141
  • 1
  • 2
  • +1 One of those obvious-once-you-are-told-it methods - excellent! I've implemented a more general version with set( key, value) and get(key) methods - a useful alternative to $broadcast. – TonyWilk Oct 19 '16 at 21:43
8

Regarding the original code - it appears you want to share data between scopes. To share either Data or State between $scope the docs suggest using a service:

  • To run stateless or stateful code shared across controllers — Use angular services instead.
  • To instantiate or manage the life-cycle of other components (for example, to create service instances).

Ref: Angular Docs link here

rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
pkbyron
  • 163
  • 3
  • 9
5

I've actually started using Postal.js as a message bus between controllers.

There are lots of benefits to it as a message bus such as AMQP style bindings, the way postal can integrate w/ iFrames and web sockets, and many more things.

I used a decorator to get Postal set up on $scope.$bus...

angular.module('MyApp')  
.config(function ($provide) {
    $provide.decorator('$rootScope', ['$delegate', function ($delegate) {
        Object.defineProperty($delegate.constructor.prototype, '$bus', {
            get: function() {
                var self = this;

                return {
                    subscribe: function() {
                        var sub = postal.subscribe.apply(postal, arguments);

                        self.$on('$destroy',
                        function() {
                            sub.unsubscribe();
                        });
                    },
                    channel: postal.channel,
                    publish: postal.publish
                };
            },
            enumerable: false
        });

        return $delegate;
    }]);
});

Here's a link to a blog post on the topic...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/

jcreamer898
  • 8,109
  • 5
  • 41
  • 56
3

This is how I do it with Factory / Services and simple dependency injection (DI).

myApp = angular.module('myApp', [])

# PeopleService holds the "data".
angular.module('myApp').factory 'PeopleService', ()->
  [
    {name: "Jack"}
  ]

# Controller where PeopleService is injected
angular.module('myApp').controller 'PersonFormCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
  $scope.people = PeopleService
  $scope.person = {} 

  $scope.add = (person)->
    # Simply push some data to service
    PeopleService.push angular.copy(person)
]

# ... and again consume it in another controller somewhere...
angular.module('myApp').controller 'PeopleListCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
  $scope.people = PeopleService
]
Oto Brglez
  • 4,113
  • 1
  • 26
  • 33
  • 1
    Your two controllers do not communicate, they only use one same service. That's not the same thing. – Greg Aug 27 '14 at 23:48
  • @Greg you can achieve the same thing with less code by having a shared service and adding $watches where needed. – Capaj Mar 24 '15 at 21:00
3

I liked the way how $rootscope.emit was used to achieve intercommunication. I suggest the clean and performance effective solution without polluting global space.

module.factory("eventBus",function (){
    var obj = {};
    obj.handlers = {};
    obj.registerEvent = function (eventName,handler){
        if(typeof this.handlers[eventName] == 'undefined'){
        this.handlers[eventName] = [];  
    }       
    this.handlers[eventName].push(handler);
    }
    obj.fireEvent = function (eventName,objData){
       if(this.handlers[eventName]){
           for(var i=0;i<this.handlers[eventName].length;i++){
                this.handlers[eventName][i](objData);
           }

       }
    }
    return obj;
})

//Usage:

//In controller 1 write:
eventBus.registerEvent('fakeEvent',handler)
function handler(data){
      alert(data);
}

//In controller 2 write:
eventBus.fireEvent('fakeEvent','fakeData');
rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
shikhar chauhan
  • 431
  • 1
  • 4
  • 9
  • For memory leak you should add an extra method to de-register from the event listeners. Anyway good trivial sample – Raffaeu Nov 15 '16 at 13:16
2

Here's the quick and dirty way.

// Add $injector as a parameter for your controller

function myAngularController($scope,$injector){

    $scope.sendorders = function(){

       // now you can use $injector to get the 
       // handle of $rootScope and broadcast to all

       $injector.get('$rootScope').$broadcast('sinkallships');

    };

}

Here is an example function to add within any of the sibling controllers:

$scope.$on('sinkallships', function() {

    alert('Sink that ship!');                       

});

and of course here's your HTML:

<button ngclick="sendorders()">Sink Enemy Ships</button>
Peter Drinnan
  • 4,344
  • 1
  • 36
  • 29
1

Starting angular 1.5 and it's component based development focus. The recommended way for components to interact is through the use of the 'require' property and through property bindings (input/output).

A component would require another component (for instance the root component) and get a reference to it's controller:

angular.module('app').component('book', {
    bindings: {},
    require: {api: '^app'},
    template: 'Product page of the book: ES6 - The Essentials',
    controller: controller
});

You can then use the methods of the root component in your child component:

$ctrl.api.addWatchedBook('ES6 - The Essentials');

This is the root component controller function:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Here is a complete architectual overview: Component Communications

georgeawg
  • 48,608
  • 13
  • 72
  • 95
kevinius
  • 4,232
  • 7
  • 48
  • 79
0

You can access this hello function anywhere in the module

Controller one

 $scope.save = function() {
    $scope.hello();
  }

second controller

  $rootScope.hello = function() {
    console.log('hello');
  }

More info here

Prashobh
  • 9,216
  • 15
  • 61
  • 91
  • 7
    A bit late to the party but: don't do this. Putting a function on the root scope is akin to making a function global, which can cause all sorts of problems. – Dan Jul 22 '15 at 13:33
0

I will create a service and use notification.

  1. Create a method in the Notification Service
  2. Create a generic method to broadcast notification in Notification Service.
  3. From source controller call the notificationService.Method. I also pass the corresponding object to persist if needed.
  4. Within the method, I persist data in the notification service and call generic notify method.
  5. In destination controller I listen ($scope.on) for the broadcast event and access data from the Notification Service.

As at any point Notification Service is singleton it should be able to provide persisted data across.

Hope this helps

Anant
  • 3,047
  • 2
  • 27
  • 33
rahul
  • 3,018
  • 4
  • 29
  • 28
0

You should use the Service , because $rootscope is access from whole Application , and it increases the load, or youc use the rootparams if your data is not more.

rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
abhaygarg12493
  • 1,565
  • 2
  • 20
  • 40
0

You can use AngularJS build-in service $rootScope and inject this service in both of your controllers. You can then listen for events that are fired on $rootScope object.

$rootScope provides two event dispatcher called $emit and $broadcast which are responsible for dispatching events(may be custom events) and use $rootScope.$on function to add event listener.

Shivang Gupta
  • 3,139
  • 1
  • 25
  • 24
0
function mySrvc() {
  var callback = function() {

  }
  return {
    onSaveClick: function(fn) {
      callback = fn;
    },
    fireSaveClick: function(data) {
      callback(data);
    }
  }
}

function controllerA($scope, mySrvc) {
  mySrvc.onSaveClick(function(data) {
    console.log(data)
  })
}

function controllerB($scope, mySrvc) {
  mySrvc.fireSaveClick(data);
}
rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
Amin Rahimi
  • 235
  • 3
  • 7
0

You can do it by using angular events that is $emit and $broadcast. As per our knowledge this is the best, efficient and effective way.

First we call a function from one controller.

var myApp = angular.module('sample', []);
myApp.controller('firstCtrl', function($scope) {
    $scope.sum = function() {
        $scope.$emit('sumTwoNumber', [1, 2]);
    };
});
myApp.controller('secondCtrl', function($scope) {
    $scope.$on('sumTwoNumber', function(e, data) {
        var sum = 0;
        for (var a = 0; a < data.length; a++) {
            sum = sum + data[a];
        }
        console.log('event working', sum);

    });
});

You can also use $rootScope in place of $scope. Use your controller accordingly.