Jer
Jer

Reputation: 5648

How can I avoid race conditions at startup when directive attributes are being interpolated?

This fiddle should make things more clear, but essentially I am assigning some attributes of an element (like its id) parameters in its directive:

myApp.directive('myDiv', function () {
return {
    restrict: 'E',
    replace: true,
    scope: {
        'elementId': '@',
        'displayName': '@',
    },
    transclude: true,
    template: '<div class="my-div" id="{{elementId}}">{{displayName}}<div ng-transclude></div></div>'
}

})

The problem is if I do things immediately at startup, like initialize other directives, those values (e.g. elementId) are not yet interpolated.

In other words, if I get a reference to a myDiv element and print its id, immediately at startup, '{{elementId}}' is printed. But if I wait a short time (say, one second), then the value that was passed as the value to the element-id attribute is printed (as I'd expect).

If you open the console while you view the Fiddle, you'll see that.

What am I doing wrong here? How can I avoid this (other than a lot of really ugly timeouts at startup)?

Upvotes: 2

Views: 1381

Answers (1)

FlavorScape
FlavorScape

Reputation: 14299

You have several things incorrect. Here's a new fiddle where you can see the 'at first' log showing the correct value: http://jsfiddle.net/0mq2xv8m/

1) You should include the inner element in the template of the second. You have transclude set to true, so it's going to replace your node. This also ensures that the second directive is not going to bind before the first one is ready. I.e. since it is in the DOM alongside the outter directive, it may instantiate out of step with the wrapping directive.

template: '<div class="my-div" id="elementId">{{displayName}}<div my-field></div></div>'

2) do id="elementId" instead of id="{{elementId}}" to pass by reference instead of value

3) typically it's bad practice to fish for attributes on a parent, it's better to pass it in with 2-way binding. This holds true in any display-list oriented programming I've worked with.

Good Practice:

You should use a controller or a link function for any "init" steps. These wont run until the directive has all its attributes/scope linked. The way you have it, it is executing during the evaluation step during $scope creation (there is no scope yet). Link functions and controllers wait for $scope to become available. A controller can be used instead of a link function (I think it is clearer and easier to unit test).

angular.module('App').controller('someController',[], function() {
        var controller = {
            init:function(){
                console.log(elementId);
             }
        }
        controller.init();
        return controller;
});


myApp.directive('myDiv', function () {
return {
    restrict: 'E',
    replace: true,
    controller:'someController',
    scope: {
        'elementId': '@',
        'displayName': '@',
    },
    transclude: true,
    template: '<div class="my-div" id="{{elementId}}">{{displayName}}<div ng-transclude></div></div>'
}

Upvotes: 2

Related Questions