Monday, November 2, 2015

Angular Directives as ES6 Classes part 2 - Scopes

Expanding on my previous post about writing directives as ES6 classes, I had a reason today to figure out scope-to-object binding. The situation is as follows. I have an ng-view with a template that includes nvd3:

    <graph tag='tag_identifier'>
        <nvd3 options='hist.options' data='hist.data' api='hist.api' ...>
    </graph>


The 'tag' attribute is an identifier for the thing being graphed (it will be fetched using ngResource) and 'api' is a reference to the nvd3 api. I will need this to call methods, such as refresh(), inside the nvd3 directive, so I have to pass these through. Inside nvd3, the scope is declared like this:
    scope: {
        data: '=',
        options: '=',
        api: '=?',
        events: '=?',
        config: '=?'
    }
but I don't want data, options, and api bound to my scope, I want them bound to my controller. The answer is described pretty well by Pascal Precht in his post Exploring Angular 1.3: Binding to Directive Controllers. This works equally well in Angular 1.4.7. I modified my registration function as follows:

    static register(module) {
        module.directive('graph', [ ()=> {
            return {
                restrict: 'AE',
                transclude: false,
                replace: true,
                controller: GraphDirectiveController,
                controllerAs: 'hist',
                scope: {}, /* true, false, or isolate */
                bindToController: {
                    'tag': '@tag',
                    'api': '=api'
                },
                templateUrl: "views/graph_widget.html",
                link: function (scope, elem, attrs, ctrl) {
                    /* set 'options' and 'data': */
                    ctrl.initGraphOptions();
                }
            };
        }]);
    }
When it comes to the lifecycle, three things are going to happen:
  • Instantiation - first, angular will create an instance of your controller
  • Setters - next, it will bind what used to be a scope to your controller instance
  • link() - this is your chance to do any post-initialization after bindings are set up
Whatever you do, you should definitely not depend on your setters getting called in any particular order. As a directive author, it would not be very nice of you to ask your users to only write attributes in a certain order. That's the main reason to use the link function just to trigger your controller to do any post-binding setup it needs to do. In my case, this means making sure that the api has been initialied before calling initGraphOptions(), which sets this.data and this.options. (Since we're now binding the scope to the controller, that's it!) Just pay attention to the lifecycle and if things get confusing, remember this is ES6 so you can always add explicit setters to your controller and log what's happening:
    set api(_api) {
        console.log("Setting nvd3 API");
        this._api = _api;
    }