Create Angular directive that keeps original element's attributes - angularjs

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

Related

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

AngularJS : directives which take a template through a configuration object, and show that template multiple times

I'm looking to create a custom directive that will take a template as a property of a configuration object, and show that template a given number of times surrounded by a header and footer. What's the best approach to create such a directive?
The directive would receive the configuration object as a scope option:
var app = angular.module('app');
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
config: '=?'
}
...
}
}
This object (called config) is passed optionally to the directive using two way binding, as show in the code above. The configuration object can include a template and a number indicating the number of times the directive should show the template. Consider, for example, the following config object:
var config = {
times: 3,
template: '<div>my template</div>'
};
It would, when passed to the directive, cause the directive to show the template five times (using an ng-repeat.) The directive also shows a header and a footer above and below the template(s):
<div>the header</div>
<div>my template</div>
<div>my template</div>
<div>my template</div>
<div>the footer</div>
What's the best way to implement this directive? Note: When you reply, please provide a working example in a code playground such as Plunker, as I've run into problems with each possible implementation I've explored.
Update, the solutions I've explored include:
The use of the directive's link function to append the head, template with ng-repeat, and footer. This suffers from the problem of the template not being repeated, for some unknown reason, and the whole solutions seems like a hack.
The insertion of the template from the configuration object into middle of the template of the directive itself. This proves difficult because jqLite seems to have removed all notion of a CSS selector from its jQuery-based API, leading me to wonder if this solution is "the Angular way."
The use of the compile function to build out the template. This seems right to me, but I don't know if it will work.
You could indeed use ng-repeat but within your directive template rather than manually in the link (as that wouldn't be compiled, hence not repeated).
One question you didn't answer is, should this repeated template be compiled and linked by Angular, or is it going to be static HTML only?
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
config: '=?'
},
templateUrl: 'myTemplate',
link: function(scope) {
scope.array = new Array(config.times);
}
}
}
With myTemplate being:
<header>...</header>
<div ng-repeat="item in array" ng-bind-html="config.template"></div>
<footer>...</footer>
I'd think to use ng-transclude in this case, because the header & footer wrapper will be provided by the directive the inner content should change on basis of condition.
Markup
<my-directive>
<div ng-repeat="item in ['1','2','3']" ng-bind-html="config.template| trustedhtml"><div>
</my-directive>
Directive
var app = angular.module('app');
app.directive('myDirective', function($sce) {
return {
restrict: 'E',
transclude: true,
template: '<div>the header</div>'+
'<ng-transclude></ng-transclude>'+
'<div>the footer</div>',
scope: {
config: '=?'
}
.....
}
}
Filter
app.filter('trustedhtml', function($sce){
return function(val){
return $sce.trustedHtml(val);
}
})

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

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

angularJS directive with isolated scope, attribute binding doesn't work

Please see this jsfiddle: http://jsfiddle.net/viro/DK5pC/3/
What I did looks right compared to the tutorials and replies I've found, so I'm sure I'm overlooking something trivial.
I'm trying to do a directive on a html element, that will create a sibling div to display a message associated with the original element.
for example, for this html :
<input ng-model="inp" tst-msg="message" />
I would create as a sibling element:
<div class="msg">Msg:<span ng-bind="tstMsg"></span></div>
I was hoping that tstMsg in the div's scope would be bound to message in the input's scope.
Here's what the directive looks like :
angular.module('tst', [])
.directive('tstMsg', function(){
var template = "<div class='msg' >Msg:<span ng-bind='tstMsg'></span></div>";
var link = function(scope,element,attrs) {
element.parent().append(template);
console.log("link called");
};
return {
restrict: 'A',
scope: {
tstMsg: '='
},
link: link
};
});
Well that doesn't work and I can't figure out why.
You need to $compile the template you're adding to the DOM. Angular hasn't had a chance to add it's handlers, for instance the ng-bind directive to that part of the dom.
So instead of just adding the element like this:
element.parent().append(template);
These steps will let Angular process your template and then add it.
newe = angular.element(template);
$compile(newe)(scope);
element.parent().append(newe);
Updated fiddle

Resources