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

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: '#'
}
};
})

Related

Provide template with expressions as attribute on directive

I'm wanting to pass a "template" into a directive, by means of an attribute. Here's a trite example of what I'm trying to accomplish:
This HTML:
<greeter person-name="Jim" greeting-template="Hello {{name}}"></greeter>
Would produce output: Hello Jim.
I've tried with a directive like this:
function greeter($interpolate) {
var directive = {
link: link,
restrict: 'EA',
template: '<div>{{evaluatedTemplate}}</div>'
};
return directive;
function link(scope, element, attrs) {
scope.name = attrs.personName;
scope.evaluatedTemplate = $interpolate(attrs.greetingTemplate)(scope);
}
}
But that doesn't work, because {{name}} in the greeting-template attribute gets evaluated in the parent scope before it gets as far as the directive link function.
Ultimately, I would need the value of attrs.greetingTemplate to literally be a string of: 'Hello {{name}}'. I figure I could do it with some alternative syntax, like having the greeting-template attribute value as: "Hello [name]" and convert "[" to "{{" before interpolation. But that feels messy. I looked at transclusion too, but the way it evaluates the directive against the parent scope looks like it could cause issues when I have multiple greeter's.
Instead of using the link function, you could use the compile function, which runs before any linking to a scope occurs, and gets passed the template element (the original DOM element) as well as its uninterpolated attributes as arguments. I think that's what you're looking for here.
In the compile function, you could store the uninterpolated template string in a variable for later use in your post-link function (which is the same as the link function if you use link rather than compile), where you can then bind it to your scope.
So your directive would look like this, with a compile property rather than a link property:
function greeter($interpolate) {
var directive = {
compile: compile,
restrict: 'EA',
scope: true,
template: '<div>{{evaluatedTemplate}}</div>'
};
return directive;
function compile(tElement, tAttrs) {
// save the uninterpolated template for use in our post-link function
var greetingTemplateUninterpolated = tAttrs.greetingTemplate;
return {
pre: function (scope, element, attrs) {},
post: function (scope, element, attrs) {
scope.name = attrs.personName;
scope.evaluatedTemplate = $interpolate(greetingTemplateUninterpolated)(scope);
}
};
}
}
Here's a fiddle showing it working.
And here's a really good article explaining how compile and link work.

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);
};
}
}
});

Should I use isolate scope in this case?

I'm implementing an custom input widget. The real code is more complex, but generally it looks like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
compile: function (tElement, tAttributes){
//flow the bindings from the parent.
//I can do it dynamically, this is just a demo for the idea
tElement.find("input").attr("placeholder", tAttributes.placeholder);
tElement.find("input").attr("ng-model", tElement.attr("ng-model"));
}
};
});
inputWidget.html:
<div>
<input />
<span>
</span>
</div>
To use it:
<input-widget placeholder="{{name}}" ng-model="someProperty"></input-widget>
The placeholder is displayed correctly with above code because it uses the same scope of the parent: http://plnkr.co/edit/uhUEGBUCB8BcwxqvKRI9?p=preview
I'm wondering if I should use an isolate scope, like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope : {
placeholder: "#"
//more properties for ng-model,...
}
};
});
With this, the directive does not share the same scope with the parent which could be a good design. But the problem is this isolate scope definition will quickly become messy as we're putting DOM-related properties on it (placeholder, type, required,...) and every time we need to apply a new directive (custom validation on the input-widget), we need to define a property on the isolate scope to act as middle man.
I'm wondering whether it's a good idea to always define isolate scope on directive components.
In this case, I have 3 options:
Use the same scope as the parent.
Use isolate scope as I said above.
Use isolate scope but don't bind DOM-related properties to it, somehow flow the DOM-related properties from the parent directly. I'm not sure if it's a good idea and I don't know how to do it.
Please advice, thanks.
If the input-widget configuration is complex, I would use an options attribute, and also an isolated scope to make the attribute explicit and mandatory:
<input-widget options="{ placeholder: name, max-length: 5, etc }"
ng-model="name"></input-widget>
There is no need to flow any DOM attributes if you have the options model, and the ngModel:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope: { options:'=', ngModel: '='}
};
});
And in your template, you can bind attributes to your $scope view model, as you normally would:
<div>
<input placeholder="{{options.placeholder}}" ng-model="ngModel"/>
<span>
{{options}}
</span>
</div>
Demo
Personally, when developing for re-use, I prefer to use attributes as a means of configuring the directive and an isolated scope to make it more modular and readable. It behaves more like a component and usually without any need for outside context.
However, there are times when I find directives with child / inherited scopes useful. In those cases, I usually 'require' a parent directive to provide the context. The pair of directives work together so that less attributes has to flow to the child directive.
This is not a very trivial problem. This is because one could have arbitrary directives on the templated element that are presumably intended for <input>, and a proper solution should ensure that: 1) these directives compile and link only once and 2) compile against the actual <input> - not <input-widget>.
For this reason, I suggest using the actual <input> element, and add inputWidget directive as an attribute - this directive will apply the template, while the actual <input> element would host the other directives (like ng-model, ng-required, custom validators, etc...) that could operate on it.
<input input-widget
ng-model="someProp" placeholder="{{placeholder}}"
ng-required="isRequired"
p1="{{name}}" p2="name">
and inputWidget will use two compilation passes (modeled after ngInclude):
app.directive("inputWidget", function($templateRequest) {
return {
priority: 400,
terminal: true,
transclude: "element",
controller: angular.noop,
link: function(scope, element, attrs, ctrl, transclude) {
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
ctrl.template = templateHtml;
transclude(scope, function(clone) {
element.after(clone);
});
});
}
};
});
app.directive("inputWidget", function($compile) {
return {
priority: -400,
require: "inputWidget",
scope: {
p1: "#", // variables used by the directive itself
p2: "=?" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var templateEl = angular.element(ctrl.template);
element.after(templateEl);
$compile(templateEl)(scope);
templateEl.find("placeholder").replaceWith(element);
}
};
});
The template (inputWidget.template.html) has a <placeholder> element to mark where to place the original <input> element:
<div>
<pre>p1: {{p1}}</pre>
<div>
<placeholder></placeholder>
</div>
<pre>p2: {{p2}}</pre>
</div>
Demo
(EDIT) Why 2 compilation passes:
The solution above is a "workaround" that avoids a bug in Angular that was throwing with interpolate values being set on a comment element, which is what is left when transclude: element is used. This was fixed in v1.4.0-beta.6, and with the fix, the solution could be simplified to:
app.directive("inputWidget", function($compile, $templateRequest) {
return {
priority: 50, // has to be lower than 100 to get interpolated values
transclude: "element",
scope: {
p1: "#", // variables used by the directive itself
p2: "=" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var dirScope = scope,
outerScope = scope.$parent;
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
transclude(outerScope, function(clone) {
var templateClone = $compile(templateHtml)(dirScope);
templateClone.find("placeholder").replaceWith(clone);
element.after(templateClone);
});
});
}
};
});
Demo 2

Getting the children of an element with an attribute directive

I have a directive that is restricted to attribute. I want to do either of 2 things. If a certain condition is met, I want to use the children for the element with the directive attribute as the model (thats the content) or if that condition is not met then I want to data bind from a service instead, so the directive would replace the children with something i was given. I have a directive that is doing the latter but Im finding it very hard to have it grab the children before the compiler comes in and replaces it with its template... Anyone know how this is done if its possible?
I think what you're looking for is element.context in your directive's link (or compile) function.
Inside your link function (pre or post), the original element that your directive was found on is stored in the passed-in element's context property. So if your service call returns no data, you can just replace the compiled element with the original element by doing element.replaceWith(element.context).
So your directive would look something like this:
.directive('ttContent', ['mySvc', function (mySvc) {
return {
restrict: 'A',
replace: true,
transclude: false,
template: '<div class="content new-content" ng-bind-html="htmlContent | sanitize"></div>',
scope: {
testDataReturned: '#'
},
link: {
pre: function (scope, element, attrs) {
},
post: function (scope, element, attrs){
mySvc.fetchContent().then(function success(data){
if (data) {
scope.htmlContent = data;
} else {
// element.context is our original, pre-compiled element
element.replaceWith(element.context);
}
}, function fail(data){
element.replaceWith(element.context);
});
}
}
};
}]);
Here's a plunk.

Passing all arbitrary attributes from html definition to a template element

I'm making a number of form directives with angular to reuse and I'd like to pass all attributes put on my directive to the input tag in my template.
So if they wrote:
<jays-input required class="well"></jays-input>
Even though my directive doesn't formally take the attributes required or class, it would transfer them to the "input" part of my template. So my template would be something like:
<input placeholder="jays input" {{$all_attributes}} />
...Where $all_attributes would be 'required class="well"'. Even better would be just the attributes that don't match what the directive expects.
While writing this I realized I could just parse the $attrs array my self, but I wonder if there is a shorthand or something. I feel like it would be common to make a directive that merely wraps html around a specific element and in that case you'd want to transfer all attributes to the focal element.
In your directive definition's compile function, add the attributes using jQuery:
app.directive('jaysInput', function() {
return {
restrict: 'E',
compile: function(element, attr) {
$('input', element).each(function(){
for (var i in attr)
{
$(this).attr(i, attr[i]);
}
}
return function(scope, element, attr) {
// return link function
};
}
}
});

Resources