ngRepeat with custom index coming up blank - angularjs

I'm using a large amount of arrays in a large form in my application. In order to make splicing out specific data from my datasets based on the user's selections, I've structured my arrays like this example:
var userList = [];
userList[user1.id] = user1;
userList[user2.id] = user2;
This lets me splice out specific elements without looping through the entire collection by using:
userList.splice(user1.id, 1);
However, when I try to make a list of Users in my HTML using an ng-repeat statement, it comes up blank. My HTML is:
<div data-ng-repeat="user in userList">{{user.name}}</div>
I suspect that ngRepeat uses 0,1,2.. by default and doesn't know how to handle my custom indexes. I've checked several sources but I can't really make sense of things. It did work when I added my users by simply pushing them into the array instead of assigning them to a specific index, so I know the rest of my code works fine.
Help? D:
EDIT:
The addition of a trackBy "track by user.id" didn't fix it.
Plunkr! http://plnkr.co/edit/8hANBvXAIplHsq0Ph6GX?p=preview

Your code isn't working because Array's indexes are zero-based meaning, they go from 0, 1, 2, ... n and you're trying to put alphanumeric indexes if you check the below code snippet the length of the array is zero.
var user1 = {
id: 'A1B2',
name: 'Pete'
};
var user2 = {
id: 'A2B3',
name: 'Jeff'
};
var userList = [];
userList[user1.id] = user1;
userList[user2.id] = user2;
console.log(userList);
console.log('length: ' + userList.length);
console.log(userList['A1B2']);
console.log(userList.A1B2); // behaving as JavaScript Object not array as property set using userList[user2.id] = user2;
So you need to set the data structure properly, you can set it as follows specifying the index of the array or by using the push function on the array to add a new item to the array.
var user1 = {
id: 'A1B2',
name: 'Pete'
};
var user2 = {
id: 'A2B3',
name: 'Jeff'
};
$scope.userList = [];
$scope.userList[0] = user1; // $scope.userList.push(user1);
$scope.userList[1] = user2; // $scope.userList.push(user2);
I suggest you change the collection name from userList to users it looks clean, you don't need to suffix a collection with the List keyword I think it looks untidy, just make the name plural.
angular
.module('demo', [])
.controller('DefaultController', DefaultController);
function DefaultController() {
var vm = this;
var pete = {
id: 'A1B2',
name: 'Pete'
};
var jeff = {
id: 'A2B3',
name: 'Jeff'
};
vm.users = [];
vm.users[0] = pete;
vm.users[1] = jeff;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="demo">
<div ng-controller="DefaultController as ctrl">
<div ng-repeat="user in ctrl.users">
<span>{{user.id}}, {{user.name}}</span>
</div>
</div>
</div>

Do following
Controller:
$scope.userList = [];
$scope.userList.push(user1);
$scope.userList.push(user2);
View:
<tr ng-repeat="user in userList track by $index">
<td>{{user.id}}</td>
<td>{{user.name}}</td></tr>

change the id of each individual user to numeric from alpha-numeric as pointed by Adbul, array's indexes are number based
for eg:-
var user1 = {
id: 0, //Any numeric Value
name: 'Pete'
}
and then try $scope.userList[user1.id] = user1;
<div data-ng-repeat="user in userList">{{user.name}}</div>

Related

Angular .extend changing object reference

I have an array of objects:
var items = [{id:1,name:'one'},{id:2,name:'two'}];
I then select one and make a copy:
var item = items[0];
var copy = angular.copy(item);
I then change a property:
item.name = 'changed';
What are the values?
console.log(item.name); // changed
console.log(items[0].name); // changed
The array element is the same object reference as the item, so the properties are the same.
Now I want to undo the change from the copy:
item = angular.extend(copy);
What are the values?
console.log(item.name); // one
console.log(items[0].name); // changed
By using .extend, I thought I was setting the properties on the item, and NOT changing the object reference, so I was expecting the array item to still be the same object reference as the item and thus to have the same name property, i.e. 'one'.
What went wrong?
If you have a look at angular.extend, it takes two args, dst and src. It will copy src object to dst, right? So, in this case, instead of doing following thing,
item = angular.extend(copy);
What you should be doing is,
item = angular.extend(items, copy)[0];
Here's code snippet:
var app = angular.module('myapp', []);
app.controller('MainCtrl', function($scope) {
var items = [{
id: 1,
name: 'one'
}, {
id: 2,
name: 'two'
}];
var item = items[0];
var copy = angular.copy(item);
item.name = 'changed';
console.log(item.name); // changed
console.log(items[0].name); // changed
console.log("===================");
item = angular.extend(items, copy)[0];
console.log(item.name); // one? (nope!)
console.log(items[0].name); // changed
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myapp" ng-controller="MainCtrl">
</div>
I think what I'm needing is:
Object.assign(item, copy);

Use ng-repeat for only some items of a collection [duplicate]

I want to use parameter in filter, when I iterate some arrays with ng-repeat
Example:
HTML-Part:
<tr ng-repeat="user in users | filter:isActive">
JavaScript-part:
$scope.isActive = function(user) {
return user.active === "1";
};
But I want to be able to use filter like
<tr ng-repeat="user in users | filter:isStatus('4')">
But its not working. How can I do something like that?
UPDATE: I guess I didn't really look at the documentation well enough but you can definitely use the filter filter with this syntax (see this fiddle) to filter by a property on the objects:
<tr ng-repeat="user in users | filter:{status:4}">
Here's my original answer in case it helps someone:
Using the filter filter you won't be able to pass in a parameter but there are at least two things you can do.
1) Set the data you want to filter by in a scope variable and reference that in your filter function like this fiddle.
JavaScript:
$scope.status = 1;
$scope.users = [{name: 'first user', status: 1},
{name: 'second user', status: 2},
{name: 'third user', status: 3}];
$scope.isStatus = function(user){
return (user.status == $scope.status);
};
Html:
<li ng-repeat="user in users | filter:isStatus">
OR
2) Create a new filter that takes in a parameter like this fiddle.
JavaScript:
var myApp = angular.module('myApp', []);
myApp.filter('isStatus', function() {
return function(input, status) {
var out = [];
for (var i = 0; i < input.length; i++){
if(input[i].status == status)
out.push(input[i]);
}
return out;
};
});
Html:
<li ng-repeat="user in users | isStatus:3">
Note this filter assumes there is a status property in the objects in the array which might make it less reusable but this is just an example. You can read this for more info on creating filters.
This question is almost identical to Passing arguments to angularjs filters, to which I already gave an answer. But I'm gonna post one more answer here just so that people see it.
Actually there is another (maybe better solution) where you can use the angular's native 'filter' filter and still pass arguments to your custom filter.
Consider the following code:
<li ng-repeat="user in users | filter:byStatusId(3)">
<span>{{user.name}}</span>
<li>
To make this work you just define your filter as the following:
$scope.byStatusId = function(statusId) {
return function(user) {
return user.status.id == statusId;
}
}
This approach is more versatile because you can do comparisons on values that are nested deep inside the object.
Checkout Reverse polarity of an angular.js filter to see how you can use this for other useful operations with filter.
If you have created an AngularJs custom filter, you can send multiple params to your filter.Here is usage in template
{{ variable | myFilter:arg1:arg2... }}
and if you use filter inside your controller here is how you can do that
angular.module('MyModule').controller('MyCtrl',function($scope, $filter){
$filter('MyFilter')(arg1, arg2, ...);
})
if you need more with examples and online demo, you can use this
AngularJs filters examples and demo
This may be slightly irrelevant, but if you're trying to apply multiple filters with custom functions, you should look into:
https://github.com/tak215/angular-filter-manager
Example I have a students list as below :
$scope.students = [
{ name: 'Hai', age: 25, gender: 'boy' },
{ name: 'Hai', age: 30, gender: 'girl' },
{ name: 'Ho', age: 25, gender: 'boy' },
{ name: 'Hoan', age: 40, gender: 'girl' },
{ name: 'Hieu', age: 25, gender: 'boy' }
];
I want to filter students via gender to be boy and filter by name of them.
The first I create a function named "filterbyboy" as following:
$scope.filterbyboy = function (genderstr) {
if ((typeof $scope.search === 'undefined')||($scope.search === ''))
return (genderstr = "")
else
return (genderstr = "boy");
};
Explaination: if not filter name then display all students else filter by input name and gender as 'boy'
Here is full HTMLcode and demo How to use and operator in AngularJs example

Angular ng-repeat, nested repeat for each parent loop

I have some trouble with getting proper data. I have many-to-many relation model, so it's 3 tables, two with data, and third is connection between them (by ID's). For example, first table is stores, second is items, and third is 'have' that connects them by id.
Now i should display available items per store. I'm using ng-repeat="store in stores" to loop through stores, and trying to create function that will return me items available in each store (store.idStore).
I have tried several approaches and none of them seems to be working for me, and since I'm new to angular, Im a little bit lost. I would appreciate any help.
Last function that I used is:
function forEachStore(id) {
angular.forEach($scope.Have, function (value, index) {
if (idStore == id) {
alert(idPlayliste);
this.push(dataFact.catchData(urlItem, idItem));
}
}, $scope.storeHasItem)
}
$scope.Have --> contains json object like({"id":1, "idStore":1, "idItem":1}, {"id":2, "idStore":1, "idItem":2}, ...)
dataFact.catchData--> my factory that gets api url and idItem and returns json object(this is working correctly).
id == store.idStore sent from ng-repeat.
So, I'm sending to this function 'store.idStore', and I want it to return me all items that is available in that store.
No alert for me :)
From your code it looks like $scope.stores is an array of Store objects. I'm assuming you have something similar for items. In your first ng-repeat loop, add something like ng-repeat="item in getItemsForStore(store.idStore)" to start the inner loop, and add the following functions to your scope (not yet tested):
$scope.getItemsForStore = function(storeId) {
var items = [];
for (var have = $scope.have, i = 0, len = have.length; i < len; i++) {
var link = have[i];
if (link.idStore === storeId && !items.contains(link.idItem) {
items.push(link.idItem);
}
}
for (int j = 0, len = items.length; j < len; j++) {
items[j] = getItem(items[j]);
}
return items;
}
function getItem(itemId) {
for (var i = 0, items = $scope.items, len = items.length; i < len; i++) {
var item = items[i];
if (item.idItem === itemId) {
return item;
}
}
throw "unknown item ID: " + itemId;
}
Things could be far more efficient if you used objects instead of arrays, with items/stores keyed by ID.
function Ctrl($scope){
$scope.stores =[
{id:1,name:'store 1'},
{id:2,name:'store 2'},
{id:3,name:'store 3'},
{id:4,name:'store 4'}
];
$scope.items =[
{id:1, name:'item 1'},
{id:2, name:'item 2'},
{id:3, name:'item 3'},
{id:4, name:'item 4'},
{id:5, name:'item 5'},
{id:6, name:'item 6'},
];
var storeItemLinked= [
{sId:1,iId:1},
{sId:1,iId:2},
{sId:1,iId:3},
{sId:2,iId:4},
{sId:2,iId:5},
{sId:2,iId:6},
{sId:3,iId:1},
{sId:3,iId:3},
{sId:3,iId:5},
{sId:4,iId:2},
{sId:4,iId:4},
{sId:4,iId:5}
];
$scope.selectedStoreItems=[];
$scope.showStoreItems=function(store){
$scope.selectedStoreItems=[];
$scope.selectedStore=store;
var itemIds=[];
for(var i=0;i<storeItemLinked.length;i++){
if(storeItemLinked[i].sId==store.id)
itemIds.push(storeItemLinked[i].iId);
}
for(var j=0;j<$scope.items.length;j++){
if(itemIds.indexOf($scope.items[j].id)>=0)
$scope.selectedStoreItems.push($scope.items[j]);
}
}
$scope.showAvailableStores = function(item){
$scope.selectedItem=item;
$scope.selectedItemStores=[];
var storeIds=[];
for(var i=0;i<storeItemLinked.length;i++){
if(storeItemLinked[i].iId==item.id)
storeIds.push(storeItemLinked[i].sId);
}
for(var j=0;j<$scope.stores.length;j++){
if(storeIds.indexOf($scope.stores[j].id)>=0)
$scope.selectedItemStores.push($scope.stores[j]);
}
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.min.js"></script>
<div ng-app ng-controller="Ctrl">
<ul>
<li ng-repeat="store in stores" ng-click="showStoreItems(store)">{{store.name}}
<ul ng-show="selectedStore==store">
<li ng-repeat="item in selectedStoreItems" ng-click="showAvailableStores(item)">
{{item.name}}
<ul ng-show="selectedItem==item">
<li ng-repeat="store in selectedItemStores">{{store.name}}</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
click the store will list all available items then click an item will list all stores that sell that item
I just mocked up the database by some js objects and you may want to change those ng-click functions to get remote data and the logic inside will depends on the view model return from server
Edit:
On a comment inspired second read :).
I thinkidStore should be value.idStore(here:
angular.forEach($scope.Have, function (value, index) {
if (idStore == id) { ... })};
)
And also:
angular.forEach(obj, iterator, [context]), where context will provide 'this' for the iterator. You're setting context to $scope.storeHasItem - a function on the $scope, I guess it's not what you're after.
Old answer:
When you're using ng-repeat="store in stores" - you get something like this: store = stores[0] in the scope + a dom template in the view (a copy of the element on which you've put the repeat)...then store = stores[1] and a copy...and so on
So in the function, the parameter you're getting is a whole store object, not just the id. Try a console.log - it should clarify things.

Updating selected array element after deleting the element from a filtered ng-repeat

I have a list of users where you click on a user to select it and put the element into an object $scope.selectedMember when selected. I'm using ng-repeat with a search box filter. The important thing is that $scope.selectedMember should always be populated with a member.
Problem is that i'm trying to overcome:
- splicing the last user out needs to automatically select the last user in the filtered array, even if it's filtered some members out with the search.
HTML
<div ng-app="myApp" ng-controller="MainCtrl">
<input ng-model="search"></input>
<div ng-repeat="(key, member) in members | filter:search | orderBy :'-name'">
<li ng-class="{active: retActive(key)}"
ng-click="selectThis($index)">
name: {{member.name}} key: {{key}}
<button ng-click="deleteThis(key)">delete</button>
</li>
</div>
Selected member name: {{selectedMember.name}}
</div>
JS
angular.module('myApp', []);
function MainCtrl($scope) {
$scope.members = [
{"name":"a", "viewIndex":0},
{"name":"b", "viewIndex":1},
{"name":"bb", "viewIndex":2},
{"name":"bb", "viewIndex":3},
{"name":"c", "viewIndex":4},
{"name":"d", "viewIndex":5},
{"name":"e", "viewIndex":6}
];
$scope.activeIndex = 0;
$scope.selectedMember = $scope.members[0];
$scope.selectThis = function (index) {
$scope.activeIndex = index;
//put array member into new object
$scope.selectedMember = $scope.members[index];
}
//splice the selected member from the array
$scope.deleteThis = function (index) {
$scope.members.splice(index, 1);
$scope.selectThis(index);
}
//angular copy and push member to the array
$scope.duplicateThis = function (index) {
}
// change class if member is active
$scope.retActive = function (index) {
return $scope.activeIndex == index;
}
}
CSS
.active {
color:blue;
}
Link to JSFiddle
One problem I see is that you are passing $index to selectThis($index). When you filter data, the loop's $index no longer represents the actual index of an item in the array - so you should pass selectThis a key, not $index.

angular grouping filter

Following angular.js conditional markup in ng-repeat, I tried to author a custom filter that does grouping. I hit problems regarding object identity and the model being watched for changes, but thought I finally nailed it, as no errors popped in the console anymore.
Turns out I was wrong, because now when I try to combine it with other filters (for pagination) like so
<div ng-repeat="r in blueprints | orderBy:sortPty | startFrom:currentPage*pageSize | limitTo:pageSize | group:3">
<div ng-repeat="b in r">
I get the dreaded "10 $digest() iterations reached. Aborting!" error message again.
Here is my group filter:
filter('group', function() {
return function(input, size) {
if (input.grouped === true) {
return input;
}
var result=[];
var temp = [];
for (var i = 0 ; i < input.length ; i++) {
temp.push(input[i]);
if (i % size === 2) {
result.push(temp);
temp = [];
}
}
if (temp.length > 0) {
result.push(temp);
}
angular.copy(result, input);
input.grouped = true;
return input;
};
}).
Note both the use of angular.copy and the .grouped marker on input, but to no avail :(
I am aware of e.g. "10 $digest() iterations reached. Aborting!" due to filter using angularjs but obviously I did not get it.
Moreover, I guess the grouping logic is a bit naive, but that's another story. Any help would be greatly appreciated, as this is driving me crazy.
It looks like the real problem here is you're altering your input, rather than creating a new variable and outputing that from your filter. This will trigger watches on anything that is watching the variable you've input.
There's really no reason to add a "grouped == true" check in there, because you should have total control over your own filters. But if that's a must for your application, then you'd want to add "grouped == true" to the result of your filter, not the input.
The way filters work is they alter the input and return something different, then the next filter deals with the previous filters result... so your "filtered" check would be mostly irrelavant item in items | filter1 | filter2 | filter3 where filter1 filters items, filter2 filters the result of filter1, and filter3 filters the result of filter 2... if that makes sense.
Here is something I just whipped up. I'm not sure (yet) if it works, but it gives you the basic idea. You'd take an array on one side, and you spit out an array of arrays on the other.
app.filter('group', function(){
return function(items, groupSize) {
var groups = [],
inner;
for(var i = 0; i < items.length; i++) {
if(i % groupSize === 0) {
inner = [];
groups.push(inner);
}
inner.push(items[i]);
}
return groups;
};
});
HTML
<ul ng-repeat="grouping in items | group:3">
<li ng-repeat="item in grouping">{{item}}</li>
</ul>
EDIT
Perhaps it's nicer to see all of those filters in your code, but it looks like it's causing issues because it constantly needs to be re-evaluated on $digest. So I propose you do something like this:
app.controller('MyCtrl', function($scope, $filter) {
$scope.blueprints = [ /* your data */ ];
$scope.currentPage = 0;
$scope.pageSize = 30;
$scope.groupSize = 3;
$scope.sortPty = 'stuff';
//load our filters
var orderBy = $filter('orderBy'),
startFrom = $filter('startFrom'),
limitTo = $filter('limitTo'),
group = $filter('group'); //from the filter above
//a method to apply the filters.
function updateBlueprintDisplay(blueprints) {
var result = orderBy(blueprints, $scope.sortPty);
result = startForm(result, $scope.currentPage * $scope.pageSize);
result = limitTo(result, $scope.pageSize);
result = group(result, 3);
$scope.blueprintDisplay = result;
}
//apply them to the initial value.
updateBlueprintDisplay();
//watch for changes.
$scope.$watch('blueprints', updateBlueprintDisplay);
});
then in your markup:
<ul ng-repeat="grouping in blueprintDisplay">
<li ng-repeat="item in grouping">{{item}}</li>
</ul>
... I'm sure there are typos in there, but that's the basic idea.
EDIT AGAIN: I know you've already accepted this answer, but there is one more way to do this I learned recently that you might like better:
<div ng-repeat="item in groupedItems = (items | group:3 | filter1 | filter2)">
<div ng-repeat="subitem in items.subitems">
{{subitem}}
</div>
</div>
This will create a new property on your $scope called $scope.groupedItems on the fly, which should effectively cache your filtered and grouped results.
Give it a whirl and let me know if it works out for you. If not, I guess the other answer might be better.
Regardless, I'm still seeing the $digest error, which is puzzling: plnkr.co/edit/tHm8uYfjn8EJk3cG31DP – blesh Jan 22 at 17:21
Here is the plunker forked with the fix to the $digest error, using underscore's memoize function: http://underscorejs.org/#memoize.
The issue was that Angular tries to process the filtered collection as a different collection during each iteration. To make sure the return of the filter always returns the same objects, use memoize.
http://en.wikipedia.org/wiki/Memoization
Another example of grouping with underscore: Angular filter works but causes "10 $digest iterations reached"
You can use groupBy filter of angular.filter module,
and do something like this:
usage: (key, value) in collection | groupBy: 'property'or 'propperty.nested'
JS:
$scope.players = [
{name: 'Gene', team: 'alpha'},
{name: 'George', team: 'beta'},
{name: 'Steve', team: 'gamma'},
{name: 'Paula', team: 'beta'},
{name: 'Scruath', team: 'gamma'}
];
HTML:
<ul ng-repeat="(key, value) in players | groupBy: 'team'" >
Group name: {{ key }}
<li ng-repeat="player in value">
player: {{ player.name }}
</li>
</ul>
<!-- result:
Group name: alpha
* player: Gene
Group name: beta
* player: George
* player: Paula
Group name: gamma
* player: Steve
* player: Scruath

Resources