angularjs directive with dynamic templates and string interpolation - angularjs

The idea is to replace the directive element with the dynamic template which refers to interpolated strings.
If I use element.html() in my directive then the strings are interpolated fine but this leaves the original custom directive html element.
If I use element.replaceWith() then strings are not interpolated. I guess it has related to scope but can't figure out what's wrong.
Plunker: http://plnkr.co/edit/HyBP9d?p=preview
UPDATE
Found the solution. Using element.replaceWith($compile(html)(scope)); works.
Updated plunker: http://plnkr.co/edit/HyBP9d?p=preview

Not sure what objective is regarding replaceWIth. The problem is likely that when you replace an elememnt, you replace all events and data bound to it. This would include the angular scope for the element.
For demo provided could do it like this:
app.directive('status', function($compile) {
var linker = function(scope, element, attrs) {
element.contents().wrap('<h'+attrs.value+'>')
};
return {
restrict: 'E',
replace: true,
template:'<div>{{value}}</div>',
transclude: true,
link: linker,
scope: {
value: '='
}
};
});
DEMO:http://plnkr.co/edit/QDxIwE?p=preview

Related

How to prevent duplicated attributes in angular directive when replace=true

I've found that angular directives that specify replace: true will copy attributes from the directive usage into the output rendered by the template. If the template contains the same attribute, both the template attribute value and the directive attribute value will be combined together in the final output.
Directive usage:
<foo bar="one" baz="two"></foo>
Directive:
.directive('foo', function() {
return {
restrict: 'E',
replace: true,
template: '<div bar="{{bar}}" baz="baz"></div>',
scope: {
bar: '#'
},
link: function(scope, element, attrs, parentCtrl) {
scope.bar = scope.bar || 'bar';
}
};
})
Output:
<div bar="one " baz="two baz" class="ng-isolate-scope"></div>
The space in bar="one " is causing problems, as is multiple values in baz. Is there a way to alter this behavior? I realized I could use non-conflicting attributes in my directive and have both the template attributes and the non-conflicting attributes in the output. But I'd like to be able to use the same attribute names, and control the output of the template better.
I suppose I could use a link method with element.removeAttr() and element.attr(). It just seems like there should be a better solution.
Lastly, I realize there is talk of deprecating remove: true, but there are valid reasons for keeping it. In my case, I need it for directives that generate SVG tags using transclusion. See here for details:
https://github.com/angular/angular.js/commit/eec6394a342fb92fba5270eee11c83f1d895e9fb
No, there isn't some nice declarative way to tell Angular how x attribute should be merged or manipulated when transplanted into templates.
Angular actually does a straight copy of attributes from the source to the destination element (with a few exceptions) and merges attribute values. You can see this behaviour in the mergeTemplateAttributes function of the Angular compiler.
Since you can't change that behaviour, you can get some control over attributes and their values with the compile or link properties of the directive definition. It most likely makes more sense for you to do attribute manipulation in the compile phase rather than the link phase, since you want these attributes to be "ready" by the time any link functions run.
You can do something like this:
.directive('foo', function() {
return {
// ..
compile: compile
// ..
};
function compile(tElement, tAttrs) {
// destination element you want to manipulate attrs on
var destEl = tElement.find(...);
angular.forEach(tAttrs, function (value, key) {
manipulateAttr(tElement, destEl, key);
})
var postLinkFn = function(scope, element, attrs) {
// your link function
// ...
}
return postLinkFn;
}
function manipulateAttr(src, dest, attrName) {
// do your manipulation
// ...
}
})
It would be helpful to know how you expect the values to be merged. Does the template take priority, the element, or is some kind of merge needed?
Lacking that I can only make an assumption, the below code assumes you want to remove attributes from the template that exist on the element.
.directive('foo', function() {
return {
restrict: 'E',
replace: true,
template: function(element, attrs) {
var template = '<div bar="{{bar}}" baz="baz"></div>';
template = angular.element(template);
Object.keys(attrs.$attr).forEach(function(attr) {\
// Remove all attributes on the element from the template before returning it.
template.removeAttr(attrs.$attr[attr]);
});
return template;
},
scope: {
bar: '#'
}
};
})

AngularJS: Retrieve Element Name from Directive

I have spent some time looking for this but I haven't found anything.
I have the following
HTML file:
<my-directive name="someName" id="someId" method="somemethod">
sometext
</my-directive>
My directive:
app.directive('myDirective', function() {
return {
restrict: 'EA',
templateUrl: "example.html",
transclude: true,
link: function(scope, element, attrs)
{
alert(element.name); //Used for testing, Not working
}
};
});
I am trying to access the element parameters in the directive (name, method, id) but I am unable to figure out how.
Thanks in advance.
Please have a look at this Plnkr
You have the attrs as parameter inside the link function. Use that instead of the element.
link: function(scope, element, attrs) {
scope.result = attrs['name'];
}
You are also using transclusion, but you haven't defined a "ng-transclude" attribute in the template.
Using an alert for testing is very bad practice. You should be writing an assertion that specifically looks for the attribute you want (name in this case) and verifying that it is what you expect it to be. As commenter doodeec above said, you'll find the value you need under attrs.name. References to the element may also need to be element[0] to ensure that you do not get an undefined or null value. Lastly, you have your directive binding to both element and attribute, which seems to be a less than optimal situation. Were I you, I would bind to one or the other, but not both. It'll make for cleaner code in both places and remove some spaghetti.

Angular Directive Passing Template Name in as an attribute

if I wanted to pass into angular an attribute which is a template name stored on the templateCache, would doing something like the following be a good approach?
app.directive('myPopup', function() {
var directive = {
link: link,
scope {
'template': '#'
}
template: $templateCache.get(template);
restrict: 'E'
};
return directive;
function link(scope, element, attrs) {
}
});
Or should it be done on the compile statement, I guess so for performace??
I'm looking to ultimately wrap a bootstrap popover and then be able to provided html content through a template attribute allowing me to reuse popovers by providing different templates.
Can anyone offer any suggestions for the best course of action?
Thanks

Illegal use of ngTransclude directive in the template

I have two directive
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: 'element',
compile: function (element, attr, linker) {
return function (scope, element, attr) {
var parent = element.parent();
linker(scope, function (clone) {
parent.prepend($compile( clone.children()[0])(scope));//cause error.
// parent.prepend(clone);// This line remove the error but i want to access the children in my real app.
});
};
}
}
});
app.directive('panel', function ($compile) {
return {
restrict: "E",
replace: true,
transclude: true,
template: "<div ng-transclude ></div>",
link: function (scope, elem, attrs) {
}
}
});
And this is my view :
<panel1>
<panel>
<input type="text" ng-model="firstName" />
</panel>
</panel1>
Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <div class="ng-scope" ng-transclude="">
I know that panel1 is not a practical directive. But in my real application I encounter this issue too.
I see some explanation on http://docs.angularjs.org/error/ngTransclude:orphan. But I don't know why I have this error here and how to resolve it.
EDIT
I have created a jsfiddle page. Thank you in advance.
EDIT
In my real app panel1 does something like this:
<panel1>
<input type="text>
<input type="text>
<!--other elements or directive-->
</panel1>
result =>
<div>
<div class="x"><input type="text></div>
<div class="x"><input type="text></div>
<!--other elements or directive wrapped in div -->
</div>
The reason is when the DOM is finished loading, angular will traverse though the DOM and transform all directives into its template before calling the compile and link function.
It means that when you call $compile(clone.children()[0])(scope), the clone.children()[0] which is your <panel> in this case is already transformed by angular.
clone.children() already becomes:
<div ng-transclude="">fsafsafasdf</div>
(the panel element has been removed and replaced).
It's the same with you're compiling a normal div with ng-transclude. When you compile a normal div with ng-transclude, angular throws exception as it says in the docs:
This error often occurs when you have forgotten to set transclude:
true in some directive definition, and then used ngTransclude in the
directive's template.
DEMO (check console to see output)
Even when you set replace:false to retain your <panel>, sometimes you will see the transformed element like this:
<panel class="ng-scope"><div ng-transclude=""><div ng-transclude="" class="ng-scope"><div ng-transclude="" class="ng-scope">fsafsafasdf</div></div></div></panel>
which is also problematic because the ng-transclude is duplicated
DEMO
To avoid conflicting with angular compilation process, I recommend setting the inner html of <panel1> as template or templateUrl property
Your HTML:
<div data-ng-app="app">
<panel1>
</panel1>
</div>
Your JS:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>",
}
});
As you can see, this code is cleaner as we don't need to deal with transcluding the element manually.
DEMO
Updated with a solution to add elements dynamically without using template or templateUrl:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<div></div>",
link : function(scope,element){
var html = "<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>";
element.append(html);
$compile(element.contents())(scope);
}
}
});
DEMO
If you want to put it on html page, ensure do not compile it again:
DEMO
If you need to add a div per each children. Just use the out-of the box ng-transclude.
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div><div ng-transclude></div></div>" //you could adjust your template to add more nesting divs or remove
}
});
DEMO (you may need to adjust the template to your needs, remove div or add more divs)
Solution based on OP's updated question:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div ng-transclude></div>",
link: function (scope, elem, attrs) {
elem.children().wrap("<div>"); //Don't need to use compile here.
//Just wrap the children in a div, you could adjust this logic to add class to div depending on your children
}
}
});
DEMO
You are doing a few things wrong in your code. I'll try to list them:
Firstly, since you are using angular 1.2.6 you should no longer use the transclude (your linker function) as a parameter to the compile function. This has been deprecated and should now be passed in as the 5th parameter to your link function:
compile: function (element, attr) {
return function (scope, element, attr, ctrl, linker) {
....};
This is not causing the particular problem you are seeing, but it's a good practice to stop using the deprecated syntax.
The real problem is in how you apply your transclude function in the panel1 directive:
parent.prepend($compile(clone.children()[0])(scope));
Before I go into what's wrong let's quickly review how transclude works.
Whenever a directive uses transclusion, the transcluded content is removed from the dom. But it's compiled contents are acessible through a function passed in as the 5th parameter of your link function (commonly referred to as the transclude function).
The key is that the content is compiled. This means you should not call $compile on the dom passed in to your transclude.
Furthermore, when you are trying to insert your transcluded DOM you are going to the parent and trying to add it there. Typically directives should limit their dom manipulation to their own element and below, and not try to modify parent dom. This can greatly confuse angular which traverses the DOM in order and hierarchically.
Judging from what your are trying to do, the easier way to accomplish it is to use transclude: true instead of transclude: 'element'. Let's explain the difference:
transclude: 'element' will remove the element itself from the DOM and give you back the whole element back when you call the transclude function.
transclude: true will just remove the children of the element from the dom, and give you the children back when you call your transclude.
Since it seems you care only about the children, you should use transclude true (instead of getting the children() from your clone). Then you can simply replace the element with it's children (therefore not going up and messing with the parent dom).
Finally, it is not good practice to override the transcluded function's scope unless you have good reason to do so (generally transcluded content should keep it's original scope). So I would avoid passing in the scope when you call your linker().
Your final simplified directive should look something like:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.replaceWith(clone);
});
}
}
});
Ignore what was said in the previous answer about replace: true and transclude: true. That is not how things work, and your panel directive is fine and should work as expected as long as you fix your panel1 directive.
Here is a js-fiddle of the corrections I made hopefully it works as you expect.
http://jsfiddle.net/77Spt/3/
EDIT:
It was asked if you can wrap the transcluded content in a div. The easiest way is to simply use a template like you do in your other directive (the id in the template is just so you can see it in the html, it serves no other purpose):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv' ng-transclude></div>"
}
});
Or if you want to use the transclude function (my personal preference):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv'></div>",
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.append(clone);
});
}
}
});
The reason I prefer this syntax is that ng-transclude is a simple and dumb directive that is easily confused. Although it's simple in this situation, manually adding the dom exactly where you want is the fail-safe way to do it.
Here's the fiddle for it:
http://jsfiddle.net/77Spt/6/
I got this because I had directiveChild nested in directiveParent as a result of transclude.
The trick was that directiveChild was accidentally using the same templateUrl as directiveParent.

ANGULAR: Not able to pass string containing {{}} in directive scope using # after upgrading to angular version to 1.2.3

I'm passing "/foo/bar/{{value}}" as a string using # to the directive so I can use the interpolate method to construct hrefs for a dropdown. I think angular is looking for value when compiling instead of passing the whole thing as a string. This was working fine in the angular v1.2.0.
The directive code goes like,
return {
templateUrl: '/views/directives/directive-name.html',
restrict: 'E',
replace: true,
scope: {
title: '#',
optHref: '#'
},
link: link: function postLink(scope, element, attrs) {
var hrefFormatter;
hrefFormatter = $interpolate(attrs.optHref);
scope.getHref = function(value, label) {
return hrefFormatter({ value: value, label: label });
};
}
Invoked like,
<directive-name
title ="name"
opt-href="/foo/bar/{{value}}" <
</directive-name>
Appreciate any pointers on what might have changed in angular to cause this or other pointers.
Instead of:
opt-href="/foo/bar/{{value}}"
Try this:
opt-href="'/foo/bar/{{value}}'"
Mind the additional '.
Thanks Michal! Used a controller function before the link to preserve the attrs.optHref in the scope. Something like,
controller: function ($scope, $element, $attrs) {
$scope.optHref = $attrs.optHref;
}
In the controller function the {{value}} was already interpolated in scope but existed in attrs. But in the link fn {{value}} was interpolated in attrs as well- so preserved it in scope and used scope.optHrefin the link fn.

Resources