April 28, 2015

Decorating AngularJS Directives to Support Rx.Observable

One of the best uses I’ve found for the various Reactive Extensions (Rx) libraries is for writing clean, maintainable, streamlined code for managing user interface state - a.k.a. “view models” in MVVM nomenclature - in complex or deceptively simple applications. Once you get over the relatively steep Rx learning curve, you’ll start thinking of UI state and all the server & user events and data it’s composed of as collection-like streams of values that change over time instead of variables that must be periodically recalculated imperatively en masse.

History

I’ve successfully used ReactiveCocoa to write view model code for a few Objective-C iOS applications that fall into the “deceptively simple” category. In the future I hope to leverage ReactiveCocoa to clean up some truly complex apps that are currently a rat’s nest of fragile sets of UI state variables - my own naive, rushed designs. I recently used the Reactive Extensions for JavaScript (RxJS) in some Node.js scripts that must fetch, filter, compose, and transform data from several different HTTP API endpoints at the same time. (I hope to write about that adventure eventually.) As someone used to imperative approaches, I’d be lying if I said any of these applications was a piece of cake to write as an Rx noob, but I shudder to think what an imperative, callback-based version of them would look like.

My web framework of choice lately is AngularJS (v1.x). Angular already does a lot to guide you into an MVVM application structure. It also bakes in support for watching simple scope variables for changes, and it allows you to bind them directly to your view document. It’s allowed me to write applications that are light years ahead of what I would have attempted back when jQuery & raw gumption were the only tools at my disposal. But for large and complex web applications with tons of user inputs & state bindings, all of Angular’s niceties still fall short of the panacea that’s in my head. I’ve written, seen, and can imagine some truly gnarly Angular controllers & view models for extremely-complicated user interfaces.

To tame such maintenance beasts and to create a modern, dynamic, reactive, rock-solid user experience, I need composability for all my application’s server events (AJAX, WebSockets), user events (clicks, key presses), model data, and view state. On a page of hundreds of inputs representing complex models, if the disabled state of a certain checkbox depends on 5 other pieces of state across the model, other controls in the view, the user’s permissions, the product’s configuration, and the phase of the moon, I need a readable, concise way to express and maintain that relationship as bugs are fixed and business requirements change. In its v1.x form, AngularJS certainly goes a long way to help facilitate such things, but in certain cases I may have stretched the Angular, Lodash, promises, and plain JavaScript combo as far as they’ll go.

RxJS for Web UIs

So now that I’ve seen RxJS prove itself for dealing with non-trivial asynchronous data fetching and processing, and I know that reactive, MVVM principles work well for some iOS UIs, I’m starting to entertain the idea of using it for web UI code. Fortunately, the RxJS maintainers have already provided a helper library called rx.angular.js for interfacing with AngularJS in some key areas.[1] It provides ways to turn Scope watchers, Scope events, and $q promises into an Rx.Observable - the fundamental, composable building block of Rx. This makes it much easier to compose, transform, and intelligently react to user events and view model changes. However, as of this writing, I haven’t found a great way to bind Observables directly to the state of elements in the view document the way you typically do with simple scope values in Angular. Though AngularJS v2 may change this, the extremely common, stock Angular v1.x directives like ngShow, ngHide, ngIf, and ngDisabled understandably have no idea what to do with Observables. So as far as I can tell, I would have to to keep all those state variables and functions on my scope, subscribe to the UI-state-related-Observables myself in my controller, and update the scope properties from rx.angular.js’s safeApply() operator.

Here’s a contrived example Plunker that shows an example of binding ng-show and ng-bind directives to boring old non-Observable scope properties that we have to “manually” keep up to date from Observable subscriptions in the controller. That’s not terrible, but it stops short of what I was able to do with ReactiveCocoa.

To get around not being able to bind common directives to Observables, I could create a module that defines a whole new set of similar directives - rxShow, rxIf, etc. - by more-or-less copy/pasting from the Angular source and changing all the scope watchers to Observable subscriptions. But what if I already have a giant code base using the ng* directives, currently bound to plain variables & functions? And what if I don’t want to convert my entire UI code base to using Observables at once? It would be nice to refactor portions of the controller code to make certain boolean variables and functions returning boolean into Observables without having to change the document much or remembering to selectively update the directive names in the document.

Angular Decorators to the Rescue

It turns out that Angular provides a way for you to decorate any service or directive with custom logic. This might result in more code than reimplementing some of the simpler stock directives in certain cases, but I think it will result in less disruption to existing applications & view documents and can immediately take advantage of any bug fixes to the directives being decorated. [2]

Let’s Rx-ify ngShow

So let’s create a decorator for Angular’s[3] ngShow directive that will allow us to seamlessly switch from binding to a boolean to binding to an Observable of boolean values.

From the AngularJS docs for $provide.decorator()

A service decorator intercepts the creation of a service, allowing it to override or modify the behavior of the service. The object returned by the decorator may be the original service, or a new service object which replaces or wraps and delegates to the original service.

In our case, we need to modify the capabilities of the ngShow directive while continuing to delegate to it for basic functionality. We also want the directive to continue to work normally when the user still wants to bind to non-Observables.

Decorators must be registered with the $provide service using a Module.config() block at module load time. Here’s the shell of an ngShow directive decorator that does nothing.

// The top-level app module that depends on rx-angular.
var appModule = angular.module('sb.app', ['rx']);

// A config block that allows direct access to the $provide service which 
// allows us to define our decorator. 
appModule.config(function($provide) {
    // Internally, Angular's directive services are named with a "Directive" 
    // suffix.
    $provide.decorator('ngShowDirective', function($delegate) {
        return $delegate;
    });
});

Note: These examples omit injector annotations for the sake of brevity.

The $delegate provided to your decorator function is an array of all the definitions of directives with the name “ngShowDirective”. Apparently it’s possible to register more than one directive with the same name, though I have no idea what that would be used for. For the sake of simplicity in this example, we’ll cross our fingers and assume that for stock AngularJS directives, there’s only one definition of a given directive. So lets store a reference to the directive’s configuration object.

// ...
$provide.decorator('ngShowDirective', function($delegate) {
    var directiveConfig = $delegate[0];
    // ...
    return $delegate;
});
// ...

This directiveConfig is similar to the directive definition object you’d return when defining your own directives. The one property we are interested in is compile - the directive’s compile function. We need to augment the behavior of the directive’s post-link function to handle Observable subscription. To make that happen, we must redefine the directive’s compile function to return our new link function instead of the old link function.

Before defining a new compile function, we need to save the original on so we can call it a bit later to ensure its logic runs. It will also give us a reference to the directive’s post-link function, which we’ll need to delegate to later.

// ...
$provide.decorator('ngShowDirective', function($delegate) {
    var directiveConfig = $delegate[0];

    // Save the original compile function for delegation later.
    var originalCompileFn = directiveConfig.compile;

    // ...
    
    return $delegate;
});
// ...

Now to define the new compile function whose responsibility is to invoke the original compile function, obtain the original link function, and return our new linking function.

// ...
var originalCompileFn = directiveConfig.compile;

directiveConfig.compile = function(tElement, tAttrs) {
    var originalLinkFn = originalCompileFn(tElement, tAttrs);

    return function postLink(scope, iElement, iAttrs, controller, transcludeFn){ /*...*/ };
};

return $delegate;
// ...

At directive post-link time, our general approach will be:

First we’ll keep a reference to the original scope and create the new child scope that will be passed into the original linking function later. We’ll also obtain the name of the scope property that contains the Observable.

// ...
return function postLink(scope, iElement, iAttrs, controller, transcludeFn) {
    var originalScope = scope;
    var childScope = originalScope.$new(false);
    var propertyName = iAttrs['ngShow'];
    // ...
};
// ...

Then we’ll watch the scope property until it becomes something defined and not null, at which point we’ll stop watching it using the unwatch function that Scope.$watch() returned.

// ...
return function postLink(scope, iElement, iAttrs, controller, transcludeFn) {
    var originalScope = scope;
    var childScope = originalScope.$new(false);
    var propertyName = iAttrs['ngShow'];

    var stopWatching = originalScope.$watch(propertyName, function(newVal) {
        if (typeof newVal !== 'undefined' && newVal !== null) {
            stopWatching();
            // ...
        }
    });
};
// ...

When our $watch() callback is invoked, we first check to see if the scope property is an Observable. Internally, RxJS uses duck-typing to determine if an object is an Observable, so we’ll follow their example by copying the private function they use and making it available somewhere in a parent function scope of our postLink function. If it’s not an Observable, we do nothing but invoke the original link function with the new child scope we created, and the directive’s own watcher should handle the inherited changes normally.

// ...
function isObservable(obj) {
    return obj && typeof obj.subscribe === 'function';
}
//...
return function postLink(scope, iElement, iAttrs, controller, transcludeFn) {
    var originalScope = scope;
    var childScope = originalScope.$new(false);
    var propertyName = iAttrs['ngShow'];

    var stopWatching = originalScope.$watch(propertyName, function(newVal) {
        if (typeof newVal !== 'undefined' && newVal !== null) {
            stopWatching();

            if (isObservable(originalScope[propertyName])) {
                // ...
            } else {
                originalLinkFn(childScope, iElement, iAttrs, controller, transcludeFn);
            }
        }
    });
};
// ...

If the directive is bound to an Observable, we’ll subscribe to the Observable a few times. The first subscription uses only the first value emission to invoke the original linking function at the last possible moment, so that the original directive’s watcher never sees the Observable object.

// ...
if (isObservable(originalScope[propertyName])) {
    var valueStream = originalScope[propertyName].share();

    // Invoke the original linking function once, the first time we
    // get a value from the Observable.
    valueStream.first().subscribe(function() {
        originalLinkFn(
            childScope,
            iElement,
            iAttrs,
            controller,
            transcludeFn
        );
    });

    //...
} else {
    originalLinkFn(childScope, iElement, iAttrs, controller, transcludeFn);
}
// ...

Next we make a second subscription that uses rx.angular.js’s safeApply() operator to ensure the new values we receive & copy to the child scope are noticed by the watcher in the original directive.

// ...
if (isObservable(originalScope[propertyName])) {
    var valueStream = originalScope[propertyName].share();

    // Invoke the original linking function once, the first time we
    // get a value from the Observable.
    valueStream.first().subscribe(function() {
        originalLinkFn(
            childScope,
            iElement,
            iAttrs,
            controller,
            transcludeFn
        );
    });

    var valueStreamDisposable = valueStream
        .safeApply(childScope, function onNextValue(value) {
            childScope[propertyName] = value;
        })
        .subscribe();


} else {
    originalLinkFn(childScope, iElement, iAttrs, controller, transcludeFn);
}
// ...

Assuming the Observable is emitting boolean values the ngShow should behave as expected, showing its content when something truthy is sent and assigned to childScope and hiding it when something falsey is sent and assigned to childScope.

Finally, we want to make sure our subscription doesn’t outlive the original scope of the directive, so we’ll register for its $destroy event so we can dispose of our subscription.

// ...
if (isObservable(originalScope[propertyName])) {
    // ...

    var valueStreamDisposable = valueStream
        .safeApply(childScope, function onNextValue(value) {
            childScope[propertyName] = value;
        })
        .subscribe();

    originalScope.$on('$destroy', function() {
        valueStreamDisposable.dispose();
    });
} else {
    originalLinkFn(childScope, iElement, iAttrs, controller, transcludeFn);
}
// ...

What About ngHide, ngDisabled, et. al?

Turns out, we can easily generalize the code above to many other directives by parameterizing a few things: the $provide instance and the directive name.

function rxDecorateDirective($provide, directiveName) {
    $provide.decorator(directiveName + 'Directive', ['$delegate', function($delegate) {
        var directiveConfig = $delegate[0];
        var originalCompileFn = directiveConfig.compile;

        directiveConfig.compile = function(tElement, tAttrs) {
            var originalLinkFn = originalCompileFn(tElement, tAttrs);

            return function postLink(scope, iElement, iAttrs, controller, transcludeFn) {
                var originalScope = scope;
                var childScope = originalScope.$new(false);
                var propertyName = iAttrs[directiveName];

                var stopWatching = originalScope.$watch(propertyName, function(newVal) {
                    if (typeof newVal !== 'undefined' && newVal !== null) {
                        stopWatching();

                        if (isObservable(originalScope[propertyName])) {
                            var valueStream = originalScope[propertyName].share();

                            valueStream.first().subscribe(function() {
                                originalLinkFn(
                                    childScope,
                                    iElement,
                                    iAttrs,
                                    controller,
                                    transcludeFn
                                );
                            });

                            var valueStreamDisposable = valueStream
                                .safeApply(childScope, function onNextValue(value) {
                                    childScope[propertyName] = value;
                                })
                                .subscribe();

                            originalScope.$on('$destroy', function() {
                                valueStreamDisposable.dispose();
                            });
                        } else {
                            originalLinkFn(childScope, iElement, iAttrs, controller, transcludeFn);
                        }
                    }
                });
            };
        };

        return $delegate;
    }]);
}

Now inside a config() block we can call rxDecorateDirective() multiple times to generate and register the decorators we want for many of the directives it makes sense to bind to an Observable.

appModule.config(['$provide', function($provide) {
    rxDecorateDirective($provide, 'ngShow');
    rxDecorateDirective($provide, 'ngHide');
    rxDecorateDirective($provide, 'ngDisabled');
    rxDecorateDirective($provide, 'ngIf');
    rxDecorateDirective($provide, 'ngBind');
}]);

I haven’t tested this approach with every directive - just the ones shown above. I know it doesn’t work with ngRepeat yet. (I should probably save that for another day.) But it seems to work for most of the simple directives I use most often.

Cleanup

Now that it’s working, we can do some cleanup.

Use Function.apply()

Currently we invoke the original compile function normally, but that doesn’t preserve its original this value and we also have to keep the tElement & tAttrs variables floating around that we aren’t using for anything else. So lets call the compile function with apply() instead, passing in directiveConfig as the this arg - since that’s what the compile functions are normally bound to - and our current compile function’s arguments object as the arguments.[4]

// 'tElement, tAttrs' args can be removed now.
directiveConfig.compile = function() {
    var originalLinkFn = originalCompileFn.apply(directiveConfig, arguments);
    //...
};

Then later, something similar for the link function:

//...
return function postLink(scope, iElement, iAttrs) {
    var originalScope = scope;
    var originalLinkArgs = arguments;
    var childScope = originalScope.$new(false);

    // Replace `originalScope` with `childScope` in the link function args
    originalLinkArgs[0] = childScope;
    //...then later on...
    originalLinkFn.apply(directiveConfig, originalLinkArgs);
    //...

Avoid Creating Another Watcher If Possible

Ideally, we don’t want to create another watcher if we don’t have to. The scope property might already resolve to an Observable or some other usable object reference at the time our linking function is invoked. If so, we might as well skip setting a watcher to wait for a valid object.

The first step is to extract a few functions - isUndefinedOrNull() and onFirstUsefulValue() - so we don’t duplicate code:

//...
return function postLink(scope, iElement, iAttrs) {
    var originalScope = scope;
    var originalLinkArgs = arguments;
    var childScope = originalScope.$new(false);
    originalLinkArgs[0] = childScope;
    var propertyName = iAttrs[directiveName];

    // If the current scope property value is null or undefined, watch for the first
    // value that isn't. Otherwise, go ahead and use the current value.
    if (isUndefinedOrNull(originalScope[propertyName])) {
        // TODO: watch for first useful value then call `onFirstUsefulValue()`...
    } else {
        onFirstUsefulValue();
    }

    function isUndefinedOrNull(value) {
        return typeof value === 'undefined' || value === null;
    }

    // Same logic we've seen before.
    function onFirstUsefulValue() {
        if (isObservable(originalScope[propertyName])) {
            var valueStream = originalScope[propertyName].share();

            valueStream.first().subscribe(function() {
                // Note the `apply()`
                originalLinkFn.apply(directiveConfig, originalLinkArgs);
            });

            var valueStreamDisposable = valueStream
                .safeApply(childScope, function onNextValue(value) {
                    childScope[propertyName] = value;
                })
                .subscribe();

            originalScope.$on('$destroy', function() {
                valueStreamDisposable.dispose();
            });
        } else {
            // Note the `apply()`
            originalLinkFn.apply(directiveConfig, originalLinkArgs);
        }
    }
};

Now to fill in the “TODO” for watching for the first non-empty value. It would probably help to think of the future watched values of the property as an Observable stream. rx.angular.js’s $rootScope.$toObservable() allows us to do just that. From that stream, we want to skip any values that are null or undefined, then for the first useful value, we want to invoke onFirstUsefulValue().

//...
return function postLink(scope, iElement, iAttrs) {
    //...
    if (isUndefinedOrNull(originalScope[propertyName])) {
        // Make an Observable stream of property change objects for 
        // originalScope[propertyName].
        originalScope.$toObservable(propertyName)
            // Pluck just the new value out of the change objects that 
            // `$toObservable` sends.
            .pluck('newValue')
            // Skip any uninitialized values, which watchers often produce 
            // initially.
            .skipWhile(isUndefinedOrNull)
            // Take the first good value after the skipped values.
            .first()
            // Setup our subscription or just invoke the original 
            // link function.
            .subscribe(onFirstUsefulValue);
    } else {
        // Immediately setup our subscription or just invoke the original 
        // link function.
        onFirstUsefulValue();
    }
    //...
};
//...

We should now be avoiding the overhead of creating an additional watcher unless absolutely necessary.

Updated Example

Here’s the updated Plunker example from earlier using a few of the decorated directives. The content of the HTML file didn’t have to change at all besides adding another script tag to load the decorator function. The scope variables that were bound to ng-show and ng-bind are now Observables that push out changes on their own.

Here’s a Gist of the final decorator generation function.

Conclusion

It might not seem like much of a change, but scale the application complexity up way beyond this simple example, and I think binding Observables representing complex compositions of data, events and business rules to the view without intermediary scope variables could be really useful. Hopefully rx.angular.js will eventually add official support for something like this.

Update 2015–09–05: Since this was originally written, I’ve updated the decorator to support arbitrary expressions instead of just simple scope properties.

References


  1. and RxJS-DOM for more framework-agnostic code.  ↩

  2. Caveat emptor: I can also imagine it might accidentally interfere with the operation of the directive being decorated, so consider this experimental.  ↩

  3. I was using AngularJS 1.3.15 at the time of this writing.  ↩

  4. Passing an Arguments object as the second argument to Function.apply() will blow up in IE < 9, or any other browser that does not support ECMAScript 5. Convert it to an Array using Array.prototype.slice.call(arguments) first in those browsers.  ↩