Thursday, December 4, 2014

The Top 5 Mistakes AngularJS Developers Make Part 3: Overusing $broadcast and $emit

This is the third part in a five-part series that covers common AngularJS mistakes. To recap, the top five mistakes I see people make are:

  1. Heavy reliance on $scope (not using controller as) 
  2. Abusing $watch
  3. Overusing $broadcast and $emit
  4. Hacking the DOM
  5. Failing to Test

When I posted the first article, one comment suggested that using controller as is fine for smaller controllers but large, complex controllers with dependencies might not work out as well. You’ll get to see some dependencies in this article, and hopefully a new way to think about how to manage interdependencies.

Overusing $broadcast and $emit

One advantage of using $scope is the variety of functions that are available like being able to $watch the model. I explained why this isn’t the best idea nor approach, and in this post will tackle another set of features. Fundamentally, the concept of events and being able to have custom events is a good one. In AngularJS, custom events are supported through the $emit (bubble up) and $broadcast (trickle down) functions to publish the event.

In Angular, scopes are hierarchical. Therefore, it is possible to communicate to child scopes or listen to children using these functions. The simple $on function is used to register for or subscribe to an event. Let’s take a simple example. Again, I’m using something contrived to show how it works but hopefully you can extrapolate to more complicated scenarios like master/detail lists or even pages with a lot of information that need some mechanism to auto-refresh when one area is updated.

For this example, I have three separate controllers. One is responsible for handling selection of a gender. A separate controller handles showing a related label and therefore must be aware of changes, and yet another controller counts how many times the gender is changed.

The HTML5 looks like this:

<div ng-app="myApp">
    <div ng-controller="genderCtrl">
        <select ng-options="g as g for g in genders" 
                ng-model="selectedGender">        
        </select>
    </div>
    <div ng-controller="babyCtrl">
        It's a {{genderText}}!
    </div>
    <div ng-controller="watchCtrl">
        Changed {{watches}} times.
    </div>
</
div>

The gender controller exposes the list and handles the current selection. Because other controllers depend on this selection, it watches for a change and broadcasts an event whenever it changes. Some of you may recognize this antique device we used in the past to broadcast music.

boombox

A common pattern I see implemented is to broadcast from the root scope because it’s guaranteed to reach all child scopes.

function GenderController($scope, $rootScope) {

    $scope.genders = genders;
    $scope.selectedGender = genders[0];
    $scope.$watch('selectedGender', function () {
        $rootScope.$broadcast('genderChanged',
            $scope.selectedGender);
    });
}

The controller that exposes the label listens for the event and updates the text accordingly.

function BabyController($scope) {
    $scope.genderText = labels[0];
    $scope.$on('genderChanged',
        function (evt, newGender) {

        $scope.genderText =
            newGender ===
            genders[0] ? labels[0] : labels[1];
    });
}

Finally, a third controller listens for the event and updates a counter every time it is fired.

function WatchController($scope) {
    $scope.watches = 0;
    $scope.$on('genderChanged', function () {
        $scope.watches += 1;
    });
}

This application works (check it out here) but I think we can do better. Why don’t I like this approach? Here are a few reasons:

  • It creates a dependency on $scope that I’m not convinced is needed.
  • It further creates a dependency on $rootScope.
  • If I choose not to use $rootScope, then my controllers have to understand the $scope hierarchy and be able to $emit or $broadcast accordingly. Try testing that! 
  • To react to a change also requires a dependency on $scope.
  • I now have to understand how custom events work and use their convention correctly (notice, for example, the new value is in the second parameter, not the first).  
  • I’m now doing something the “Angular way” that I might be able to do with pure JavaScript. I’m pretty sure the closer to JavaScript I stay, the easier it will be to upgrade and migrate this code later on.

OK, so what’s the answer? One way to look at this is not as a sequence of events (i.e. user changes gender, which triggers a change, which triggers an event, which triggers a response) but instead look at the result. What really happens? The gender change really transitions the state of the model. The label state simply alternates based on the gender selection state, and the counter iterates. So, state change results in mutated model. Do I really need a message to communicate this?

Let me break this down a different way. First, when I think about something common across controllers, I immediately think of a service. Whether it is defined via a factory or service isn’t the point here (if you don’t know or understand the difference, read Understanding Providers, Services, and Factories in Angular) but rather that there is a common state shared across controllers. So, I create a gender service:

function GenderService() { }
 
angular.extend(GenderService.prototype, {
    getGenders: function () {
        return genders.slice(0);
    },
    getLabelForGender: function () {
        return this.selectedGender === genders[0] ?
            labels[0] : labels[1];
    },
    selectedGender: genders[0]
});

The service gives me a copy of the list of genders, allows me to get or set a selected gender and returns the label for the gender. All of the useful, common functionality is packaged in one component. Aside from the way I wired it up using Angular’s extend function, it is a pure POJO object I can test without depending on Angular.

Now I can create a controller that is also a POJO. It does rely on the gender service so it will use constructor injection. Although Angular will handle this for me in the app, I can easily create my own instance of the gender service or mock it and pass it in the constructor for testing and still not depend on Angular at all … or $scope … or $emit … or $broadcast.

function GenderController(genderService) {
    this.genders = genderService.getGenders();
    this.genderService = genderService;
} Object.defineProperty(GenderController.prototype,
    'selectedGender', {
        enumerable: true,
        configurable: false,
        get: function () {
            return this.genderService.selectedGender;
        },
        set: function (val) {
            this.genderService.selectedGender = val;
        }
    });

Notice our “controller” is really just an object that exposes the list of genders and has a property that proxies the selected gender through to the gender service. I could actually have just exposed the service and bound to it directly, but that in my opinion is too much information. My UI should just be concerned with the list and selection, not the underlying implementation of how it is managed. The end result now is that you can select a gender and the service will hold the selection. Notice it is not sending any messages, so how does the label controller handle changes? Take a look:

function BabyController(genderService) {
    this.genderService = genderService;
} Object.defineProperty(BabyController.prototype,
    'genderText', {
        enumerable: true,
        configurable: false,
        get: function () {
            return this.genderService.getLabelForGender();
        }
    });

See how simple that is? I can write a test that creates three POJOs (the service and controllers), mimics updating the selection on the selection controller and verify the gender label changes on the label controller. My UI simply binds to the property it is interested in (the genderText property) and doesn’t get muddied with any presentation or business logic going on “behind the scenes.” When the label property is referenced, it asks the service what the label should be and always returns the correct value based on the current selection.

By now you’ve probably guessed what the watcher controller looks like …

function WatchController(genderService) {

    this.genderService = genderService;
    this._watches = 0;
    this._lastSelection = genderService.selectedGender;
} Object.defineProperty(
    WatchController.prototype,
    'watches', {
        enumerable: true,
        configurable: false,
        get: function () {
            if (this.genderService.selectedGender !==
                this._lastSelection) {
                this._watches += 1;
                this._lastSelection =
                    this.genderService.selectedGender;
            }
            return this._watches;
        }
    });

It simply keeps track of watches internally, and whenever they are accessed externally checks to see if the selection changed. If you don’t like this type of mutation in the property getter, you can expose it as a method and data-bind to the result of the method call instead.

Now I have four POJOs with no Angular dependencies. I can test them to my heart’s content, I don’t need to spin up a $scope and I don’t even care how $emit or $broadcast are implemented. Take a look for yourself at the working example with no $emit or $broadcast.

There is one thing to call out. Some of you may recognize that this approach (specifically for the watch controller) does depend on Angular indirectly. The implementation of the watcher is based on accessing the watches count. Independent of Angular, you could manipulate the selection several times and the watcher would miss it unless you polled it each time. I know in Angular it will always be refreshed due to the digest cycle so in the Angular context it will work fine, but if I wanted something more “independent” I’d need to expose an event from the gender service and register from the watcher service to update the count even when it isn’t polled. In that case I’d probably implement my own event aggregator that doesn’t rely on $scope or $scope hierarchy to function.

The reason this works without watches or messages, by the way, comes back to the way the Angular $digest cycle works. When you mutate the model by changing the gender selection, Angular automatically checks the values of the other properties that are data-bound. This, of  course, calls through to the getter that was implemented and results in the correct value being returned and refreshed to the UI. Once again, we avoid extra watches.

Because watches and event handlers can mutate the model, Angular actually has to go back and revaluate expressions each time a watch function or event handler is called to make sure there are no further updates. In this model, the model itself is already refreshed so there is only one pass needed (you’ll see multiple passes because there are multiple bindings, but no extra passes due to watches).

This approach reduces dependencies and therefore the complexity of the code, makes it easier to test, and provides a performance benefit to boost. It really just involves thinking about things in terms of relationships and state rather than the idea of a “process” that “pushes” things. Let Angular handle all of that lifting and keep your design simple and straightforward. In a later post I’ll reference a more complex project that shows a fully working app using this principle with no dependencies on $scope inside controllers.

Although I’ve addressed some common scenarios related to data-binding, invariably you will run into situations that require you to manipulate the DOM. It may be something simple like refreshing the page title based on a context change, or something more complex like rendering SVG or setting the focus on a control based on a validation error. That often leads to the fourth mistake I see developers make, which is hacking the DOM directly, often from the controller itself!

In the next post I’ll share with you how to handle that using AngularJS. Please take some time to comment and share your thoughts with me, and if you feel I can provide further value please don’t be shy – the contact form on this page will email me directly so reach out and let me know how I can help!

Thanks,

signature[1]