Repeat a directive in Angular - javascript

I have a directive which build a star icon. I want to repeat it 'n' times. And then I want to change the class in 'active' to 'n' stars.
This is the directive which build a star and, on click, it add the class 'active'.
<div ng-app="app">
<div ng-controller="ctrl">
<div class="presentation">
<button-star repeatStars="7" limitActiveStar="5" classToAdd="active"></button-star>
</div>
.directive('buttonStar', function() {
return {
scope: {
repeatStars: "=",
limitActiveStar: "=",
classToAdd: "="
},
restrict: 'E',
template: '<button class="btn btn-icon"><span class="glyphicon glyphicon-star"></span></button>',
link: function(scope, elem) {
elem.addClass('active');
}
};
})
So, in my example, I'd like to repeat the directive 7 times and add to the first 5 elements the class 'active'

Couldn't you use a ng-repeat directive in that case ?
admitting you have 7 items in items
<button-star ng-repeat="item in items track by $index" ng-class="{'active': $index < 5}">

Add this in your controller :
$scope.number = 7;
$scope.getNumber = function(num) {
return new Array(num);
}
then
<button-star ng-repeat="i in getNumber(number) track by $index" repeat-stars="7" limit-active-star="5" class-to-add="{{$index < 5 ? 'active' : ''}}"></button-star>
should work

I made a plunker with a working solution, here:
https://plnkr.co/edit/kX9yXUVwOC7ZD0qnyMt7?p=preview
I created a parent directive called buttonStars that has several child directive elements buttonStar inside it. The first limitActiveStars will have the class active added to them, while the rest won't have it.

Related

AngularJs Directive update

I want to update my directive content only at some desired places, but not at others. I have simulated my problem here:
http://jsfiddle.net/Lvc0u55v/2945/
The problem is, I have an 'editor' directive which is applied in two places:
<span class="editor1" editor ></span>
<span class="editor2" editor ></span>
I want to update the content of span class="editor1" on button click.
How do I do it?
Why not go with a relatively Angularesque approach by isolating the scope of the directive and to a maximum extent, avoid jQuery in your logic.
So you could have your directive defined as such:
.directive('editor', function() {
return {
scope: {
upd : '=',
editordata : '=data'
},
template: '<div>{{editordata}}</div>',
controller: function($scope, $rootScope, $element) {
$rootScope.$on('update', function(evt, data) {
if(data.upd === $scope.upd){
$scope.editordata = data.txt;
}
})
},
link: function(scope, el, attr) {}
}
})
Here, you are passing the required information which the editor directive depends upon through its scope, namely upd (which I suppose is how you want to uniquely identify the items by) and the text data.
Meanwhile, you can define a list of the editor items in the common parent controller MyCtrl and iterate over them in the DOM with ng-repeat.
// MyCtrl controller
$scope.list = [
{upd: 'editor1', data: 'original data for editor1'},
{upd: 'editor1', data: 'original data for editor2'}
]
<!-- HTML -->
<div ng-repeat="item in list" upd="item.upd" data="item.data"></div>
Demo
You can check if the current directory has the "editor1" class and if so complete your logic.
You can look at this example :
element[0].querySelector('.editor1') !== undefined'

Angularjs Directive that repeats over an array

I have setup the following directive:
app.directive('starRating', function () {
return {
restrict: 'EA',
scope: {
rating: '=rating'
},
template:
'<ul class=\'list-unstyled\'>' +
'<li><span class=\'glypicon glyphicon-star\'></span></li>' +
'</ul>'
};
});
I then have the following HTML:
<star-rating rating="rating"></star-rating>
rating is an array as such: [1,3,2,4,5] and this implies that the first rating is 1 star, 2nd rating is 3 stars, ect.
The goal of the directive is to repeat the amount of .glyphicon-star icons of the rating.
You can use a for loop to concatenate the 'li' elements inside the 'ul' in the template. This is cheaper than using an ng-repeat. You probably dont need to use a list (ul's and li's) at all.
Also your scope can simply be:
scope: {
rating: '='
},

Angular, nested directive repeats, pass if parent is $last

I am trying to trigger an event once all the repeats in this nested repeat are done. So I know you can do a $last, but I have no way of telling inside the inner repeat that it is on the $last in the parent repeat. So I have this
<div>
<div ng-repeat="filter in filters.filters" repeat-directive-one filter="filter" update-filter="updateFilter">
</div>
</div>
Then inside repeat-directive-one looks like
<div ng-repeat="item in filter.values | limitTo: filter.showMore ? filter.values.length : '5' track by $index" repeat-directive-two value="::item" update-filter="updateFilter" ng-if="!$last"></div>
<div ng-repeat="item in filter.values | limitTo: filter.showMore ? filter.values.length : '5' track by $index" repeat-directive-two value="::item" update-filter="updateFilter" ng-if="$last"checkbox-fix></div>
So what that does is if it is on the last item in that repeat it adds on the checkbox-fix directive to fix an issue. But if the upper repeat has 4 items, this fires 4 times so I would like something to the degree of
ng-if="$last && parent.$last"
but that logic doesn't seem to be working as intended. I even tried to pass the parent last (boolean) as an isolate scope attr like (in repeat directive one)
<div ng-repeat="filter in filters.filters" repeat-directive-one filter="filter" update-filter="updateFilter" last-property="$last">
but logging $scope.lastProperty returns nothing. So I would like a way to pass down some sort of flag that the parent repeat is $last. Is there any way of doing this? Thanks!
You can try something like this to broadcast when the outerloop is complete and then the final loop of the inner should start watching the $last
<div ng-repeat="stuff in stuffs" outer-loop>
<div ng-repeat="item in items" inner-loop></div>
</div>
angular.module('test').directive('outerLoop', function($rootScope){
return function(scope, element, attrs) {
if (scope.$last){
$rootScope.$broadcast('outerLoopComplete');
}
};
});
angular.module('test').directive('innerLoop', function(){
return function(scope, element, attrs) {
var outerLoopComplete = false;
scope.$on('outerLoopComplete', function(){
outerLoopComplete;
});
if(outerLoopComplete && $last){
//inner loop complete
}
};
});
I ended up solving it based on #trevor 's advice like so :
<div ng-repeat="item in filter.values | limitTo: filter.showMore ? filter.values.length : '5' track by $index" repeat-directive-two value="::item" update-filter="updateFilter" checkbox-fix></div>
Stuck the checkbox-fix on the repeat (no need for 2 toggled via ng-if), and then inside the checkbox-fix directive I did like so :
return {
restrict: 'A',
link: function(elem, attr, tr) {
if(elem.$parent.$parent.$last && elem.$last){
//fixed!
}
}
};
So elem.$last worked great, I had to check the data/structure a bit to realize I had to do elem.$parent.$parent.$last instead of just 1 level of parent.
https://docs.angularjs.org/api/ng/directive/ngInit
You can use ngInit to set the value of $last to a scope variable, you should be able to access this variable from the child scope directly and indirectly in the case of isolate scope.

How to handle click event in my class

I am trying to add a class to a clicked element and remove it when user clicks other elements.
I have something like
<li ng-repeat='test in tests' >
<a href='' ng-click='pickTest($index, $event)'>{{test.title}}</a>
</li>
js
$scope.pickTest = function(index, event) {
$(event.target).addClass('blur');
//this works when user clicks one of the <a> tags
//but I want to remove the class if user clicks another <a> tag
};
How do I do this?
Thanks!
You can use ng-class to determine if the class needs to be appended based on a certain condition. Additionally, using $index in an ng-repeat is really not advisable because it pose problems when filters are applied in the ng-repeat directive. You can create two functions, isActive() for the ng-class directive and setActive() to set the active item.
angular.module('app', [])
.controller('Ctrl', function($scope) {
var activeTest = {};
$scope.tests = [{
title: 'Test 1'
}, {
title: 'Test 2'
}, {
title: 'Test 3'
}, {
title: 'Test 4'
}];
$scope.setActive = function(test) {
activeTest = test;
};
$scope.isActive = function(test) {
return angular.equals(activeTest, test);
};
});
.blur {
color: red;
}
<div ng-app="app" ng-controller="Ctrl">
<ul>
<li ng-repeat="test in tests">
{{test.title}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Use ng-class, like this:
<li ng-repeat='test in tests' >
<a ng-class={blur:blurActive} href='' ng-click='pickTest($index, $event);blurActive=true'>{{test.title}}</a>
</li>
Notice that you don't need to set blurActive to true inside your function, because ng-repeat creates a new scope for each "repeated item", so you can set it active inside the same ng-click, after you call your function, in this way the logic of your function won't be mixed with the design.
Doing DOM manipulation in a controller is considered bad practice, you could achieve this angular way using ng-class:-
<li ng-repeat="test in tests">
<a href="#" ng-click="pickTest($index, $event)"
ng-class="{'blur': opt.selectedIdx == $index}">{{test.title}}</a>
</li>
and in your controller, just do:-
$scope.opt = {}; //Set an initial value in your controller
$scope.pickTest = function(index, $event) {
$event.preventDefault(); //If you need
$scope.opt.selectedIdx = index ; //Set the current index here, You could do this inline as well in the html, but probably better off having logic in your controller
}
Plnkr
Just select all other elements with the class blur and remove the class from them before you assign the class to the currently clicked element.
$scope.pickTest = function(index, event) {
$('.blur').removeClass('blur');
$(event.target).addClass('blur');
};

ng-repeat finish event

I want to call some jQuery function targeting div with table. That table is populated with ng-repeat.
When I call it on
$(document).ready()
I have no result.
Also
$scope.$on('$viewContentLoaded', myFunc);
doesn't help.
Is there any way to execute function right after ng-repeat population completes? I've read an advice about using custom directive, but I have no clue how to use it with ng-repeat and my div...
Indeed, you should use directives, and there is no event tied to the end of a ng-Repeat loop (as each element is constructed individually, and has it's own event). But a) using directives might be all you need and b) there are a few ng-Repeat specific properties you can use to make your "on ngRepeat finished" event.
Specifically, if all you want is to style/add events to the whole of the table, you can do so using in a directive that encompasses all the ngRepeat elements. On the other hand, if you want to address each element specifically, you can use a directive within the ngRepeat, and it will act on each element, after it is created.
Then, there are the $index, $first, $middle and $last properties you can use to trigger events. So for this HTML:
<div ng-controller="Ctrl" my-main-directive>
<div ng-repeat="thing in things" my-repeat-directive>
thing {{thing}}
</div>
</div>
You can use directives like so:
angular.module('myApp', [])
.directive('myRepeatDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$last){
window.alert("im the last!");
}
};
})
.directive('myMainDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('border','5px solid red');
};
});
See it in action in this Plunker.
If you simply want to execute some code at the end of the loop, here's a slightly simpler variation that doesn't require extra event handling:
<div ng-controller="Ctrl">
<div class="thing" ng-repeat="thing in things" my-post-repeat-directive>
thing {{thing}}
</div>
</div>
function Ctrl($scope) {
$scope.things = [
'A', 'B', 'C'
];
}
angular.module('myApp', [])
.directive('myPostRepeatDirective', function() {
return function(scope, element, attrs) {
if (scope.$last){
// iteration is complete, do whatever post-processing
// is necessary
element.parent().css('border', '1px solid black');
}
};
});
See a live demo.
There is no need of creating a directive especially just to have a ng-repeat complete event.
ng-init does the magic for you.
<div ng-repeat="thing in things" ng-init="$last && finished()">
the $last makes sure, that finished only gets fired, when the last element has been rendered to the DOM.
Do not forget to create $scope.finished event.
Happy Coding!!
EDIT: 23 Oct 2016
In case you also want to call the finished function when there is no item in the array then you may use the following workaround
<div style="display:none" ng-init="things.length < 1 && finished()"></div>
//or
<div ng-if="things.length > 0" ng-init="finished()"></div>
Just add the above line on the top of the ng-repeat element. It will check if the array is not having any value and call the function accordingly.
E.g.
<div ng-if="things.length > 0" ng-init="finished()"></div>
<div ng-repeat="thing in things" ng-init="$last && finished()">
Here is a repeat-done directive that calls a specified function when true. I have found that the called function must use $timeout with interval=0 before doing DOM manipulation, such as initializing tooltips on the rendered elements. jsFiddle: http://jsfiddle.net/tQw6w/
In $scope.layoutDone, try commenting out the $timeout line and uncommenting the "NOT CORRECT!" line to see the difference in the tooltips.
<ul>
<li ng-repeat="feed in feedList" repeat-done="layoutDone()" ng-cloak>
{{feed | strip_http}}
</li>
</ul>
JS:
angular.module('Repeat_Demo', [])
.directive('repeatDone', function() {
return function(scope, element, attrs) {
if (scope.$last) { // all are rendered
scope.$eval(attrs.repeatDone);
}
}
})
.filter('strip_http', function() {
return function(str) {
var http = "http://";
return (str.indexOf(http) == 0) ? str.substr(http.length) : str;
}
})
.filter('hostName', function() {
return function(str) {
var urlParser = document.createElement('a');
urlParser.href = str;
return urlParser.hostname;
}
})
.controller('AppCtrl', function($scope, $timeout) {
$scope.feedList = [
'http://feeds.feedburner.com/TEDTalks_video',
'http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/',
'http://sfbay.craigslist.org/eng/index.rss',
'http://www.slate.com/blogs/trending.fulltext.all.10.rss',
'http://feeds.current.com/homepage/en_US.rss',
'http://feeds.current.com/items/popular.rss',
'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml'
];
$scope.layoutDone = function() {
//$('a[data-toggle="tooltip"]').tooltip(); // NOT CORRECT!
$timeout(function() { $('a[data-toggle="tooltip"]').tooltip(); }, 0); // wait...
}
})
Here's a simple approach using ng-init that doesn't even require a custom directive. It's worked well for me in certain scenarios e.g. needing to auto-scroll a div of ng-repeated items to a particular item on page load, so the scrolling function needs to wait until the ng-repeat has finished rendering to the DOM before it can fire.
<div ng-controller="MyCtrl">
<div ng-repeat="thing in things">
thing: {{ thing }}
</div>
<div ng-init="fireEvent()"></div>
</div>
myModule.controller('MyCtrl', function($scope, $timeout){
$scope.things = ['A', 'B', 'C'];
$scope.fireEvent = function(){
// This will only run after the ng-repeat has rendered its things to the DOM
$timeout(function(){
$scope.$broadcast('thingsRendered');
}, 0);
};
});
Note that this is only useful for functions you need to call one time after the ng-repeat renders initially. If you need to call a function whenever the ng-repeat contents are updated then you'll have to use one of the other answers on this thread with a custom directive.
Complementing Pavel's answer, something more readable and easily understandable would be:
<ul>
<li ng-repeat="item in items"
ng-init="$last ? doSomething() : angular.noop()">{{item}}</li>
</ul>
Why else do you think angular.noop is there in the first place...?
Advantages:
You don't have to write a directive for this...
Maybe a bit simpler approach with ngInit and Lodash's debounce method without the need of custom directive:
Controller:
$scope.items = [1, 2, 3, 4];
$scope.refresh = _.debounce(function() {
// Debounce has timeout and prevents multiple calls, so this will be called
// once the iteration finishes
console.log('we are done');
}, 0);
Template:
<ul>
<li ng-repeat="item in items" ng-init="refresh()">{{item}}</li>
</ul>
Update
There is even simpler pure AngularJS solution using ternary operator:
Template:
<ul>
<li ng-repeat="item in items" ng-init="$last ? doSomething() : null">{{item}}</li>
</ul>
Be aware that ngInit uses pre-link compilation phase - i.e. the expression is invoked before child directives are processed. This means that still an asynchronous processing might be required.
It may also be necessary when you check the scope.$last variable to wrap your trigger with a setTimeout(someFn, 0). A setTimeout 0 is an accepted technique in javascript and it was imperative for my directive to run correctly.
I did it this way.
Create the directive
function finRepeat() {
return function(scope, element, attrs) {
if (scope.$last){
// Here is where already executes the jquery
$(document).ready(function(){
$('.materialboxed').materialbox();
$('.tooltipped').tooltip({delay: 50});
});
}
}
}
angular
.module("app")
.directive("finRepeat", finRepeat);
After you add it on the label where this ng-repeat
<ul>
<li ng-repeat="(key, value) in data" fin-repeat> {{ value }} </li>
</ul>
And ready with that will be run at the end of the ng-repeat.
<div ng-repeat="i in items">
<label>{{i.Name}}</label>
<div ng-if="$last" ng-init="ngRepeatFinished()"></div>
</div>
My solution was to add a div to call a function if the item was the last in a repeat.
This is an improvement of the ideas expressed in other answers in order to show how to gain access to the ngRepeat properties ($index, $first, $middle, $last, $even, $odd) when using declarative syntax and isolate scope (Google recommended best practice) with an element-directive. Note the primary difference: scope.$parent.$last.
angular.module('myApp', [])
.directive('myRepeatDirective', function() {
return {
restrict: 'E',
scope: {
someAttr: '='
},
link: function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$parent.$last){
window.alert("im the last!");
}
}
};
});
i would like to add another answer, since the preceding answers takes it that the code needed to run after the ngRepeat is done is an angular code, which in that case all answers above give a great and simple solution, some more generic than others, and in case its important the digest life cycle stage you can take a look at Ben Nadel's blog about it, with the exception of using $parse instead of $eval.
but in my experience, as the OP states, its usually running some JQuery plugins or methods on the finnaly compiled DOM, which in that case i found that the most simple solution is to create a directive with a setTimeout, since the setTimeout function gets pushed to the end of the queue of the browser, its always right after everything is done in angular, usually ngReapet which continues after its parents postLinking function
angular.module('myApp', [])
.directive('pluginNameOrWhatever', function() {
return function(scope, element, attrs) {
setTimeout(function doWork(){
//jquery code and plugins
}, 0);
};
});
for whoever wondering that in that case why not to use $timeout, its that it causes another digest cycle that is completely unnecessary
I had to render formulas using MathJax after ng-repeat ends, none of the above answers solved my problem, so I made like below. It's not a nice solution, but worked for me...
<div ng-repeat="formula in controller.formulas">
<div>{{formula.string}}</div>
{{$last ? controller.render_formulas() : ""}}
</div>
I found an answer here well practiced, but it was still necessary to add a delay
Create the following directive:
angular.module('MyApp').directive('emitLastRepeaterElement', function() {
return function(scope) {
if (scope.$last){
scope.$emit('LastRepeaterElement');
}
}; });
Add it to your repeater as an attribute, like this:
<div ng-repeat="item in items" emit-last-repeater-element></div>
According to Radu,:
$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {mycode});
For me it works, but I still need to add a setTimeout
$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {
setTimeout(function() {
mycode
}, 100); });
If you simply wants to change the class name so it will rendered differently, below code would do the trick.
<div>
<div ng-show="loginsuccess" ng-repeat="i in itemList">
<div id="{{i.status}}" class="{{i.status}}">
<div class="listitems">{{i.item}}</div>
<div class="listitems">{{i.qty}}</div>
<div class="listitems">{{i.date}}</div>
<div class="listbutton">
<button ng-click="UpdateStatus(i.$id)" class="btn"><span>Done</span></button>
<button ng-click="changeClass()" class="btn"><span>Remove</span></button>
</div>
<hr>
</div>
This code worked for me when I had a similar requirement to render the shopped item in my shopping list in Strick trough font.

Categories