Reputation: 25
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
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.
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.
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