how to append directive restrict='A' to existing element - angularjs

There is a need to append custom directive wich is restricted to 'A' (attribute) to some of the instances of others directives (the second one, e.g.) after it was rendered using ng-repeat.
<some-directive ng-repeat="item in vm.items"></some-directive>
that 'A'-directive is derived from uib-popover, but I suppose solution for pure uib-popover will also work. Also, it would be great to safely remove than appended popovers later.
Any suggestions how to implement it?

You can do it in this way::
Create custom directive with priority more than ng-repeat priority....
ng-repeat has 1000 priority.
angular.module('x').directive('customDir', function() {
return {
priority: 1001, // as ng-repeat has priority level 1000
restrict: 'A',
compile: function () {
return function () {...}
}
}
})
Uses::
<some-directive ng-repeat="item in vm.items" custom-dir ></some-directive>

Related

Angular js Custom Directive on element with ng-repeat - Can't set CSS classes

I'm trying to use a custom directive on an element with ng-repeat, like so:
<article ng-repeat="product in ctrl.products" class="product entity"
product="product" selected-retailer="ctrl.selectedRetailer"></article>
I've read that, in order for this to work, you need to set the priority on your custom directive to something higher than the ng-repeat directive. So I've defined my directive like so, setting the priority to 1001:
angular.module('MainApp')
.directive('product', function () {
return {
restrict: 'A',
scope: {
product: '=',
selectedRetailer: '='
},
priority: 1001,
templateUrl: '/Static/Templates/product.html',
link: function ($scope, $element, $attributes) {
$element.addClass('testCssClass');
}
};
});
...and this works, it loops through my products.
The problem is, I'm also trying to set a CSS class on the directive's element (article) using:
$element.addClass('testCssClass');
...in the link function, but it doesn't seem to work.
But if I remove ng-repeat and just show first product item, like so:
<article class="product entity" product="ctrl.products[0]"
selected-retailer="ctrl.selectedRetailer"></article>
...the CSS class shows up just fine (i.e. "product entity testCssClass").
How can I get this to work with ng-repeat?
This was indeed a very unpleasant surprise form Angular.
You have to set priority to -1001 for it to work. Reason: (from the docs of priority in the $compile service)
Priority
[...] Directives with greater numerical priority are compiled first. [...] post-link functions are run in reverse order
From a small investigation (fiddle) it seems that the $element passed to the link function is the HTML comment helper inserted by ng-repeat.

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

Custom directive in ng-repeat with isolated scope : loop items are suddenly null

I'm trying to use a custom directive in an ng-repeat loop. Without my custom directive the loop works fine: all items are displayed. But if I use my directive on the ng-repeat then all the items in the loop seem to be undefined or null, at least not printed.
Here is a simplified example:
http://jsfiddle.net/vtH64/13/
angular.module('myTest', []).directive('makecool', function(){
return {
scope: {
'flippity': '&'
},
link: function(scope, element){
element.append(", Yo!");
// do something with flippity
}
};
});
angular.module('myApp',['myTest']).controller('ListStuff', function($scope){
$scope.list = ["hi","there","this","be","a","list"];
});
It seems to have something to do with the isolated scope, because without the
scope: {
'flippity': '&'
},
which isolates the scope it works fine (http://jsfiddle.net/vtH64/15/), eventhough I will not be able to access 'flippity', which I need in the real world app.
What I am doing wrong here?
link method gets element's attributes as the third argument:
link: function(scope, element , attributes){
So you can get the flippity in a very easy way: attrs["flippity"]
Working fiddle: http://jsfiddle.net/vtH64/17/
Try with $parent if you are including the isolated scope...
<li ng-repeat="item in list" flippity="flop" makeCool>{{$parent.item}}</li>

Access repeated item inside ng-repeated directive

I'm building a custom directive in AngularJS that I need to be repeated a couple of times. Currently, my page looks like this:
<div my-item ng-repeat="item in items" />
And my directive looks like this:
module.directive('myItem', function() {
return {
restrict: 'A',
replace: true,
scope: { item: '&' },
template: '<div id="item{{$index}}"></div>',
link: function($scope, element, attributes) {
element.append('<div>' + $scope.item.name + '</div>');
}
};
});
However, inside the linking function, $scope.item.name yields undefined. I'm wondering if there is any way I could access the repeated item inside my directive.
If not, what would be my alternatives? Move the ng-repeat inside the directive, maybe?
P.S. I know that you should (generally speaking) not do DOM manipulation this way, but since I might have ~2000 items that would result in 6000 bindings, and I'm afraid that would lead to severe performance issues.
You should pass item as attribute to directive created a sample directive
http://plnkr.co/edit/l5r6zIc7ncT1XldRuB98?p=preview

How to update attribute on element via directive using angularjs

I have a pretty simple case in AngularJS where:
<select ng-repeat="el in elms" disabled="disabled" remove-disable>
<option>make a selection</option>
</select>
Initially my select is empty and so I added the disable attr to avoid having people click on it.
When the ajax call is completed and the select renders the list of options I want to remove the disable attribute.
It looks straight forward, right? but all I have seen is approaches using $watch and not for exactly this case.
I'm approaching it from a jQuery point of view where an looking at the DOM after the ajax call, finding the element and removing the attr. like this:
$('select').removeAttr('disabled');
Unfortunately I don't want to do jQuery, I want to do it with a directive, since that is what is for. the angular folks say that all DOM manipulations should be done via directives so I will like to know just how.
enrollmentModule.directive('removeDisable', function () {
return {
restrict: 'A',
scope: {
ngModel : '='
},
link: function (scope, element, attrs) {
console.log('no people yet');
if (element[0].complete) {
console.log('element finish rendering');
};
scope.$watch(attrs.ngModel, function () {
console.log('agents arrived');
});
}
};
});
AngularJS has a ngDisabled directive that you can use to make the link between the state of the list and an expression :
<select ng-repeat="el in elms" ng-disabled="elms.length == 0">
<option>make a selection</option>
</select>

Resources