directive for array without using ng-repeat - angularjs

Once again I stumbled on slow performance of ng-repeat and can't figure how to build a directive that will render an array of elements without utilizing ng-repeat anywhere (even in the template)
So, how do you guys do that?
Even if I iterate through the array, using template for every single element:
.directive('foo', function($parse, $compile) {
return {
restrict: 'E',
scope: { items: '=myarray' },
link: function (scope, element, attrs){
var tmplt = '<div> {{name}} </div>';
scope.items.forEach(function(){
element.append(tmplt(scope));
// now I'd like to have a scope of that element,
// and pass it into the template, and use properties related
// only to that element
// but I don't know how to get a scope for a child element
});
scope.$watch(attrs.myarray, function(value) { console.log('something change'); })
}
}});
If I choose to have a single template for all the elements, then again I have no choice but to use ng-repeat in it and it will create ngRepeatWatchers and everything gets slow down again.

Though I agree with #Mark, but here is dirty solution for your question:
http://plnkr.co/edit/wxM8mSoNsRGXBJUao5Xb?p=preview
The idea is to create isolated child scopes:
scope.items.forEach(function(item){
var childScope = scope.$new(true);
angular.extend(childScope, item);
element.append($compile(tmplt)(childScope));
});
And do not forget to delete those child scopes unpon each refresh.
and sill this solution needed to be benchmarked to see is it faster and how much comparing to ngRepeat.

Related

Angular Animation Directive

I'm trying to write an angular directive that will animate a list of words similar to http://codepen.io/rachsmith/pen/BNKJme . However, I'm needing to load the text from a json file and then select a random sentence to apply the animation to. I have this part working, but am having trouble accessing the directive's child elements. I am assuming this is because the directive is being called before the elements are rendered, but using scope.$on($viewContentLoaded, function... has not made a difference.
I have jQuery and Underscore available.
Here is my code:
Controller
Data.sentences().then(function (response) {
var sentences = response.data;
$scope.sentence = _.sample(sentences);
});
View
<div class="rotator">
<p>{{sentence.static}}</p>
<text-rotator>
<span class="word" ng-repeat="item in sentence.options">{{item}}</span>
</text-rotator>
</div>
Directive
app.directive('textRotator', function () {
return {
restrict: 'E',
link: function (scope, el, attrs) {
var words = el.children('.word');
//cannot access array of items with class of word
}
};
});
Your assumption is correct, the ng-repeat-ed words are not yet in the DOM at the time the link function of the directive is executed. The sentence object is fetched asynchronously.
Listening on $viewContentLoaded won't help: this is an event fired by ngRoute module when the content of the ngView is loaded. After a digest cycle followed by DOM updates due to a change on the model, this event is not fired.
Actually, I think you're creating yourself troubles as the data could be (should be) passed as a parameter to the directive. The child word elements would be the template of the directive. I suggest something like the following:
app.directive('textRotator', function () {
return {
restrict: 'E',
scope: {
options: '='
},
templateUrl: 'words.html',
link: // ...
}
});
Template:
<text-rotator options="sentence.options"></text-rotator>
This fiddle might help you. The animation part has been replaced by a simple toggling of the opacity. Also, the words are mocked in the controller, you should make sure they are resolved by the router in the definition of the route / state, or otherwise you would have to add a watcher in the directive.

How can I be notified when DOM elements are added to my directive?

I've got a simple directive that draws a few elements, like in this example. I want to programatically set some style properties but in the link function, the elements are apparently not there yet.
Here's a fiddle.
What I think is happening is that when I call the colorSquares function, there are no squares yet in the DOM. Wrapping it in a $timeout, it works, but that just feels so wrong.
Is there any way I can be notified when the elements exist? Or is there a place that I can put the code which will access them that is guaranteed to run after they exist?
myApp.directive('myDirective', ['$timeout', function ($timeout) {
return {
restrict: 'E',
replace: false,
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
function colorSquares() {
var squaresFromDOM = document.getElementsByClassName('square');
for (var i = 0; i < squaresFromDOM.length; i++) {
squaresFromDOM[i].style['background-color'] = '#44DD44';
}
}
// this does not work, apparently because the squares are not in the DOM yet
colorSquares();
// this works (usually). It always works if I give it a delay that is long enough.
//$timeout(colorSquares);
},
template: '<div><div ng-repeat="s in squares" class="square"></div></div>'
};
}]);
You should work with Angular rather than against it which is to say you should use data bindings to do what you are trying to do rather than events/notifications in this context.
http://jsfiddle.net/efdwob3v/5/
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
scope.style = {"background-color": "red"};
},
template: '<div><div ng-repeat="s in squares" class="square" ng-style="style"></div></div>'
That said there's no difference in doing the above and just using a different class that has that red background color or even just doing style="background-color: red;"
you put the answer in your qeustion, "It always works if I give it a delay that is long enough.".
So just make the delay long enough, in this situation that can be achieved by adding an onload event because when the elements get added to the DOM it calls that event.
So instead of just colorSquares(); you could use:
window.addEventListener("load", colorSquares);
Though this may not be the ideal solution since it will also trigger when something else triggers the onload event.
Answering your question directly. To know if an element is added to a directive or to the DOM in general, you can simply put a directive on that element, since the directive will run only when the element on which it "sits" is already in the DOM.
Using part of your code as an example:
myApp.directive('myDirective', function () {
return {
...
//put custom directive that will notify when DOM is ready
template: '<div><div ng-repeat-ready ng-repeat="s in squares" class="square"></div></div>'
};
});
And here is the custom ng-repeat-ready directive:
myApp.directive('ngRepeatReady', function () {
return {
link: function (scope) {
if (scope.$last) {
//do notification stuff here
//for example $emit an event
scope.$emit('ng-repeat is ready');
}
}
}
});
This directive will run when the element on which is sits is already in the DOM and check if the element has $last property on the scope (ng-repeat sets this flag for the last element of the iterated object) which means that the ng-repeat directive is done and you can now operate on the DOM safely.

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

angular.js Getting the element from inside $evalAsync in directive

I'm finding that I'm using scope.$evalAsync inside a directive quite a lot. Mainly to do DOM stuff/jquery plugins that need all the template {{vars}} compiled.
I can get at the scope object from inside $evalAsync but not the element. In latest case in question, I'm manipulating an element that gets rendered with an ngRepeat. I'm currently getting the element by composing a jquery selector based on the scope object e.g.
scope.$evalAsync(function (scope) {
$("#item-" + scope.id).runJQplugin();
})
Although this works, to me it would be more intuitive to be able to do this
scope.$evalAsync(function (scope,element) {
element.runJQplugin();
})
Am I approaching this right or have I misunderstood something fundamental with directives?
You always have access to the element from the link and the controller of a directive through the closure scope. So in link function:
link: function(scope, elem, attrs) {
...
scope.$evalAsync(function(scope) {
elem.runJQplugin();
});
...
},
Controller (you need to specify the special $element dependency):
controller: ["$scope", "$element", function($scope, $element) {
...
scope.$evalAsync(function(scope) {
$element.runJQplugin();
});
...
}],

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>

Resources