Almotasim
Almotasim

Reputation: 25

Defer processing of directive in AngularJs

I have the need to defer the processing of a directive nested inside another one until an asynchronous operation is performed by the nesting directive. This is easily done with two lines of JQuery, but I was wondering if there was a purely Angular way to do the same, maybe using $q.

In http://jsfiddle.net/4smtgs3f/1/ you can find an example of what I mean:

<div ng-controller="MyCtrl">
    <loadData url="http://ip.jsontest.com/">
        <transformAndOutput/>
    </loadData>
</div>

loadData is loading data from some URI, while one or more transformers handle the processing and display of the data. BTW, in the JSFiddle I use two different loadData directives: loadData1 uses $http asynchronously and fails to load the data in time, while loadData2 uses JQuery $.ajax synchronously and works very well.

So the problem is: $http only works asynchronously, and the inner directive is processed before the asynchronous loading operation has completed. Is there an angular way to obtain the same result?

I know that it is possible to defer the execution of a user-defined function, using then or $q or something, but can I defer the processing of a directive? How would I do?

Thanks for all you can do.

FV

Upvotes: 1

Views: 1705

Answers (1)

Hugo Wood
Hugo Wood

Reputation: 2270

You are not thinking 'the Angular way' enough yet. :)

Timing issues

You create the directive content inside the link function, which means all your content has to be available at that time. As you have noticed, this is a wrong assumption. The content must be allowed to update when new data is available (i.e. when asynchronous operations complete).

The Angular way of manipulating the DOM is two-way bindings. The idiomatic way of displaying content in place of the directive is to write a template for the directive.

function myDirective() {
    return {
        ...
        template: '<div>{{someVar}}</div>',
        ...
    };
}

Using this technique, the DOM will update every time content is updated (that's why we use Angular right?). Add a template to your transformAndOutput directive, and remove the elm.append line in the link function. Now, when the asynchronous callback updates content, the DOM will update to reflect the change.

DEMO

You will notice that in this new demo, both lines show The value of content as called by loadData2 is: 2.0.171.17, which is not what you expect. This brings us to a second issue.

Scoping

Directives generally should have their own, isolated scope, so that they don't 'dirty' the host scope, or cause conflict with other components. In the code you provided, your two directives both modify their host scope by reading and writing to scope.data and scope.callingFunction, so there is a conflict.

To make a directive have its own scope, use the scope option.

function myDirective() {
    return {
        ...
        scope: true,
        ...
    };
}

The directive scope does not inherit the host scope. You can, however, import some data from the host scope. I will not detail that here since it is not needed to answer the question. Please refer to Angular's Guide to Directives.

On the other hand, a nested directive's scope does inherit from the scope of parent directives. This means the transformAndOutput directive can access the properties of the scope of its parent loadData. You should encode that transformAndOutput requires a parent loadData by adding a require: '^loadData' option to transformAndOutput (in the demo it is either loadData1 or loadData2 so I could not add this option).

Add scope: true to your loadData1 and loadData2 directives so that they each have their own content and callingFunction. The previous holder for those variables - the controller - can now be safely removed.

DEMO

A last minor thing: your directive should use HTML-like names (lower case + dashes) in the HTML and JS-like names (camel case) in JS. The conversion is automatically handled by Angular.

Final code

app.directive('loadData1', function($http) {
    return {
        restrict: 'E',
        replace: true,
        scope: true,
        link: function(scope, elm, attrs) {
            scope.callingFunction = 'loadData1'
            // the following to remove bad CORS warnings
            delete $http.defaults.headers.common['X-Requested-With'];   
            $http.get(attrs.url).success(function(data) {
                scope.content = data.ip;
            })  
        },
    };
})


app.directive('transformAndOutput', function() {
    return {
        restrict: 'E',
        scope: true,
        template: '<p>The value of content as called by {{callingFunction}} is: {{content}}</p>',
    };
})

Upvotes: 2

Related Questions