I have a directive which is transcluding it's content. And in the transcluded content is a directive which requires the controller of the transcluding directive. This throws an error if i'm creating a transclude function in the transcluding directive. I think this is because the transcluded content gets cloned when you provide a transclude function (https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L846).
I also have a plunker describing my problem: http://plnkr.co/edit/rRKWW6zfjZuUiw1BY4zs?p=preview
What i want to do is i want to transclude the content and parse all of the transcluded content and then put it in the right place in the DOM and compile it myself. The transcluded content is actually the configuration for my directive.
I've also tried emptying the cloned array i receive in the transcluding function, because i actually don't need the content to be transcluded automatically. I just need to parse it and transclude it manually on a later point in time. Angular doesn't need to do anything with my transcluded content. But this doesn't work because the directives are already identified when the transcluding function is called. So when i empty the array i receive an error here (https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L961).
kind regards,
Daan
When you use require: "^controller" you are telling Angular that the directive requires controller to be attached to an ancestor DOM element at the time the link function is run.
When you do transclusion without using the ngTransclude directive, your parent directive link function get's passed a transclude method. (You already knew that; this is just for completeness.) This transclude method does the following:
Extracts the content for transclusion
If a cloneAttachFn was passed in, clone the content and call the cloneAttachFn
Calls $compile() to compile and link the content (or cloned content) using the scope supplied by the call to transclude (defaults to a new scope that inherits from the $parent of the directive scope).
If you call transclude, and end up not attaching the content as a descendant of an element with the required controller (or not adding the content to the DOM at all), then the content will have no parent with the required controller. Because it can't find the required controller, you get an error.
In your example, if you use kmBar with require: "^kmFoo", you are restricted to adding the transcluded content to DOM nodes that are descendants of nodes that have kmFoo.
The easiest fix is to go ahead and append it to kmFoo's element for the purposes of $compile() and linking, but immediately detach it.
Detach (as opposed to remove) maintains click handlers, etc, so everything will continue to work if you append the element later. If you are using early versions of AngularJS, you may need to include jQuery to get detach since it wasn't included in early versions of jqLite.
Here's a snippet of a Plunk I put together
app.directive('kmFoo', function() {
return {
restrict: 'A',
scope: true,
template: '<div></div>',
transclude: true,
controller: function() {
// ...
},
link: function(scope, $element, attrs, ctrl, transcludeFn) {
console.log('linking foo');
// We are going to temporarily add it to $element so it can be linked,
// but after it's linked, we detach it.
transcludeFn(scope, function(clone) {
console.log('transcluding foo');
$element.append(clone);
c = clone;
}).detach();// <- Immediately detach it
}
};
});
app.directive('kmBar', function() {
return {
restrict: 'A',
scope: true,
require: '^kmFoo',
link: function(scope, $element, attrs, fooCtrl) {
console.log('linking bar');
// Right now it's a child of the element containing kmFoo,
// but it won't be after this method is complete.
// You can defer adding this element to the DOM
// for as long as you want, and you can put it wherever you want.
}
};
});
First of all, why do need the transclude function? Is this not sufficient?
app.directive('kmFoo', function() {
return {
'restrict': 'A',
'scope': true,
'controller': function() {
this.tryMe = function() { console.log("Success!") };
},
'link': function(scope, element, attrs, ctrl) {
console.log('linking foo');
var innerHtml = element.html();
// do something with innerHtml
element.html("<div>Empty</div>");
}
};
});
app.directive('kmBar', function() {
return {
'restrict': 'A',
'scope': true,
'require': '^kmFoo',
'link': function(scope, element, attrs, fooCtrl) {
fooCtrl.tryMe();
}
};
});
But if you really want to get access to the fooController and have a transclude function in kmFoo, you can access the controller via element.controller() after all linking is done and all controllers are initialized.
app.directive('kmFoo', function() {
return {
'restrict': 'A',
'scope': true,
'template': '<div ng-transclude></div>',
'transclude': true,
'controller': function() {
this.tryMe = function() { console.log("Success!") };
},
'link': function(scope, $element, attrs, ctrl, transcludeFn) {
console.log('linking foo');
// when you put the transclude function in comments it won't throw an error
transcludeFn(scope, function(clone) {
console.log('transcluding foo');
});
}
};
});
app.directive('kmBar', function() {
return {
'restrict': 'A',
'scope': true,
'template': "<button ng-click='tryMe()'>Feeling lucky?</button>",
'link': function(scope, element, attrs) {
scope.getFooCtrl = function() {
return element.parent().controller('kmFoo');
};
console.log('linking bar');
console.log('parent not yet known: ' + element.parent().toString());
},
'controller': function($scope) {
$scope.tryMe = function() {
$scope.getFooCtrl().tryMe();
};
}
};
});
See it in action with this plnkr.
To perform this without having it displayed in the DOM, you can use a
transclude: 'element'
on the second directive.
This will avoid using some tricks to get the information you need.
app.directive('kmFoo', function() {
return {
'restrict': 'A',
'scope': true,
'template': '<div ng-transclude></div>',
'transclude': true,
'controller': function() {
},
'link': function(scope, $element, attrs, ctrl, transcludeFn) {
console.log('linking foo');
// when you put the transclude function in comments it won't throw an error
//transcludeFn(scope, function(clone) {
// console.log('transcluding foo');
//});
}
};
});
app.directive('kmBar', function() {
return {
'restrict': 'A',
'scope': {},
'require': '^kmFoo',
'link': function(scope, $element, attrs, fooCtrl) {
console.log('linking bar');
}
};
});
app.directive('kmBarWithElement', function() {
return {
'restrict': 'A',
'scope': {},
'transclude': 'element',
'require': '^kmFoo',
'link': function(scope, $element, attrs, fooCtrl, transclude) {
transclude(function(clone) {
console.log('here the element: ', clone);
});
}
};
});
Here is a working example: http://plnkr.co/edit/lbT7oz74Yz7IZvEKp77T?p=preview
Related
I want to create a new directive into ui.boostrap.accordion module to avoid accordion open click event.
I have the following code in another file.js:
angular.module('ui.bootstrap.accordion')
.directive('accordionGroupLazyOpen', function() {
return {
require: '^accordion',
restrict: 'EA',
transclude: true,
replace: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'template/accordion/accordion-group.html';
},
scope: {
heading: '#',
isOpen: '=?',
isDisabled: '=?'
},
controller: function() {
this.setHeading = function(element) {
this.heading = element;
};
},
link: function(scope, element, attrs, accordionCtrl) {
accordionCtrl.addGroup(scope);
scope.openClass = attrs.openClass || 'panel-open';
scope.panelClass = attrs.panelClass;
scope.$watch('isOpen', function(value) {
element.toggleClass(scope.openClass, value);
if (value) {
accordionCtrl.closeOthers(scope);
}
});
scope.toggleOpen = function($event) {
};
}
};
})
The problem is when I execute the app I get the following error:
Controller 'accordionGroup', required by directive
'accordionTransclude', can't be found!
Error link
Any ideas?
As I see from the source code ( maybe not your version but still the same):
// Use in the accordion-group template to indicate where you want the heading to be transcluded
// You must provide the property on the accordion-group controller that will hold the transcluded element
.directive('uibAccordionTransclude', function() {
return {
require: '^uibAccordionGroup', // <- look at this line in your version
link: function(scope, element, attrs, controller) {
scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) {
if (heading) {
element.find('span').html('');
element.find('span').append(heading);
}
});
}
};
So I guess it tries to find a parent directive in the view that matches accordionGroup but since you add the accordionGroupLazyOpen and not the accordionGroup it cannot find it.
In the error page you provided states:
This error occurs when HTML compiler tries to process a directive that
specifies the require option in a directive definition, but the
required directive controller is not present on the current DOM
element (or its ancestor element, if ^ was specified).
If you look in the accordion-group-template file you will see that the accordionTransclude directive gets called there.
I'm reading an article on transclusion and have got to about here here. At this point, the author has said that making a directive in this way
angular.module('app', [])
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
logoSrc: '#',
header: '#',
body: '#'
}
};
});
<my-directive header="header"
body="body"
logo-src="logoSrc.png">
</my-directive>
Is awkward and not desirable. It suggests instead to make the HTML inside the tag
app.directive('myCard', function() {
return {
scope: {
title: '=myTitle',
pic: '=myPic'
},
templateUrl: 'myCard',
transclude: true,
link: function(scope, el, attrs, ctrl, transclude) {
el.find('.content').append(transclude());
}
};
});
Is it necessary to use jQuery and DOM manipulation to use directives this way? Is there really any advantage other than cleaner looking templates which don't need to be appended in this way?
I want to create a component that displays itself as a collapsible box.
When it is expanded, it should show the transcluded content; when it is collapsed it should only show its label.
myApp.directive('collapsingBox', function() {
return {
restrict: 'E',
transclude: true,
require: '^ngModel',
scope: {
ngModel: '='
},
template: '<div ng-controller="CollapseController" class="collapsingBox"><div class="label">Title: {{ ngModel.title }}</div><br/><div ng-transclude ng-show="expanded">Test</div></div>',
link: function($scope, element, attr) {
element.bind('click', function() {
alert('Clicked!');
$scope.toggle();
});
}
};
});
This component should be reusable and nestable, so I wanted to manage the values (like "title" and "expanded") in a controller that gets instantiated for every use of the directive:
myApp.controller('CollapseController', ['$scope', function($scope) {
$scope.expanded = true;
$scope.toggle = function() {
$scope.expanded = !$scope.expanded;
};
}]);
This "almost" seems to work:
http://plnkr.co/edit/pyYV0MAikXThvMO8BF69
The only thing that does not work seems to be accessing the controller's scope from the event handler bound during linking.
link: function($scope, element, attr) {
element.bind('click', function() {
alert('Clicked!');
$scope.toggle(); // this is an error -- toggle is not found in scope
});
}
Is this the correct (usual?) way to create one instance of the controller per use of the directive?
How can I access the toggle-Function from the handler?
Rather than using ng-controller on your directive's template, you need to put the controller in your directive's controller property:
return {
restrict: 'E',
transclude: true,
require: '^ngModel',
scope: {
ngModel: '='
},
template: '<div class="collapsingBox"><div class="label">Title: {{ ngModel.title }}</div><br/><div ng-transclude ng-show="expanded">Test</div></div>',
controller: 'CollapseController',
link: function($scope, element, attr) {
element.bind('click', function() {
alert('Clicked!');
$scope.toggle();
});
}
};
As it is CollapseController's scope will be a child scope of your directive's scope, which is why toggle() isn't showing up there.
Is there a way for not losing connection to the current controller when you are wrapping data with a directive ?
My problem is, that the directive within the wrapped template has no connection to the outside controller any more and so I can not execute the function.
Wrapping Directive:
myApp.directive('wrapContent', function() {
return {
restrict: "E",
scope: {
model: "=",
datas: "="
},
templateUrl: "./any/template.php",
link: function(scope, element, attr) {
// any
}
};
});
Directive within the wrapped Template
myApp.directive('doAction', function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
$(elem).click(function(e) {
scope.$apply(attrs.doAction);
});
}
}
});
Conroller:
lmsApp.controller('OutsideController', function ($scope){
$sope.sayHello = function() {
alert("hello");
};
});
HTML where I want to execute the function (template.php):
<div>
<do-action="sayHello()"></do-action>
</div>
How I call the wrapContent directive which is outside (Updated):
<div ng-controller="OutsideController">
<wrap-content model="any" datas="data_any"></wrap-content>
</div>
How can I execute the sayHello() function?
Thank you for your help! I would appreciate every answer.
wrapContent directive will be processed with the scope of controller.
DoAction directive will be processed with the isolateScope of wrapContent directive.
Solution1:
Get a reference to the sayHello function in wrapContent using '&' and execute it in event handler.
Solution2:
Instead of using scope in your event handler, use scope.$parent.
You should pass sayHallo function to your parent directive using &
myApp.directive('wrapContent', function() {
return {
restrict: "E",
scope: {
model: "=",
datas: "=",
sayHallo: "&"
},
templateUrl: "./any/template.php",
link: function(scope, element, attr) {
// any
}
};
});
HTML
<div ng-controller="OutsideController">
<wrap-content model="any" datas="data_any" sayHallo="sayHallo()"></wrap-content>
</div>
Then in your child directive, you will have sayHallo in your scope, to call it just do it this:
myApp.directive('doAction', function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
scope.sayHallo();
}
}
});
And you dont need pass it again. So your child directive should looks like this:
<div>
<do-action></do-action>
</div>
UPDATE
If you want to use all your parent model functions,without passing each function. In your child directive,just use scope.model to have access to model attributes and functions.
myApp.directive('doAction', function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
scope.model.sayHallo();
}
}
});
Is possible to do the following in Angular?
<div ng-controller="MainCtrl" ng-init="name='World'">
<test name="Matei">Hello {{name}}!</test> // I expect "Hello Matei"
<test name="David">Hello {{name}}!</test> // I expect "Hello David"
</div>
My directive is simple but it's not working:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
replace: true,
transclude: true,
template: '<div class="test" ng-transclude></div>'
};
});
I also tried to use the transclude function to change the scope and it works. The only problem is that I loose the template.
app.directive('test', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
transclude: true,
template: '<div class="test" ng-transclude></div>',
link: function(scope, element, attrs, ctrl, transclude) {
transclude(scope, function(clone) {
element.replaceWith(clone);
});
}
};
});
Is it possible to do this while keeping the template in place and clone to be appended into the element with ng-transclude attribute?
Here is a Plnkr link you could use to test your solution: http://plnkr.co/edit/IWd7bnhzpLmlBpoaoJct?p=preview
That happened to you because you were too aggressive with the element.
You can do different things instead of replaceWith that will... replace the current element.
You can append it to the end, prepend it... pick one template element and insert it inside... Any DOM manipulation. For example:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
transclude: true,
template: '<div class="test">This is test</div>',
link: function(scope, element, attrs, ctrl, transclude) {
transclude(scope, function(clone) {
element.append(clone);
});
}
};
});
Here I decided to put it after the element, so you could expect:
This is test
Hello Matei!
This is test
Hello David!
Notice I did not included ng-transclude on the template. It is not needed because you're doing that by hand.
So TL;DR;: Play with the element, don't just replace it, you can insert the transcluded html where you want it, just pick the right element, and call the right method on it.
For the sake of completeness here is another example: http://plnkr.co/edit/o2gkfxEUbxyD7QAOELT8?p=preview