Why AngularJS directive compiling not support nested directives - angularjs

In my understanding, $compile should be able to support nested directive compilation/link, but we came across an issue that the compilation/link is incomplete - only the outermost directive got rendered into DOM, and the issue only reproduced when both below conditions are true:
The inner directive template is loaded via templateUrl (e.g. async manner)
The compilation is triggered outside Angular context.
I wrote a jsfiddler to demo it, part code listed below, for complete case http://jsfiddle.net/pattern/7KjWP/
myApp.directive('plTest', function($compile){
return {
restrict :'A',
scope: {},
replace: true,
template: '<div>plTest rendered </div>',
link: function (scope, element){
$('#button1').on('click', function(){
var ele;
ele = $compile('<div pl-shared />')(scope);
console.log('plTest compile got : '+ ele[0].outerHTML);
// scope.$apply();
element.append(ele);
});
}
};
});
myApp.directive('plShared', function($compile, $timeout){
return {
restrict: 'A',
scope: {},
replace: true,
link: function (scope, element){
// comment out below line to make render success
//$timeout(function(){});
var el = $compile('<div pl-item></div>')(scope);
console.log('plShared compile got:' + el[0].outerHTML);
element.append(el);
}
};
});
myApp.directive('plItem', function($timeout){
return {
restrict: 'A',
scope:{},
template:'<div>plItem rendered <div pl-avatar/></div>',
link: function(scope){
}
};
});
myApp.directive('plAvatar', function(){
return {
restrict: 'A',
scope: {}
, templateUrl: 'avatar.html'
// ,template: 'content of avatar.html <div pl-image></div>'
};
});
Interestingly, i can workaround the issue by either calling scope.$apply() somewhere after compile() call (line 27)
or adding $timeout(function(){}) call into the link func of one of inner directive (line 41). Is this a defect or by design?

$(foo).on(bar, handler) is a jQuery event, which means AngularJS does not know the specifics of it, and will not (can not) run an apply-digest cycle after it to process all the bindings.
scope.$apply was made for this, and as you rightly say, fixes it. The rule of thumb is: if you implement UI functionality in an AngularJS application using other libraries (specifically: outside of apply-digest cycles), you must call scope.$apply yourself.
HTH!

After element.append(el), try to compile again as you have just modified the DOM.
You could try something such as $compile(element)(scope); or $compile(element.contents())(scope);.
As said before me, I would also change the event handler as follows :
$('#button1').on('click', function(){
scope.$apply( function(){
//blablalba
});
});
Also, justa piece of advice in case you would want to minify your code, I would declare the compile dependency using the following syntax :
.directive('directiveName',['$service1',''$service2,...,'$compile', function($service1, $service2,...,$compile){
//blablabla
}]}

Related

Adding directives to an element using another directive

I am trying to create a directive that adds some html code but also adds additional attributes/directives.
Using the code below, an ng-class attribute is indeed added, but it seems angular does not recognize it as a directive anymore. It is there, but it has no effect.
How can I get it to work? Thanks.
The Angular code:
angular.module('myModule', [])
.directive('menuItem', function () {
return {
restrict: 'A',
template: '<div ng-if="!menuItem.isSimple" some-other-stuff>{{menuItem.name}}</div>'
+'<a ng-if="menuItem.isSimple" ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.attr('ng-class', '{active : menuItem.isActivated()}');
}
}
});
And the html:
<li menu-item="menuItem" ng-repeat="menuItem in getMenuItems()" />
EDIT:
The solution by #Abdul23 solves the problem, but another problem arises: when the template contains other directives (like ng-if) these are not executed. It seems the problem just moved.
Is it possible to also make the directives inside the template work?
Or perhaps insert the html using the compile function instead of the template parameter. Since I want a simple distinction based on some value menuItem.isSimple (and this value will not change), perhaps I can insert only the html specific to that case without using ng-if, but how?
You need to use $compile service to achieve this. See this answer.
For your case it should go like this.
angular.module('myModule', [])
.directive('menuItem', function ($compile) {
return {
restrict: 'A',
template: '<a ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.removeAttr('menu-item');
element.attr('ng-class', '{active : menuItem.isActivated()}');
var fn = $compile(element);
return function(scope){
fn(scope);
};
}
}
});

testing ng-transclude doesn't work

I'm writing two directives that wrap ui-bootstrap's tabset and tab directives.
In order for the content of my directives to be passed to the wrapped directives, I'm using transclusion in both of them.
This works quite well, the only problem is that I'm failing at writing a test that checks that. My test uses a replacement directive as a mock for the wrapped directive, which I replace using $compileProvider before each test.
The test code looks something like this:
beforeEach(module('myModule', function($compileProvider) {
// Mock the internally used 'tab' which is a third party and should not be tested here
$compileProvider.directive('tab', function() {
// Provide a directive with a high priority and 'terminal' set to true, makes sure that
// the mock directive will get executed, and that the real directive will not
var mock = {
priority: 100,
terminal: true,
restrict: 'EAC',
replace: true,
transclude: true,
template: '<div class="mock" ng-transclude></div>'
};
return mock;
});
}));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
beforeEach(function() {
$scope = $rootScope.$new();
});
afterEach(function() {
$scope.$destroy();
});
it('Places the enclosed html inside the tab body', function() {
element = $compile("<div><my-tab>test paragraph</my-tab></div>")($scope);
$scope.$digest();
console.log("element.html() = ", element.html());
expect(element.text().trim()).toEqual("test paragraph");
});
The template of my directive looks something like this:
<div><tab><div ng-transclude></div></tab></div>
The directive module looks something like this:
angular.module('myModule', ['ui.bootstrap'])
.directive('myTab', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'templates/my-tab.tpl.html',
scope: {
}
};
});
The result of the print to the console is this:
LOG: 'element.html() = ', '<div class="ng-isolate-scope" id=""><div id="" heading="" class="mock"><ng-transclude></ng-transclude></div></div>'
Any ideas on why the transclusion doesn't take place (again, it works outside of the test just fine) ?
Update
I've since moved on to other things and directives, and ran into this issue again, but now it's more crucial, the reason being, that the directive I place inside the parent directive, requires the parent controller in its link function.
I've done more research into this, and it turns out that for some reason, compiling the mock directive doesn't create an instance of the transcluded content.
The reason I know that, is that I've placed a printout in every hook possible in both directives (both the mock and the transcluded one), i.e. compile, pre-link, post-link and controller constructor, and I see that the only printouts are from the mock directive.
Now, here's the really interesting part: I've tried using the transclude function in the mock directive's link function to "force" the compilation of the transcluded directive, which worked ! (another proof that it didn't take place implicitly).
Where's the catch you ask ? Well, it still doesn't work. This time, since the link function of the transcluded directive fails since it doesn't find the controller of the mock directive. What ?!
Here's the code:
Code
var mod = angular.module('MyModule', []);
mod.directive('parent', function() {
return {
restrict: 'E',
replace: true,
template: '<div class="parent">...</div>',
controller: function() {
this.foo = function() { ... };
}
};
});
mod.directive('child', function() {
return {
restrict: 'E',
require: '^parent',
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.foo();
}
};
});
Test
describe('child directive', function() {
beforeEach(module('MyModule', function($compileProvider) {
$compileProvider.directive('parent', function() {
return {
priority: 100,
terminal: true,
restrict: 'E',
replace: true,
transclude: true,
template: '<div class="mock"><ng-transclude></ng-transclude></div>',
controller: function() {
this.foo = jasmine.createSpy();
},
link: function(scope, element, attrs, ctrls, transcludeFn) {
transcludeFn();
}
};
});
}));
});
This test fails with an error message such as:
Error: [$compile:ctreq] Controller 'parent', required by directive
'child', can't be found!
Any thoughts, ideas, suggestions would be highly appreciated.
Ok, probably the shortest bounty ever in the history of SO ...
The problem was with the terminal: true and priority: 100 properties of the mock directive. I was under the impression (from an article I read online about how to mock directives), that these properties cause the compiler to stop compiling directives with the same name and prioritize the mock directive to be evaluated first.
I was obviously wrong. Looking at this and this, it becomes clear that:
'terminal' stops any other directives that were not processed yet from being processed
'priority' is used to make sure that the mock directive is processed before the directive it is mocking
The problem, is that this causes all other processing to stop, including the ng-transclude directive, which has the default priority of 0.
However, removing these properties causes all hell to break loose, since both directives have been registered, and so forth (I won't burden you with all the gory details). In order to be able to remove these properties, the two directives should reside in different modules, and there should be no dependency between them. In short, when testing the child directive, the only directive named parent that's evaluated should be the mock directive.
In order to support real life usage, I've introduced three modules to the system:
A module for the child directive (no dependencies)
A module for the parent directive (no dependencies)
A module that has no content, but has a dependency on both child and parent modules, which is the only module you'll ever need to add as a dependency in your code
That's pretty much it. I hope it helps anyone else that runs into such problems.

Why is the post link function executed before the transcluded child link functions?

The timing of (pre/post)link functions in AngularJS are well defined in the documentation
Pre-linking function
Executed before the child elements are linked. Not safe to do DOM
transformation since the compiler linking function will fail to locate
the correct elements for linking.
Post-linking function
Executed after the child elements are linked. It is safe to do DOM
transformation in the post-linking function.
and this blog post clearly illustrates this expected order.
But this order does not seem to apply when using ng-transclude and nested directives.
Here is an example for a dropright element (See the Plunkr)
<!-- index.html -->
<dropright>
<col1-item name="a">
<col2-item>1</col2-item>
<col2-item>2</col2-item>
</col1-item>
<col1-item name="b">
...
</col1-item>
</dropright>
// dropright-template.html
<div id="col1-el" ng-transclude></div>
<div id="col2-el">
<!-- Only angularJS will put elements in there -->
</div>
// col1-item-template.html
<p ng-transclude></p>
// col2-item-template.html
<div ng-transclude></div>
The dropright looks like
The directives write a log in the console when their link and controller functions are called.
It usually displays:
But sometimes (after few refreshes), the order is not as expected:
The dropright post-link function is executed before the post-link function of its children.
It may be because, in my particular case, I am calling the dropright controller in the children's directives (See the Plunkr)
angular.module('someApp', [])
.directive('dropright', function() {
return {
restrict: 'E',
transclude: 'true',
controller: function($scope, $element, $attrs) {
console.info('controller - dropright');
$scope.col1Tab = [];
$scope.col2Tab = [];
this.addCol1Item = function(el) {
console.log('(col1Tab pushed)');
$scope.col1Tab.push(el);
};
this.addCol2Item = function(el) {
console.log('(col2Tab pushed)');
$scope.col2Tab.push(el);
};
},
link: {
post: function(scope, element, attrs) {
console.info('post-link - dropright');
// Here, I want to move some of the elements of #col1-el
// into #col2-el
}
},
templateUrl: 'dropright-tpl.html'
};
})
.directive('col1Item', function($interpolate) {
return {
require: '^dropright',
restrict: 'E',
transclude: true,
controller: function() {
console.log('-- controller - col1Item');
},
link: {
post: function(scope, element, attrs, droprightCtrl) {
console.log('-- post-link - col1Item');
droprightCtrl.addCol1Item(element.children()[0]);
}
},
templateUrl: 'col1-tpl.html'
};
})
.directive('col2Item', function() {
var directiveDefinitionObject = {
require: '^dropright',
restrict: 'E',
transclude: true,
controller: function() {
console.log('---- controller - col2Item');
},
link: {
post: function(scope, element, attrs, droprightCtrl) {
console.log('---- post-link - col2Item');
droprightCtrl.addCol2Item(element.children()[0]);
}
},
templateUrl: 'col2-tpl.html'
};
return directiveDefinitionObject;
});
Is there any clean way to execute the link function of a directive after all the link functions of its children while using transclusion?
This is my theory - its not the transclude aspect that is causing the sequence issue but rather the template being a templateUrl. The template needs to be resolved before the post link function get to act on it - hence we say post link function is safe to do DOM manipulation. While we are getting 304s for all the 3 templates - we do have to read them and it ultimately resolves the template promise.
I created a plunker with template instead of templateUrl to prove the corollary. I have hot refresh/plunker Stop/Run many times but I always get link - dropright at the end.
Plunker with template instead of templateUrl
I don't pretend to understand the compile.js code fully. However it does appear that in
compileTemplateUrl function $http.success() resolves the template and then on success the applyDirectivesToNode function is called passing in postLinkFn.
https://github.com/angular/angular.js/blob/master/src/ng/compile.js
This may be just weirdness with Plunker. I tried copying the files to my local IIS, and was not able to replicate the issue.

Angular Directive Different Template

I have a directive myDirective with variable type. If I run <my-directive type="X"> I want the directive to use templateUrl: x-template.html.
If I do <my-directive type="Y"> I want the directive to use templateUrl: y-template.html.
This is my current directive.
app.directive('myDirective', function() {
var myDirective = {
templateUrl: 'X-template.html',
restrict: 'E',
scope: {
type: '='
},
};
return myDirective;
});
I read thru stackoverflow and angular documentation but have not found anything that I need.
I am now trying to do something along the lines of:
if ($scope.type === 'X') {
templateUrl: 'X-template.html',
}
else if ($scope.type === 'Y') {
templateUrl: 'Y-template.html',
}
But do not know where to do it.
Do you guys know if this is possible and how?
Angular will accept a function as the template option, so you could do something like so:
.directive('myDirective', function () {
return {
templateUrl: function (tElement, tAttrs) {
if (tAttrs) {
if (tAttrs.type === 'X') {
return 'X-template.html';
}
if (tAttrs.type === 'Y') {
return 'Y-template.html';
}
}
}
}
});
For more info, see the documentation for the $compile service.
You can work around this issue using ng-include inside compile:
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
fiddle
If you're willing to live on the bleeding edge with a build on the 1.1.x code path (note the warning attached to every 1.1.x build notes entry so I don't dilute this answer by repeating it again here), you're in luck--this very feature was just added in the 1.1.4 release on April 3rd. You can find the release notes for 1.1.4 here and the feature's task log includes a Jasmine test that demonstrates how to use the new functionality.
If you're more conservative and are using a 1.0.x release, then you won't be able to accomplish this as easily, but it can be done. Mark Rajcok's solution looks like it would fit your requirements as-stated, but I would just add a few additional notes:
Aside from its 1.1.4 release, compile-time directives don't support modification at runtime.
As of 1.1.4, you can safely modify the attributes of compile-time directives, but only from another compile-time directive.
You may want to consider replaceWith() instead of append() since <my-directive> is not a standard-defined HTML element type.
If your X and Y templates contain additional directives, I don't think you'll be able to pass attributes on <my-template> through to the root element of your template so easily.
A directive with replace: true will transfer attributes from the source element to its replacement root, but I do not think that ngInclude will do the same from is host to the root of the included template.
I also seem to recall that ngInclude does not require that its template have exactly one root element.
You could perhaps preserve attributes on a replacement parent by using replaceWith() instead of append() and wrapping the <div ng-include=""> tag within a <div></div>. The outer <div> could hold attributes and would still be accessible after the <div ngInclude> element replaced itself with loaded content.
Be aware that ngInclude creates a new scope. This subjects you to a flashing yellow klaxons warning about the dangers of primitive scope models. For more information, see this fine page from Angular's GitHub depot.
I can propose another alternative for those on 1.0.x, but it involves a fair amount of code. It's a more heavy-weight operation, but it has the upside of not only being able of switching between templates, but full-fledged directives as well. Furthermore, its behavior is more readily dynamic.
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/directive/my-directive.html',
link: function(scope, element, attrs, ctrl) {
// You can do this with isolated scope as well of course.
scope.type = attrs.type;
}
}
);
my-directive.js
<div ng-switch on="{{type}}">
<div ng-switch-where="X" ng-include="X-template.html"></div>
<div ng-switch-where="Y" ng-include="Y-template.html"></div>
</div>
my-directive.html
This is my version for optionally overriding a default template
templateUrl: function (elem, attrs) {
if (attrs.customTemplate) {
return '/path/to/components/tmpl/' + attrs.customTemplate + '.html';
} else {
return '/path/to/components/tmpl/directive.html';
}
}
e.g on a directive
<div my-directive custom-template="custom"></div>
I solve this problem so:
app.directive("post", function ($templateCache, $compile) {
function getTemplate(mode) {
switch (mode) {
case "create":
return "createPost.html";
case "view":
return "viewPost.html";
case "delete":
return "deletePost.html";
}
}
var defaultMode = "view";
return {
scope: {},
restrict: "AE",
compile: function (elem, attrs, transclude) {
return function ($scope, $element, $attr) {
function updateTemplate() {
$element.html("");
$compile($templateCache.get(getTemplate($scope.mode)).trim())($scope, function (clonedElement, scope) {
clonedElement.appendTo($element);
});
}
$scope.mode = $attr.mode || defaultMode;
$scope.$watch("mode", updateTemplate);
}
}
}
});
It's probably not the best way to do this, but I have no extra scope.
Ok, this might help someone here :-)
To inject your custom attr into your link or controller function use the following.
I'm at work right now but will post a fiddle later if I get a chance :-)
.directive('yourDirective', function() {
return {
restrict: 'EA',
template: '<div></div>', // or use templateUrl with/without function
scope: {
myAttibute: '#myAttr' // adds myAttribute to the scope
},
link: function(scope) {
console.log(scope.myAttibute);
}
}
}
// HTML ""
// Console will output "foo"

Angularjs passing object to directive

Angular newbie here. I am trying to figure out what's going wrong while passing objects to directives.
here's my directive:
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
and this is the template where I call the directive:
<div walkmap="store.walks"></div>
store.walks is an array of objects.
When I run this, scope.walks logs as undefined while scope logs fine as an Scope and even has a walks child with all the data that I am looking for.
I am not sure what I am doing wrong here because this exact method has worked previously for me.
EDIT:
I've created a plunker with all the required code: http://plnkr.co/edit/uJCxrG
As you can see the {{walks}} is available in the scope but I need to access it in the link function where it is still logging as undefined.
Since you are using $resource to obtain your data, the directive's link function is running before the data is available (because the results from $resource are asynchronous), so the first time in the link function scope.walks will be empty/undefined. Since your directive template contains {{}}s, Angular sets up a $watch on walks, so when the $resource populates the data, the $watch triggers and the display updates. This also explains why you see the walks data in the console -- by the time you click the link to expand the scope, the data is populated.
To solve your issue, in your link function $watch to know when the data is available:
scope.$watch('walks', function(walks) {
console.log(scope.walks, walks);
})
In your production code, just guard against it being undefined:
scope.$watch('walks', function(walks) {
if(walks) { ... }
})
Update: If you are using a version of Angular where $resource supports promises, see also #sawe's answer.
you may also use
scope.walks.$promise.then(function(walks) {
if(walks) {
console.log(walks);
}
});
Another solution would be to add ControllerAs to the directive by which you can access the directive's variables.
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
controllerAs: 'dir',
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
And then, in your view, pass the variable using the controllerAs variable.
<div walkmap="store.walks" ng-init="dir.store.walks"></div>
Try:
<div walk-map="{{store.walks}}"></div>
angular.module('app').directive('walkMap', function($parse) {
return {
link: function(scope, el, attrs) {
console.log($parse(attrs.walkMap)(scope));
}
}
});
your declared $scope.store is not visible from the controller..you declare it inside a function..so it's only visible in the scope of that function, you need declare this outside:
app.controller('MainCtrl', function($scope, $resource, ClientData) {
$scope.store=[]; // <- declared in the "javascript" controller scope
ClientData.get({}, function(clientData) {
self.original = clientData;
$scope.clientData = new ClientData(self.original);
var storeToGet = "150-001 KT";
angular.forEach(clientData.stores, function(store){
if(store.name == storeToGet ) {
$scope.store = store; //declared here it's only visible inside the forEach
}
});
});
});

Resources