i have seen different example for rending an element in the link function
example one:
var template = '<span><input type="text" ng-model="ngModel"></span>';
element.html(template);
$compile(element.contents())(scope);
example two:
var template = '<span><input type="text" ng-model="ngModel"></span>';
element.html(template);
var el = $compile(element.contents())(scope);
element.replaceWith(el);
i had tried 2-3 simple directives which works even without replacing the element. so what is the use case for the "element.replaceWith(el)". When is it necessary to user "element.replaceWith(el)" at the end of the link function?
Replacement is actually optional, and the final result won't be exactly the same:
Your first example: the element with your directive has the span as its only child
Your second example: the element with your directive is finally replaced with the span -> one level less in the DOM.
All is about what you want in the DOM at the end. If you consider the original container with the directive is a useless wrapper only declaring a component, you will want to unwrap the content.
Related
I have a custom directive that generates an input with its validation errors , eventually after building the input, here is the part I'm wondering about :
var html= template.get();
element.html(html);
$compile(element)(scope);
I also added to the directive object which I think is not making difference since I don't have template in the object or should it?
replace:true
but yet the directive element DOM is still there , and the generated input is being appended as a child , can you help me out in this ?
Yes, replace is used in conjunction with template/templateUrl.
For the templates that are retrieved dynamically in link, use
var html= template.get();
element.replaceWith($compile(html)(scope));
Notice that obvious drawback in comparison with replace is that directive's attributes won't be translated to template element, this has to be done manually.
Its not working because you haven't provided a template parameter and your link function was manually adding the elements. Remove the link function and add a template parameter like so and it'll work. Ive updated your fiddle with it working too
app.directive('test',function($compile){
return {
restrict:'E',
replace:true,
template:'<input type="text" name="test">'
}
});
In the angular documentation for the compile service (starting at line 412) there is a description of the transclude function that is passed into the linking function of a directive.
The relevant part reads:
function([scope], cloneLinkingFn, futureParentElement)
In which (line 212):
futureParentElement: defines the parent to which the cloneLinkingFn will add the cloned elements.
default: $element.parent() resp. $element for transclude:'element' resp. transclude:true.
only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
and when the cloneLinkinFn is passed,
as those elements need to created and cloned in a special way when they are defined outside their usual containers (e.g. like <svg>).
See also the directive.templateNamespace property.
I fail to see the point of futureParentElement however. It says
defines the parent to which the cloneLinkingFn will add the cloned elements.
But you do that in the cloneLinkingFn itself like this:
transclude(scope, function (clone) {
some_element.append(clone);
});
And you can't use the transclude function without defining the cloning function in the first place.
What is the proper usage/a use for futureParentElement?
The answer to this can be found by looking at the the git blame of compile.js: the commit that added futureParentElement is https://github.com/angular/angular.js/commit/ffbd276d6def6ff35bfdb30553346e985f4a0de6
In the commit there is a test that tests a directive svgCustomTranscludeContainer
directive('svgCustomTranscludeContainer', function() {
return {
template: '<svg width="400" height="400"></svg>',
transclude: true,
link: function(scope, element, attr, ctrls, $transclude) {
var futureParent = element.children().eq(0);
$transclude(function(clone) {
futureParent.append(clone);
}, futureParent);
}
};
});
by testing how compiling the html <svg-custom-transclude-container><circle cx="2" cy="2" r="1"></circle> behaves:
it('should handle directives with templates that manually add the transclude further down', inject(function() {
element = jqLite('<div><svg-custom-transclude-container>' +
'<circle cx="2" cy="2" r="1"></circle></svg-custom-transclude-container>' +
'</div>');
$compile(element.contents())($rootScope);
document.body.appendChild(element[0]);
var circle = element.find('circle');
assertIsValidSvgCircle(circle[0]);
}));
So it looks like if you are creating an SVG image with a directive whose template wraps transcluded SVG content in <svg> ... </svg> tags, then that SVG image won't be valid (by some definition), if you don't pass the correct futureParentElement to $transclude.
Trying to see what it actually means not to be valid, beyond the test in the source code, I created 2 directives based on the ones in the unit test, and used them to try to create an SVG image with partial circle. One using the futureParentElement:
<div><svg-custom-transclude-container-with-future><circle cx="1" cy="2" r="20"></circle></svg-custom-transclude-container></div>
and one that is identical but that doesn't:
<div><svg-custom-transclude-container-without-future><circle cx="2" cy="2" r="20"></circle></svg-custom-transclude-container></div>
As can be seen at http://plnkr.co/edit/meRZylSgNWXhBVqP1Pa7?p=preview, the one with the futureParentElement shows the partial circle, and the one without doesn't. The structure of the DOM appears identical. However Chrome seems to report that the second circle element isn't an SVG node, but a plain HTML node.
So whatever futureParentElement actually does under the hood, it seems to make sure that transcluded SVG content ends up being handled as SVG by the browser.
I try do something with directive in angular, but I've some problem with $compile function in programmatically html element, call here "phe".
var phe = angular.element('<div style="background-color:orange">{{value}}</div>');
When I append "phe" after or before the directive's element, it work like a charm...
var $phe = $compile(phe)(scope);
element.after($phe);
but if I wrapped the directive element with this "phe" the $compile not work.
element.wrap($phe);
Somebody have some idea?
I have made a plunker http://plnkr.co/edit/0x2MmQ7WYmiNog0IEzTj?p=preview
it works if you change the compilation sequence... compile the element before placing it in the dom
var phe_b = angular.element('<div style="background-color:orange"> b {{value}}</div>');
var $phe_b = $compile(phe_b)(scope);
element.before($phe_b);
do same for after...
The reason it doesn't work with wrap is because wrap clones the DOM element. In other words, if you did:
var wrapper = angular.element("<div>");
element.wrap(wrapper);
console.log(wrapper[0] !== element[0].parentNode); // this would output "true"
So, the element that you compiled/linked is not the same that ends up in the DOM.
You could, of course, get the wrapping element (it's the return value of wrap) and $compile it, but you need to be careful not to re-compile/re-link certain directives that were applied on the current element (including the very same directive) and its children.
Let's say I have a boolean format that controls how a collection of items should be displayed by my directive. Depending on the value of format, my html changes radically.
So far I'm using two separate templates, and I dynamically recompile the directive whenever format changes, by using $watch(format) in the link function of the directive.
I wonder if it would be faster to use only one template, and split all my code between a big ng-show="format" and a ng-hide="format". What's the best practice in that situation?
$compile is expensive (relatively). Your use case sounds like a candidate for ng-switch, so don't reinvent the wheel if ng-switch works for you.
However, inside your directive, you can cache the compilation of each template and then re-link them by calling the link function returned by $compile. This would be pretty much as performant as ng-switch .
In pseudo code your link function would look like:
link: function(scope, element, attrs) {
//create and cache your template link functions
var format1link = $compile(template1);
var format2link = $compile(template2);
scope.$watch('format') {
//call appropriate link function based on the value of format
var newElement = format1link(scope);
//or
var newElement = format2link(scope);
//replace element contents with newElement
});
}
i have writted a directive to generate input fields from scope fields, everything is working fine except that the parent ng-form stays invalid even though the ng-form within directive is invalid.
this is how i am checking the state of the form:
<ng-form name="parentForm" class="form-horizontal">
<form-field ng-model="input.name" field="fields[0]" ng-change="changed(input.name)"></form-field>
<form-field ng-model="input.age" field="fields[1]"></form-field>
<pre> parent form: valid : {{parentForm.$valid}}</pre>
</ng-form>
and below is the link function
var linkFunction = function (scope, element, attrs) {
var fieldGetter = $parse(attrs.field);
var field = fieldGetter(scope);
var template = input(field, attrs); //genrate the template
element.replaceWith($compile(template)(scope)); //replace element with templated code
};
i guess the problem is that i need to compile the parent element rather than the element itself to get validations working, but not sure on how to do it
element.replaceWith($compile(template)(scope));
PLUNKER LINK
According to the docs on FormController, there is the $addControl() method that is used to:
Register a control with the form.
Input elements using ngModelController do this automatically when they are linked.
This gives us the hint that the "ng-modeled" elements will take care of everything in their link function as long as we give them the chance. Giving them the chance (in this context) means that they should be able to locate their parent ngForm element while linking.
You have been (quite small-heartedly) depriving them of this right, by first compiling and linking them and only then inserting them into the DOM (shame on you).
Once you know the cause, the solution is easy:
You need to first insert them into the DOM and then link them.
E.g.:
// Instead of this:
element.replaceWith($compile(template)(scope));
// Which is equivalent to this:
var linkFunc = $compile(template); // compiling
var newElem = linkFunc(scope); // linking
element.replaceWith(newElem); // inserting
// You should do this:
var newElem = angular.element(template); // creating
element.replaceWith(newElem); // inserting
$compile(newElem)(scope); // compiling + linking
// (could be done in 2 steps
// but there is no need)
See, also, this short demo.