Angular ng-repeat, nested repeat for each parent loop - angularjs

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.

Related

ngRepeat with custom index coming up blank

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>

dynamically bind input value to select list by its JSON data 'id'

I'm working with some fairly complicated data (simplified somewhat below). Here I have a list of selector lists, each with an id specified in its JSON data. I then have a list of texts which need to bind a specified field (I've got that bit sorted) in a specified (via the text's listid) selector list (haven't got that bit sorted).
This is what my data in the js controller file looks like:
angular.module('myApp',[])
.controller('myCtrl',function($scope,$timeout){
$scope.selectorLists=[
{
'id': 3,
'name': 'SelectionList ABC',
'rows': [
{'id':5, 'name':'ABCrow 5', 'Afield1': 'ABCrow5field1', 'Afield2': 'ABCrow5field2'},
{'id':9, 'name':'ABCrow 9', 'Afield1': 'ABCrow9field1', 'Afield2': 'ABCrow9field2'}
]
},
{
'id': 5,
'name': 'SelectionList XYZ',
'rows': [
{'id':1, 'name':'XYZrow 1', 'Xfield1': 'XYZrow1field1', 'Xfield2': 'XYZrow1field2'},
{'id':2, 'name':'XYZrow 2', 'Xfield1': 'XYZrow2field1', 'Xfield2': 'XYZrow2field2'}
]
}
];
$scope.texts=[
{'id': 453,
'name': 'Input Text1',
'listid': 3,
'listrowid': 9,
'listfieldname': 'Afield1'
},
{'id': 454,
'name': 'Input Text2',
'listid': 5,
'listrowid': 2,
'listfieldname': 'Xfield2'
}
];
});
And this is what the html looks like:
<div ng-app="myApp" ng-controller="myCtrl">
<div ng-repeat='selectionList in selectorLists'>
<label for="ddl{{selectionList.id}}">{{selectionList.name}}:</label>
<select id="ddl{{selectionList.id}}" ng-model="selectionList.selectedRow"
ng-options="row as row.name for row in selectionList.rows" ></select>
</div>
<div ng-repeat='text in texts'>
<label for='txt{{text.id}}'>{{text.name}}</label>
<input type="text" id='txt{{text.id}}' value='{{selectorLists[id=text.listid].selectedRow[text.listfieldname]}}' />
</div>
</div>
I can use a $filter in my controller and get the right list there, but I need to get it to bind its selected value to my text input (by passing in text.listid).
var listid = 5;
var result = $filter('filter')($scope.selectorLists, {id:listid})[0];
I also need to be able to initialise a selected value in the selector lists, either by cycling through my texts and coming across the first listid that references the list, and has a listrowid set (i.e. to any value > 0; 0 defines none is selected for that text), or by doing that server-side and just setting a selectedRowId (or, better yet, set the selectedRow) in each selector list. I'm just not sure how to use either way to set the selected item in the html select objects.
As I'm completely new to angularjs, just a nudge in the right direction would be much appreciated.
Oh, yes, and here's the fiddle.
OK, so I've found a fairly basic javascript-based solution, but would be really happy to see/use a more angularjs one if anyone has any pointers.
So, added these to my controller (yes, a bit of repeated code so I'd definitely refactor this):
$scope.getSelectedValue = function(selectorListId, fieldname) {
for (var i = 0, len = $scope.selectorLists.length; i < len; i++) {
if ($scope.selectorLists[i].id === selectorListId) {
return $scope.selectorLists[i].selectedRow[fieldname];
}
}
};
$scope.getSelectedRow = function(selectorList) {
for (var i = 0, len = selectorList.rows.length; i < len; i++) {
if (selectorList.rows[i].id === selectorList.selectedRowId) {
return selectorList.rows[i];
}
}
};
//will add selectedRowid server-side so now I just iterate over the selectorLists and
//then call the function that iterates over the rows looking for the right id
for (var i = 0, len = $scope.selectorLists.length; i < len; i++) {
$scope.selectorLists[i].selectedRow = $scope.getSelectedRow($scope.selectorLists[i]);
}
And from my input I now call the first function with value='{{getSelectedValue(text.listid, text.listfieldname)}}'
The full jsfiddle is here.
Update
Refactored for a bit of code re-use and I've now got in my js controller file:
$scope.getById = function(items, id) {
for (var i = 0, len = items.length; i < len; i++) {
if (items[i].id === id) {
return items[i];
}
}
}
for (var i = 0, len = $scope.selectorLists.length; i < len; i++) {
$scope.selectorLists[i].selectedRow = $scope.getById($scope.selectorLists[i].rows, $scope.selectorLists[i].selectedRowId);
}
And the input value is now set with value='{{getById(selectorLists, text.listid).selectedRow[text.listfieldname]}}', and here's the latest fiddle.

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.

How can I display one attribute of a set of data just once?

I'm doing some testing with Angular to see if I can replicate what I already have in PHP more efficiently.
I have a set of data stored in JSON:
[
{
"name":"Blue Widget",
"description":"blue-widget",
"snippet":"The best blue widget around!",
"category":"Home Widgets",
"popular":true
},
{
"name":"Red Widget",
"description":"red-widget",
"snippet":"The best red widget around!",
"category":"Outdoor Widgets",
"popular":true
},
{
"name":"Green Widget",
"description":"green-widget",
"snippet":"The best green widget around!",
"category":"Work Widgets",
"popular":true
},
{
"name":"Yellow Widget",
"description":"yellow-widget",
"snippet":"The best yellow widget around!",
"category":"Home Widgets",
"popular":true
}
]
I'm grabbing this in my controller and adding it to my view in a fairly standard way (yes, I know not to use $http directly in a controller in production):
widgetApp.controller('widgetListCtrl', function($scope,$http){
$http.get('widgets/widgets.json').success(function(data){
$scope.widgets = data
})
})
If I use:
<li ng-repeat="widget in widgets">{{widget.category}}</li>
Then naturally it will just go through and list:
Home Widgets
Outdoor Widgets
Work Widgets
Home Widgets
What I'd like to do is generate a list of each widget.category but with each category only appearing once, so a user could then click on a category and be shown all the widgets in that category. How can I go about this? Sorry, I haven't got anything to go on because I pretty much have no idea where to start.
You can use the existing 'unique' filter from AngularUI.
<li ng-repeat="widget in widgets | unique: 'widget.category' ">{{widget.category}}</li>
Be sure to include a reference to the filters module in your app as well (e.g. angular.module('yourModule', ['ui', 'ui.filters']);).
You'd have to build a list of unique categories:
widgetApp.controller('widgetListCtrl', function($scope,$http){
$http.get('widgets/widgets.json').success(function(data){
$scope.uniqueCategories = [];
for (var i = 0; i < $scope.widgets.length; i++) {
if ($scope.uniqueCategories.indexOf($scope.widgets[i].category) === -1)
$scope.uniqueCategories.push($scope.widgets[i].category);
}
});
});
Make a dropdown with the model set to the category:
<select ng-model="categoryFilter" ng-options="category as category for category in uniqueCategories"></select>
And use a filter on your repeat:
<li ng-repeat="widget in widgets | filter: { category: categoryFilter }">{{widget.category}}</li>
Create a filter
app.filter('unique', function() {
return function (arr, field) {
return _.uniq(arr, function(a) { return a[field]; });
};
});
In Markup
<li ng-repeat="widget in widgets | unique:'category'">{{widget.category}}</li>
Create a distinct filiter and use it on your view:
angular.filter("distinct", function () {
return function (data, propertyName) {
if (angular.isArray(data) && angular.isString(propertyName)) {
var results = [];
var keys = {};
for (var i = 0; i < data.length; i++) {
var val = data[i][propertyName];
if (angular.isUndefined(keys[val]) && val != null) {
keys[val] = true;
results.push(val);
};
};
return results;
}
else {
return data;
}
}
})
<li ng-repeat="widget in widgets | distinct:'category'">{{widget.category}}</li>

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