Reputation: 1783
TL;DR:
Using attr value in a directive's templateUrl method, the attr has not been interpolated when using sub-directives. End result is the literal {{attrName}}/something.html
.
Full story: I have an outer directive, which includes inner directives. The trick is, these inner directives are also items which can live on their own without knowledge of the parent.
The rules are simple enough:
Index.html:
<div zoo feeding-time="8am" template-base="/templates"></div>
OR - index.html could also be: gorilla can specify the attr value explicitely because its not inheriting
<div gorilla template-base-url="/templates"></div>
Zoo.html - it passes its own configuration on to gorilla
<div gorilla template-base-url="{{templateBaseUrl}}"></div>
zoo-directive.js
angular.module("app").directive("zoo", [function() {
return {
restrict: "A",
scope: true,
templateUrl: function(element, attrs) {
// THIS ONE NEVER FAILS BECAUSE ITS NEVER INTERPOLATED
var base = attrs.templateBaseUrl;
return base + "/zoo.html";
},
link: function(scope, element, attrs) {
// its my job to give gorilla a templateURL in this case
scope.templateBaseUrl = attrs.templateBaseUrl;
}
};
}]);
gorilla.html
angular.module("app").directive("gorilla", [function() {
return {
restrict: "A",
scope: true,
templateUrl: function(element, attrs) {
// THIS ONLY FAILS WHEN INCLUDED BY ZOO.HTML
// AND THEN, ONLY SOMETIMES. RACE CONDITION? PRIORITY?
// DOES NOT FAIL WHEN INCLUDED BY INDEX.HTML DIRECTLY
var base = attrs.templateBaseUrl;
return base + "/gorilla.html";
}
};
}]);
This works. Sometimes. Sometimes, the literal {{templateBaseUrl}}
is used from the tempateUrl
method. I've only been able to trace it as far as seeing that when attrs.templateBaseUrl
is used by gorilla's templateUrl
method, attrs.templateBaseUrl
it has not yet been interpolated.
So, gorilla.link()
is running prior to {{templateBaseUrl}}
being interpolated and kaboom. 404 at "{{templateBaseUrl}}/gorilla.html"
How can I avoid this?
https://docs.angularjs.org/error/$compile/tpload?p0=%7B%7BtemplateBaseUrl%7D%7D%2Fgorilla.html
I have this baseUrl stuff in a provider which each item relies on, but it has the same effect as this simplified version. It must be a parse order issue.
Upvotes: 4
Views: 1523
Reputation: 19098
Your method is failing because the templateUrl
function has to run before the 'compile' phase of the controller (you can't compile a template if you haven't got it). With nested directives, all of the compile functions are run first, followed by the linking functions. I find the below diagram useful as a reference for 'what runs when' when using nested directives - it's from a fairly in depth article on the subject - a good read.
With this in mind, it's clear to see that when your gorilla directive is compiling, the link function of the zoo hasn't run, which means the scope value hasn't even been set, let alone interpolated into the attribute.
It looks like you're going to have to fetch and compile the template yourself. You can make use of angular's $templateRequest
to ensure that the template is cached correctly. We can avoid worrying about whether the interpolation has happened by just using the value from the scope (I have used an isolate scope as it makes things less ambiguous and is generally better practise, but if you wanted you could just use scope inheritance).
Disclaimer: the below code has been written without running it, and will definitely contain typos and errors! Hopefully you'll see the logic though...
angular.module("app")
.directive("gorilla", function($templateRequest, $compile) {
return {
restrict: "A",
// set up an isolate scope
scope: {
tplBaseUrl: '='
},
link: {
pre: function (scope, elem, attr) {
// Decide if the url is directly set or is dynamic
var baseUrl = scope.tplBaseUrl ? scope.tplBaseUrl : attr.tplBaseUrl;
// request the template
$templateRequest(baseUrl + '/gorilla.html')
.then(function (response) {
tpl = response.data;
// compile the html, then link it to the scope
$elem = $compile(tpl)(scope);
// append the compiled template inside the element
elem.append($elem);
});
},
post: function (scope, elem, attr){
// you can put your normal link function code in here.
}
}
};
});
Note that the first line of the pre-link function essentially checks whether there is a scope variable set with the name you passed, and if not, assumes you've given it a url string (so uses the attribute value). This is possibly not the best way to do it - I would be tempted to use two different attributes, but it's up to you.
Importantly (consult the diagram again!), your parent (zoo) directive must set the value of the template base in its pre-link function, otherwise when the link functions of the child directive run the value will be undefined
.
You could use similar to the child directive here, or use your original approach. This code simplified to give an example of how the tplBaseUrl
would be set during the pre-link.
angular.module("app")
.directive("zoo", function() {
return {
restrict: "A",
template: '<div gorilla tpl-base-url="tplBaseUrl"></div>',
link: {
pre: function (scope, elem, attr) {
// make sure it is set here!
scope.tplBaseUrl = "/templates";
},
post: function (scope, elem, attr){
// link logic
}
}
};
});
And finally this should also work if you were to set it statically:
<div gorilla template-base-url="/templates"></div>
Upvotes: 4