Use of "for...of" in ng-repeat - javascript

Looking through ng-repeats source, it doesn't look like theres any instance of it using for-of. Is there any custom directive that does this or some other way of achieving this loop in templates to make use of iterator functions?
Class with iterator
class Cache{
constructor(items){
this.cache = {
"one" : 1,
"two" : 2
};
};
// custom iterator that turns our cache into an array
// for use in "for...of" loops
[Symbol.iterator](){
var index = 0;
// turn cache object into array of its values (underscore method)
var data = _.values(this.cache);
return {
next: function(){
if(index < data.length){
return {
value: data[index++],
done: false
};
}else{
return { done:true };
}
}
};
};
};
var myCache = new Cache();
// looping my cache in simple js would look like
for(let val of myCache){
console.log(val);
}
// 1, 2
proposed angularjs ng-repeat directive
<ul>
<li ng-repeat="item in myCache track by $index"></li>
</ul>
However that does not work as ng-repeat does not implement for...of. My question is: is there a way to get the ng-repeat directive to work nicely with iterators with minimal interface changes, or better yet, a custom directive identical to ng-repeat that is made for for...of loops?

You could just use Array.from to convert your iterable source to an array, which ngRepeat will be able to iterate:
<ul>
<li ng-repeat="item in Array.from(myCache) track by $index"></li>
</ul>
Ideally this would happen in your javascript directive/controller:
scope.myCache = Array.from(new Cache());
View:
<ul>
<li ng-repeat="item in myCache track by $index"></li>
</ul>

Related

ng-repeat track by $index display the item only once if the similar key exists multiple times in angularjs

I was getting [ngRepeat:dupes]: error in my angularjs code.
So I searched on stackoverflow and found the solution which stated to use track by $index. This is working for me now. But, it displays the item multiple times if the same key is present multiple times. I want to display only one item if the same key exists multiple times.
here is my sample code from the view :
<div ng-repeat="user in users | filter:myFilter track by $index">
</div>
here is the current output :
Insted of showing these two cards, I want the code to show only one card as they both are the same.
How do I do it?
pass array into a function and create an array without the repeating values
like myFunction(users) so the repeat section will become ng-repeat="user in myFunction(users)". Write the function yourself.
I fixed it like this by creating a filter:
var app = angular.module('angularTable', []);
app.filter('unique', function() {
// we will return a function which will take in a collection
// and a keyname
return function(collection, keyname) {
// we define our output and keys array;
var output = [],
keys = [];
// we utilize angular's foreach function
// this takes in our original collection and an iterator function
angular.forEach(collection, function(item) {
// we check to see whether our object exists
var key = item[keyname];
// if it's not already part of our keys array
if(keys.indexOf(key) === -1) {
// add it to our keys array
keys.push(key);
// push this item to our final output array
output.push(item);
}
});
// return our array which should be devoid of
// any duplicates
return output;
};
});
app.controller('listdata', function($scope, $http) {
your controller logic for the users array
});
});
In the view :
<div ng-repeat="user in users |unique:'name' | filter:myFilter track by $index">
</div>

Iterate over object properties

In my aurelia app, I need to display a list of elements. For performance reasons, these elements are stored as a javascript object instead of an array. That is what the object looks like :
var x = {
0 : {...},
3 : {...},
5 : {...},
}
Here is the part of the template where I display these elements :
<template>
<ul>
<li repeat.for="property of object | ObjectToArray">
${ property.text }
</li>
</ul>
</template>
As you can see, I'm currently using a value converter to be able to iterate over my object properties. The value converter simply converts the object to an array :
export class ObjectToArrayValueConverter {
toView(data : {[key : string] : any}) : any[] {
var array = [];
for (var key in data) {
array.push(data[key]);
}
return array;
}
}
This solution works very well as long as properties do not get removed or added to the object after the list has been rendered for the first time. The reason is that the value converter only gets called once.
In my case, however, I need my list to stay up to date whatever happens.
I know I could create a function that could be manually called every time the object is modified, but that would add some unwanted complexity in business logic.
Is there an aurelia functionality that coud help me achieve what I want ? I could not find any help in the docs. Thank you !
You can get the keys using Object.prototype.keys and call Array.prototype.map on it to get an array every time you want to list it out.
var obj={...}; //Object with many keys
var genArray = Object.keys(obj).map(function(key){return obj[key];})
//Using as a function
function getKeysAsArray(obj){
if(!obj){
return [];
}
return Object.keys(obj).map(function(key){return obj[key]});
}
There's an easier strategy, as shown in the docs:
export class KeysValueConverter {
toView(obj) {
return Reflect.ownKeys(obj);
}
}
Usage:
<p repeat.for="greeting of friends | keys">${greeting}, ${friends[greeting].name}!</p>

ng-repeat index descending order in angularjs [duplicate]

How can i get a reversed array in angular?
i'm trying to use orderBy filter, but it needs a predicate(e.g. 'name') to sort:
<tr ng-repeat="friend in friends | orderBy:'name':true">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
<tr>
Is there a way to reverse original array, without sorting.
like that:
<tr ng-repeat="friend in friends | orderBy:'':true">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
<tr>
I would suggest using a custom filter such as this:
app.filter('reverse', function() {
return function(items) {
return items.slice().reverse();
};
});
Which can then be used like:
<div ng-repeat="friend in friends | reverse">{{friend.name}}</div>
See it working here: Plunker Demonstration
This filter can be customized to fit your needs as seen fit. I have provided other examples in the demonstration. Some options include checking that the variable is an array before performing the reverse, or making it more lenient to allow the reversal of more things such as strings.
This is what i used:
<alert ng-repeat="alert in alerts.slice().reverse()" type="alert.type" close="alerts.splice(index, 1)">{{$index + 1}}: {{alert.msg}}</alert>
Update:
My answer was OK for old version of Angular.
Now, you should be using
ng-repeat="friend in friends | orderBy:'-'"
or
ng-repeat="friend in friends | orderBy:'+':true"
from https://stackoverflow.com/a/26635708/1782470
Sorry for bringing this up after a year, but there is an new, easier solution, which works for Angular v1.3.0-rc.5 and later.
It is mentioned in the docs:
"If no property is provided, (e.g. '+') then the array element itself is used to compare where sorting". So, the solution will be:
ng-repeat="friend in friends | orderBy:'-'" or
ng-repeat="friend in friends | orderBy:'+':true"
This solution seems to be better because it does not modify an array and does not require additional computational resources (at least in our code). I've read all existing answers and still prefer this one to them.
Simple solution:- (no need to make any methods)
ng-repeat = "friend in friends | orderBy: reverse:true"
You can reverse by the $index parameter
<tr ng-repeat="friend in friends | orderBy:'$index':true">
You can just call a method on your scope to reverse it for you, like this:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
<script>
angular.module('myApp', []).controller('Ctrl', function($scope) {
$scope.items = [1, 2, 3, 4];
$scope.reverse = function(array) {
var copy = [].concat(array);
return copy.reverse();
}
});
</script>
</head>
<body ng-controller="Ctrl">
<ul>
<li ng-repeat="item in items">{{item}}</li>
</ul>
<ul>
<li ng-repeat="item in reverse(items)">{{item}}</li>
</ul>
</body>
</html>
Note that the $scope.reverse creates a copy of the array since Array.prototype.reverse modifies the original array.
if you are using 1.3.x, you can use the following
{{ orderBy_expression | orderBy : expression : reverse}}
Example List books by published date in descending order
<div ng-repeat="book in books|orderBy:'publishedDate':true">
source:https://docs.angularjs.org/api/ng/filter/orderBy
If you are using angularjs version 1.4.4 and above,an easy way to sort is using the "$index".
<ul>
<li ng-repeat="friend in friends|orderBy:$index:true">{{friend.name}}</li>
</ul>
view demo
When using MVC in .NET with Angular you can always use OrderByDecending() when doing your db query like this:
var reversedList = dbContext.GetAll().OrderByDecending(x => x.Id).ToList();
Then on the Angular side, it will already be reversed in some browsers (IE). When supporting Chrome and FF, you would then need to add orderBy:
<tr ng-repeat="item in items | orderBy:'-Id'">
In this example, you'd be sorting in descending order on the .Id property. If you're using paging, this gets more complicated because only the first page would be sorted. You'd need to handle this via a .js filter file for your controller, or in some other way.
You can also use .reverse(). It's a native array function
<div ng-repeat="friend in friends.reverse()">{{friend.name}}</div>
That's because you are using JSON Object. When you face such problems then change your JSON Object to JSON Array Object.
For Example,
{"India":"IN","America":"US","United Kingdon":"UK"} json object
[{"country":"India","countryId":"IN"},{"country":"America","countryId":"US"},{"country":"United Kingdon","countryId":"UK"}]
The orderBy filter performs a stable sorting as of Angular 1.4.5. (See the GitHub pull request https://github.com/angular/angular.js/pull/12408.)
So it is sufficient to use a constant predicate and reverse set to true:
<div ng-repeat="friend in friends | orderBy:0:true">{{friend.name}}</div>
I found something like this, but instead of array i use objects.
Here is my solution for objects:
Add custom filter:
app.filter('orderObjectBy', function() {
return function(items, field, reverse){
var strRef = function (object, reference) {
function arr_deref(o, ref, i) {
return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]);
}
function dot_deref(o, ref) {
return !ref ? o : ref.split('[').reduce(arr_deref, o);
}
return reference.split('.').reduce(dot_deref, object);
};
var filtered = [];
angular.forEach(items, function(item) {
filtered.push(item);
});
filtered.sort(function (a, b) {
return (strRef(a, field) > strRef(a, field) ? 1 : -1);
});
if(reverse) filtered.reverse();
return filtered;
};
});
Which can then be used like
<div ng-repeat="(key, value) in items | orderObjectBy:'field.any.deep':true">
If you need old browser support, you will need to define the reduce function (this is only available in ECMA-262 mozilla.org)
// Production steps of ECMA-262, Edition 5, 15.4.4.21
// Reference: http://es5.github.io/#x15.4.4.21
if (!Array.prototype.reduce) {
Array.prototype.reduce = function(callback /*, initialValue*/) {
'use strict';
if (this == null) {
throw new TypeError('Array.prototype.reduce called on null or undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
var t = Object(this), len = t.length >>> 0, k = 0, value;
if (arguments.length == 2) {
value = arguments[1];
} else {
while (k < len && !(k in t)) {
k++;
}
if (k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
value = t[k++];
}
for (; k < len; k++) {
if (k in t) {
value = callback(value, t[k], k, t);
}
}
return value;
};
}
I had gotten frustrated with this problem myself and so I modified the filter that was created by #Trevor Senior as I was running into an issue with my console saying that it could not use the reverse method. I also, wanted to keep the integrity of the object because this is what Angular is originally using in a ng-repeat directive. In this case I used the input of stupid (key) because the console will get upset saying there are duplicates and in my case I needed to track by $index.
Filter:
angular.module('main').filter('reverse', function() {
return function(stupid, items) {
var itemss = items.files;
itemss = itemss.reverse();
return items.files = itemss;
};
});
HTML:
<div ng-repeat="items in items track by $index | reverse: items">
Im adding one answer that no one mentioned. I would try to make the server do it if you have one. Clientside filtering can be dangerous if the server returns a lot of records. Because you might be forced to add paging. If you have paging from the server then the client filter on order, would be in the current page. Which would confuse the end user. So if you have a server, then send the orderby with the call and let the server return it.
Useful tip:
You can reverse you're array with vanilla Js: yourarray .reverse()
Caution: reverse is destructive, so it will change youre array, not only the variable.
I would sugest using array native reverse method is always better choice over creating filter or using $index.
<div ng-repeat="friend in friends.reverse()">{{friend.name}}</div>
Plnkr_demo.

Building AngularJS variable name during ng-repeat

I have a set of items that come in as follows. (I have no control over the variable's name)
pubDate0, pubDate1, pubDate2
They are accessed like so
<div>
<i>
{{newsData.pubDate0[0]}}
</i>
</div>
<div>
<i>
{{newsData.pubDate1[0]}}
</i>
</div>
<div>
<i>
{{newsData.pubDate2[0]}}
</i>
</div>
Is there anyway to concatenate this variable name using ng-repeat so that I do not have to write all of this repetitive code?
I have tried the following inside a ng-repeat, and many more similar combinations.
<p ng-repeat="t in getTimes(10) track by $index"> //force looped 10 times
{{(newsData.pubDate+$index+[0])}}
</p>
//Tried the following as well
{{(newsData.pubDate+$index+[0])}}
{{('newsData.pubDate'+$index+[0])}}
{{('newsData.pubDate'+$index+'[0]')}}
{{newsData.pubDate+$index+[0]}}
{{newsData.pubDate+($index)+[0]}}
{{newsData.pubDate+{{$index}}+[0]}}
{{newsData.pubDate($index)[0]}}
{{newsData.pubDate$index[0]}}
{{newsData.pubDate{{$index}}[0]}}
{{newsData.pubDate+$index+[0]}}
Running out of guesses. :(
You have two ways to access a js property:
var obj = {prop1: 'p1', prop2: 'p2'};
console.log(obj.prop1); //p1
var propName = 'prop';
var index = 1;
console.log(obj[propName + index]); //p1
Simply, use the second way:
newsData['pubDate'+$index][0];
JSFIDDLE.
You can achieve simply using new Array(10) if you know the length see the below code;
//Controller:
$scope.getTimes = function(x) {
return new Array(x);
}
<p ng-repeat="t in getTimes(10) track by $index"> //force looped 10 times
{{(newsData['pubDate'+$index][0])}}
</p>
But I would convert the data the way I wanted just before resolving the promise (or assigning the data to the scope) and if you use the data all over your application I definitely suggest that.
Assuming the below code is your service call;
var getAllNews = function() {
var deferred = $q.defer();
$http.get('/api/news')
.success(function (resultNews) {
var pubDates = []; //will be new pubDate array
for (var key in resultNews) {
//check if it is a pubDate
if (key.indexOf("pubDate") === 0) {
pubDates.push(resultNews[key]); // add it to array
delete resultNews[key]; // remove from the object
}
}
//finally assign new array as a property of result
resultNews.pubDates = pubDates;
deferred.resolve(resultNews);
});
return deferred.promise;
};
But the second approach might have some problem related to order of items as browsers new promise to return them in order (even they are).

With an angularjs ng-repeat, can I force one element to be first in the list?

I have an array, keywords, printing with an ng-repeat:
<li ng-repeat="keyword in keywords"> {{ keyword }} </li>
Which when it's sorted alphabetically would display, e.g:
Apples
Cucumbers
Daikon
Turnip
I want that when a user searches a specific keyword, that keyword gets "pinned" to the top of the list, no matter how else the list is sorted. So if the user searches "Turnip", Turnip is first in the list, while the rest remains sorted alphabetically:
Turnip
Apples
Cucumbers
Daikon
I am wondering if this functionality is possible with ng-repeat, or if I will need to construct it by inserting an additional element at the top and then filtering just that one from the array.
I'm adding another answer, as I think both could be used, but this one with sorting is much slicker!
Here, I just do a sort of the array of objs on the pinned first then on the name value as you wanted it:
<li ng-repeat="obj in array | orderBy:['pinned','name']:reverseSort ">{{ obj.name }} [<label>Pin</label><input type="checkbox" ng-model="obj.pinned" ng-click="pinObj(obj)" />]</li>
http://plnkr.co/edit/8NGW3b?p=info
Cheers
You can create a custom angular filter that handles the sorting. Then you could just use
<li ng-repeat="keyword in keywords|my_sort"> {{ keyword }} </li>
http://docs.angularjs.org/guide/filter
good luck!
I could imagine that you could have instead of just a key in your array, you could have an array of objects for example:
array {
[ key: "Turnip",
pinned: true],
[ key: "Apples",
pinned: false] }
And then, in your ng-repeat, then you could have a filter that splits out the pinned versus unpinned as required.
app.filter('pinned', function() {
return function (list, pinned, scope) {
var test = (pinned == 'true' ? true : false);
var returnArray = [];
for (var i = 0; i < list.length; i++) {
if (list[i].pinned === test) {
returnArray.push(list[i]);
}
}
return returnArray;
};
});
I've created this plunk to show what I mean above. A potentially slicker solution would be to sort your array by the pinned attribute.
http://plnkr.co/edit/onFG7K61gLLqX31CgnPi?p=preview

Categories