Passing all arbitrary attributes from html definition to a template element - angularjs

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

Related

Create Angular directive that keeps original element's attributes

I've seen other SO questions that kind of ask this, but they usually have some unique complication thrown in, and the answer seems surprisingly complicated in proportion to the simplicity of the desired result. I'd like to ask the simplest possible version of this question.
Let's say I have a directive that looks like this:
<my-input type="text" placeholder="foo">
I want the output to look like this:
<input type="text" placeholder="foo" class="bar">
All I want to do is output a new element and add some new attributes, but keep the original element's attributes. How can this be achieved?
Use an attribute for the directive rather than an element:
<input type="text" placeholder="foo" my-directive>
And make myDirective add the desired new attributes to its element.
You directive template function will give access to your all attributes defined on element. You can easily add those attribute inside html element.
Directive
app.directive('myInput', function(){
return {
restrict: 'E',
template: function(element, attrs){
var template = '<input type="'+ attrs.text +'"/>'
//add other attributes from here
return template;
}
}
});
To my knowledge there is not straight forward way to copy all attributes from one element to another, so you would need to loop through then or know what ones you want to support.
The following is the start of a directive that will replace the element the has the directive with a different element (input, in this case)
.directive('myInput', function() {
return {
replace: true,
template: function(element, attrs) {
var inputElem = angular.element('input');
// Copy attributes from element to inputElem;
angular.forEach(element[0].attributes, function(attr) {
inputElem[0].setAttribute(attr.name, attr.value);
})
// Add new attributes
return inputElem;
}
}
})

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

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

How to pass all attributes passed to my custom directive to the control used inside directive template?

Can someone help me with this problem?
Let's say I have a custom directive called mycontrol. I want to pass all the attributes in this directive to a control used inside my directive template. Here is an example -
<mycontrol data-var1="value1" data-var2="value2"..../>
My template is - <input/>
How do I define the directive definition object to get the output as
<input data-var1="value1" data-var2="value2"...>
Please note that the attributes number and names are not fixed.
All of the attributes should be carried over.
Look at this fiddle: http://jsfiddle.net/XgA6T/
Open the console and look at the element that is logged.
The directive looks like this:
myApp.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<input>',
link: function(scope, elem, attrs) {
console.log(elem);
var domElem = elem[0];
if (domElem.dataset && domElem.dataset != null) {
console.log(domElem.dataset);
scope.values = domElem.dataset;
}
}
};
});

wrapping inputs in directives in angular

I had the idea to wrap inputs into custom directives to guarantee a consistent look and behavior through out my site. I also want to wrap bootstrap ui's datepicker and dropdown. Also, the directive should handle validation and display tooltips.
The HTML should look something like this:
<my-input required max-length='5' model='text' placeholder='text' name='text'/>
or
<my-datepicker required model='start' placeholder='start' name='start'/>
in the directives i want to create a dom structure like:
<div>
<div>..</div> //display validation in here
<div>..</div> //add button to toggle datepicker (or other stuff) in here
<div>..</div> //add input field in here
</div>
I tried various ways to achieve this but always came across some tradeoffs:
using transclude and replace to insert the input into the directives dom structure (in this case the directive would be restricted to 'A' not 'E' like in the example above). The problem here is, that there is no easy way to access the transcluded element as I want to add custom attributes in case of datepicker. I could use the transclude function and then recompile the template in the link function, but this seems a bit complex for this task. This also leads to problems with the transcluded scope and the toggle state for the datepicker (one is in the directives scope, the other in the transcluded scope).
using replace only. In this case, all attributes are applied to the outermost div (even if I generate the template dom structure in the compile function). If I use just the input as template, then the attributes are on the input, but I need to generate the template in the link function an then recompile it. As far as I understand the phase model of angular, I would like to avoid recompiling and changing the template dom in the link function (although I've seen many people doing this).
Currently I'm working with the second approach and generating the template in the link function, but I was wondering if someone had some better ideas!
Here's what I believe is the proper way to do this. Like the OP I wanted to be able to use an attribute directive to wrapper an input. But I also wanted it to work with ng-if and such without leaking any elements. As #jantimon pointed out, if you don't cleanup your wrapper elements they will linger after ng-if destroys the original element.
app.directive("checkboxWrapper", [function() {
return {
restrict: "A",
link: function(scope, element, attrs, ctrl, transclude) {
var wrapper = angular.element('<div class="wrapper">This input is wrappered</div>');
element.after(wrapper);
wrapper.prepend(element);
scope.$on("$destroy", function() {
wrapper.after(element);
wrapper.remove();
});
}
};
}
]);
And here's a plunker you can play with.
IMPORTANT: scope vs element $destroy. You must put your cleanup in scope.$on("$destroy") and not in element.on("$destroy") (which is what I was originally attempting). If you do it in the latter (element) then an "ngIf end" comment tag will get leaked. This is due to how Angular's ngIf goes about cleaning up its end comment tag when it does its falsey logic. By putting your directive's cleanup code in the scope $destroy you can put the DOM back like it was before you wrappered the input and so ng-if's cleanup code is happy. By the time element.on("$destroy") is called, it is too late in the ng-if falsey flow to unwrap the original element without causing a comment tag leak.
Why not doing a directive like that?
myApp.directive('wrapForm', function(){
return {
restrict: 'AC',
link: function(scope, inputElement, attributes){
var overallWrap = angular.element('<div />');
var validation = angular.element('<div />').appendTo(overallWrap);
var button = angular.element('<div />').appendTo(overallWrap);
var inputWrap = angular.element('<div />').appendTo(overallWrap);
overallWrap.insertBefore(inputElement);
inputElement.appendTo(inputWrap);
inputElement.on('keyup', function(){
if (inputElement.val()) {
validation.text('Just empty fields are valid!');
} else {
validation.text('');
}
});
}
}
});
Fiddle: http://jsfiddle.net/bZ6WL/
Basically you take the original input field (which is, by the way, also an angularjs directive) and build the wrappings seperately. In this example I simply build the DIVs manually. For more complex stuff, you could also use a template which get $compile(d) by angularjs.
The advantage using this class or html attribute "wrapForm": You may use the same directive for several form input types.
Why not wrap the input in the compile function?
The advantage is that you will not have to copy attributes and will not have to cleanup in the scope destroy function.
Notice that you have to remove the directive attribute though to prevent circular execution.
(http://jsfiddle.net/oscott9/8er3fu0r/)
angular.module('directives').directive('wrappedWithDiv', [
function() {
var definition = {
restrict: 'A',
compile: function(element, attrs) {
element.removeAttr("wrapped-with-div");
element.replaceWith("<div style='border:2px solid blue'>" +
element[0].outerHTML + "</div>")
}
}
return definition;
}
]);
Based on this: http://angular-tips.com/blog/2014/03/transclusion-and-scopes/
This directive does transclusion, but the transcluded stuff uses the parent scope, so all bindings work as if the transcluded content was in the original scope where the wrapper is used. This of course includes ng-model, also min/max and other validation directives/attributes. Should work for any content. I'm not using the ng-transclude directive because I'm manually cloning the elements and supplying the parent(controller's) scope to them. "my-transclude" is used instead of ng-transclude to specify where to insert the transcluded content.
Too bad ng-transclude does not have a setting to control the scoping. It would make all this clunkyness unnecessary.
And it looks like they won't fix it: https://github.com/angular/angular.js/issues/5489
controlsModule.directive('myWrapper', function () {
return {
restrict: 'E',
transclude: true,
scope: {
label: '#',
labelClass: '#',
hint: '#'
},
link: link,
template:
'<div class="form-group" title="{{hint}}"> \
<label class="{{labelClass}} control-label">{{label}}</label> \
<my-transclude></my-transclude> \
</div>'
};
function link(scope, iElement, iAttrs, ctrl, transclude) {
transclude(scope.$parent,
function (clone, scope) {
iElement.find("my-transclude").replaceWith(clone);
scope.$on("$destroy", function () {
clone.remove();
});
});
}
});

Resources