Directives, Bootstrapping, and FrankenApps with AngularJS

One of the things that AngularJs is really great for is taking what would be loose, untestable interactivity and turning it into tight, testable modules that can be used as directives. For example, if you want to have a button that has a complex interaction, you can describe that complex action as a directive, and then inject it into your now far less complex html. Often, these fancy interactivity directives live in their own little worlds without controllers (and especially without routing).

If the data that your directive needs to operate on is very simple, writing the directive is trivial.

The script to drive the directive might look like this:

var directivesModule = angular.module('directivesModule', 
    ['buttonDependencies']);

directivesModule
    .directive('specialButton', function (ButtonService) {
        return {
            restrict: 'A',
            replace: true,
            scope: {
                firstthinger: '@',
                secondthinger: '@'
            },
            templateUrl: 'template-url.tpl.html',
            link: function (scope) {
                // This is our complex interaction, delivered 
                // by buttonDependencies, which isn't shown.
                ButtonService.doAThing();  
            }
        };
    });

The page rendering the directive might contain something like the following:

<div ng-app="directivesModule">
    <div special-button firstthinger="a string" 
        secondthinger="another string"></div>
    Other page stuff!!
</div>

The directive template could look somewhat like the following:

<div>
    <div>
        {{ firstthinger }}
    </div>
    <div>
        {{ secondthinger }}
    </div>
</div>

What happens though if you have more complex data than strings to pass into your directive? Then you'll want to use a two-way binding on the complex parameter.

var directivesModule = angular.module('directivesModule',
    ['buttonDependencies']);

directivesModule
    .directive('specialButton', function (ButtonService) {
        return {
            restrict: 'A',
            replace: true,
            scope: {
                firstthinger: '@',
                secondthinger: '@'
                complexthinger: '='
            },
            templateUrl: 'template-url.tpl.html',
            link: function (scope) {
                ButtonService.doAThing();  
            }
        };
    });

Assuming that complexthinger is in scope as omgthing when you want to use it, you can simply pass the name of your complexthinger into the directive like the following:

<div ng-app="directivesModule">
    <div special-button firstthinger="a string" 
        secondthinger="another string" 
        complexthinger="omgthing"></div>
    Other page stuff!!
</div>

Now this is well and good when your primary page is written in Angular. But what if your primary page is driven by something totally non-Angular?

Then you want to be able to easily get this omgthing into scope without having to commit to a whole controller-based application. After all, this directive is just spice. It doesn't comprise the main logic of your application, and you want to leave yourself open to rewriting the main page content in angular later, and getting reuse out of your existing directives.

In this case, we want to bootstrap an app (so we can have more than one directive on the page if we need), and get the data into scope. So in the primary page (in Ruby or whatever) we'd have something like this (h/t to @sharondio for help with bootstrapping):

<script>
    // This woould be populated by your ruby, python, whatever.
    window.angularData.thingerdata = { prop: "foo" }; 

    var buttonWrapper = angular.module('buttonWrapper',
        ['directivesModule']).run(['$window', '$rootScope',
        function($window, $rootScope) {
            $rootScope.myfancydata = $window.angularData.thingerdata;
    }]);

    angular.element(document).ready(function () {
        angular.bootstrap(document.getElementById('special_button_scope'),
            ['buttonWrapper']);
    });
</script>
<div id="special_button_scope">
    <div special-button firstthinger="a string" 
        secondthinger="another string" 
        complexthinger="myfancydata"></div>
    Other page stuff!!
</div>

Aye, it's true, we've polluted our rootScope. But that data has got to go somewhere! If we do later decide to redo our page functionality in AngularJS, we can decide then where that data really ought to go. We could have created a service and got the data via AJAX in our directivesModule module, but in an environment where every AJAX request is going to hurt your initial load time, I feel like injecting it via $window is a reasonable compromise to save users the additional roundtrip before they can interact with the page.

Now, we could alternatively make a service to pull myfancydata directly out of the $window into the directive instead of attaching it to window, passing it into the $rootScope, and then using that as a parameter to the directive. Here's why I didn't: this directive isn't just used in a regular page! It's also used in a page that is primarily AngularJS, and on that page, I've got multiple instances of the specialButton directive. So for this scenario, it actually makes sense to pass the complexthinger to each directive rather than pulling it out of $window, since keeping references around to which complexthinger goes with which directive is just as icky. But you'll need to choose the best approach for you.

This is what I came up with. I'd love to hear your approach for dealing with directives, bootstrapping, and frankenapps in AngularJS, along with any feedback in the comment section!

Syndicate content