AngularJS: Forget About $broadcast() And Say Hi To The SharedContextService

AngularJS: Forget About $broadcast() And Say Hi To The SharedContextService

In AngularJS there one thing that you cannot do properly: Call a controller's method from another controller. In order to do that you must use $broadcast() and $watch() in both controllers. In my opinion, you should avoid using all these $ methods from AngularJS. This is not a very good practice if you want to comunicate between two controllers.

The correct logic to do this kind of thing is to use a "Callback Registration Service". Here's what that means:

In the first controller you will add the method you want to access in an external service (that's the registration). Now in the second controller, inside your logic, you can trigger the callback by calling the service, and thus trigger the method from the first controller.

It's easy to do and much prettier. That's what I call a "good coding practice". And no, it's not an overkill. I've been using this same service on each of my projects. You simply have to copy paste the JS file in your project and learn how to use it. The best thing about this is that it is scalable, meaning you can trigger several methods at once if they are registered correctly in your service.

Stop Talking. Start Coding.

Here is the magic file

(function() {
    'use strict';
    var module;

    angular
        .module('app')
        .factory('sharedContextService', sharedContextService);

    // Implements observer pattern
    function sharedContextService() {
        var observerCallbacks = [];

        var context = {
            user: {},
            model: {
                isAuthModalOpen: false,
            },
            isInit: false,
            register: registerObserverCallback,
            notify: notifyObservers,
            hasChanged: modelHasChanged,
            isLoading: false,
            source: {
                none: 0,
                filter: 1,
                exampleTrigger: 2
            }
        };

        return context;

        ///////////////////

        function registerObserverCallback(dependencySource, callback) {
            if (!observerCallbacks[dependencySource]) {
                observerCallbacks[dependencySource] = [];
            }
            observerCallbacks[dependencySource].push(callback);
        }

        function notifyObservers(dependencySource) {
            if (!dependencySource) {
                // Init
                angular.forEach(observerCallbacks, function(callbacksArray) {
                    angular.forEach(callbacksArray, function(callback) {
                        // source, isInit
                        callback(dependencySource, true);
                    });
                });
            } else {
                angular.forEach(observerCallbacks[dependencySource], function(callback) {
                    // source, isInit
                    callback(dependencySource, false);
                });
            }
        }

        function modelHasChanged(source) {
            if (context.model) {
                notifyObservers(source);
            }
        }
    }
})();

In this code, you only have to care about the context object, which contain several properties you will use accross your entire application. For example, I've set a "user" object. You can store you authenticated user in this object, and use its properties anywhere in your controllers, without having to constantly call a user "User Service" several times.

In the model object you can store any properties that should reflect the global state of your application. In the example above I've set a "isAuthModalOpen" property. That can be used for example if you use a modal, instead of a full dedicated page, in order to display a login form. You can then open and close the modal from any controller that includes the sharedContextService.

Now, that was just a simple method to share global properties accross your application. Let's analyze the interesting part: the Callback Registration. Here's a simple example based on simple logic. The goal is to have a list of articles, and a filter bar above it, so you can choose a category, a date range or whatever, and it will reload the list of article when you change a filter. The filter bar has its own controller, as well as for the articles list.

// FilterController.js
(function() {
    'use strict';

    angular
        .module('app')
        .controller('FilterController', ['sharedContextService', 'filterService', FilterController]);

    function FilterController(sharedContextService, filterService) {
        var vm = this;

        // Variables
        vm.user = sharedContextService.user;
        vm.context = sharedContextService.model;
        vm.someFilterOptions = {};

        // Functions
        vm.init = init;
        vm.updateFilters = updateFilters;

        ////////////

        function init() {
            // code to get some datas from filterService
        }

        function updateFilters(datas) {
            // code to store updated datas into filterService
            // Communicate to the callback service that we need to update the list of articles
            sharedContextService.hasChanged(sharedContextService.source.filter);
        }

    }
})();
// ArticleController.js
(function() {
    'use strict';

    angular
        .module('app')
        .controller('ArticleController', ['sharedContextService', 'articleService', ArticleController]);

    function ArticleController(sharedContextService, lookService) {
        var vm = this;

        // Variables
        vm.articles = [];
        vm.user = sharedContextService.user;
        vm.context = sharedContextService.model;
        vm.isLoading = true;

        // Functions
        vm.init = init;
        vm.getArticles = getArticles;

        ////////////

        function init() {
            // register the method in our Callback Service and specify the source
            sharedContextService.register(sharedContextService.source.filter, getArticles);
            // Get default list of articles
            vm.getArticles();
        }

        function getArticles() {
            vm.articles = [];
            vm.isLoading = true;
            vm.filters = filterService.getFiltersDatas(); // just an example
            return articleService.getArticles(filters)
                .then(function(articles) {
                    vm.articles = articles;
                    vm.isLoading = false;
                });
        }

    }
})();

The important thing to notice is that we register the method in a "source". And by triggering this "source", you will call each methods that are registered in that source. That's why this callback logic is expandable.

You can of course define as much source as you want, in order to cover every part of your application's logic.

Do not hesitate to ask for more examples or explanations in the comments below. I strongly believe that this is the kind of logic that every AngularJS app needs, and that we really need to learn to avoid using $broadcast().

I hope it will help some of you :)

All credits for this amazing idea goes to my dear friend Esteban Goffaux