ng-repeat a single element over nested objects - angularjs

Say I have an object with keys corresponding to products and values corresponding to objects which in turn have keys corresponding to price points at which those products have sold, and values corresponding to amount sold.
For example, if I sold 10 widgets at $1 and 5 widgets at $2, I'd have the data structure:
{ 'widget': {'1': 10, '2': 5} }
I'd like to loop over this structure and generate rows in a table such as this one:
thing price amount
---------------------
widget $1 10
widget $2 5
In Python it's possible to nest list comprehensions to traverse lists data structures like this. Would such a thing be possible using ng-repeat?

How about this?
http://plnkr.co/edit/ZFgu8Q?p=preview
Controller:
$scope.data = {
'widget1': {
'1': 10,
'2': 5
},
'widget2': {
'4': 7,
'6': 6
}
};
View:
<div ng-controller="MyCtrl">
<table>
<thead>
<tr>
<td>thing</td>
<td>price</td>
<td>amount</td>
</tr>
</thead>
<tbody ng-repeat="(productName, productData) in data">
<tr ng-repeat="(price, count) in productData">
<td>{{productName}}</td>
<td>{{price|currency}}</td>
<td>{{count}}</td>
</tr>
</tbody>
</table>
</div>
Output:
thing price amount
----------------------
widget1 $1.00 10
widget1 $2.00 5
widget2 $4.00 7
widget2 $6.00 6
This would output a tbody per product (thanks to Sebastien C for the great idea).
If needed, you can differentiate between the first, middle and last tbody (using ng-repeat's $first, $middle and $last) and style them with ng-class (or even native CSS selectors such as :last-child -- I would recommend ng-class though)

ng-repeat does not currently have a possible way to complex iterate inside objects (the way it's possible in python). Check out the ng-repeat source code and note that the regex expression matched is:
(key, value) in collection - and that they push into the key array and assign to the value list, and so you cannot possibly have a complex ng-repeat sadly...
There are basically 2 types of solutions which were already answered here:
Nested ng-repeat like the first answer suggested.
Rebuilding your data object to fit 1 ng-repeat like the second answer suggested.
I think solution 2 is better as I like to keep my sorting & coding logic inside the controller, and not deal with it in the HTML document. This will also allow for more complex sorting (i.e based on price, amount, widgetName or some other logic).
Another thing - the second solution will iterate over possible methods of a dataset (as hasOwnProperty wasn't used there).
I've improved the solution in this Plunker (based on the finishingmove Plunker) in order to use angular.forEach and to show that the solution is rather simple but allows for complex sorting logic.
$scope.buildData = function() {
var returnArr = [];
angular.forEach($scope.data, function(productData, widget) {
angular.forEach(productData, function( amount, price) {
returnArr.push( {widgetName: widget, price:price, amount:amount});
});
});
//apply sorting logic here
return returnArr;
};
$scope.sortedData = $scope.buildData();
and then in your controller:
<div ng-controller="MyCtrl">
<table>
<thead>
<tr>
<td>thing</td>
<td>price</td>
<td>amount</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in sortedData">
<td>{{ item.widgetName }}</td>
<td>{{ item.price|currency }}</td>
<td>{{ item.amount }} </td>
</tr>
</tbody>
</table>
</div>

Just transform your object to an array... it's pretty easy in JS.
Something like:
$scope.data = { 'widget': { '1': 10, '2': 5 } };
var tableData = [];
for (item in $scope.data) {
var thing = item;
for (subitem in $scope.data[thing]) {
tableData.push({
thing: thing,
price: subitem,
amount: $scope.data[thing][subitem]
});
}
}
I've created a jsfiddle with this example: http://jsfiddle.net/b7TYf/

I used a simple directive which has a recursive function to loop over my nested object and create nested elements. This way you can keep your nested object structure.
Code:
angular.module('nerd').directive('nestedItems', ['$rootScope', '$compile', function($rootScope, $compile) {
return {
restrict: 'E',
scope: false,
link: function(scope, element, attrs, fn) {
scope.addElement = function(elem, objAr) {
var ulElement = angular.element("<ul></ul>");
if (objAr == undefined || objAr.length == 0) {
return [];
}
objAr.forEach(function(arrayItem) {
var newElement = angular.element("<li>"+arrayItem.val+"</li>");
ulElement.append(newElement);
scope.addElement(newElement,arrayItem.sub);
});
elem.append(ulElement);
};
scope.addElement(element,scope.elements);
}
};
}]);
My Object :
$scope.elements = [
{
id: 1,
val: "First Level",
sub: [
{
id: 2,
val: "Second Level - Item 1"
},
{
id: 3,
val: "Second Level - Item 2",
sub: [{
id: 4,
val: "Third Level - Item 1"
}]
}
]
}
];
My HTML
<nested-items></nested-items>

Related

Angular smart-table st-sort not working correctly wrong order displayed

I have a problem to sort my data with smart table, specifically when including a turkish character. Wrong order is generated.
In my controller:
$scope.rowCollection = [{
a: 'Çanakkale',
b: '3'
}, {
a: 'Ceyhan',
b: '2'
}, {
a: 'ĞĞĞĞĞ',
b: '4'
}, {
a: 'Ankara',
b: '1'
}, {
a: 'Zonguldak',
b: '5'
}];
$scope.displayedCollection = [].concat($scope.rowCollection);
and my html:
<tr ng-repeat="row in displayedCollection">
<td ng-repeat="col in columns">{{row[col]}}</td>
</tr>
Here's the plunk:
http://plnkr.co/edit/JW4G1n2QszIqYjcAmlNz
How can i fix it ?
Thanks for your help
This is what I've found for you:
The smart-table version in your plunk is missing some parts (line 164) which doesn't allow you to do what you want. I've changed it to version 2.1.8 in my plunk
Use st-set-sort="yourFilterName" on your table, where your st-table attribute is:
<table st-table="displayedCollection" st-set-sort="turkishFilter" st-safe-src="rowCollection" class="table table-striped">
Write a custom filter function:
angular.module('myApp', ['smart-table'])
.filter('turkishFilter', function(){
return function(items, field, isDescending){
//If you don't create a copy of the array,
//smart-table won't be able to restore the natural order state
var result = items.slice();
//Working only for string properties ATM!
result.sort(function(first, second){
//return first.a.localeCompare(second.a, 'tr');
//OR
return first[field].localeCompare(second[field], 'tr');
//localCompare() is supported only in IE11 and upwards
});
if (isDescending){
result.reverse();
}
return result;
};
})
Working plunk HERE

Bind filter in angularjs template [duplicate]

My goal is to apply a formatting filter that is set as a property of the looped object.
Taking this array of objects:
[
{
"value": "test value with null formatter",
"formatter": null,
},
{
"value": "uppercase text",
"formatter": "uppercase",
},
{
"value": "2014-01-01",
"formatter": "date",
}
]
The template code i'm trying to write is this:
<div ng-repeat="row in list">
{{ row.value | row.formatter }}
</div>
And i'm expecting to see this result:
test value with null formatter
UPPERCASE TEXT
Jan 1, 2014
But maybe obviusly this code throws an error:
Unknown provider: row.formatterFilterProvider <- row.formatterFilter
I can't immagine how to parse the "formatter" parameter inside the {{ }}; can anyone help me?
See the plunkr http://plnkr.co/edit/YnCR123dRQRqm3owQLcs?p=preview
The | is an angular construct that finds a defined filter with that name and applies it to the value on the left. What I think you need to do is create a filter that takes a filter name as an argument, then calls the appropriate filter (fiddle) (adapted from M59's code):
HTML:
<div ng-repeat="row in list">
{{ row.value | picker:row.formatter }}
</div>
Javascript:
app.filter('picker', function($filter) {
return function(value, filterName) {
return $filter(filterName)(value);
};
});
Thanks to #karlgold's comment, here's a version that supports arguments. The first example uses the add filter directly to add numbers to an existing number and the second uses the useFilter filter to select the add filter by string and pass arguments to it (fiddle):
HTML:
<p>2 + 3 + 5 = {{ 2 | add:3:5 }}</p>
<p>7 + 9 + 11 = {{ 7 | useFilter:'add':9:11 }}</p>
Javascript:
app.filter('useFilter', function($filter) {
return function() {
var filterName = [].splice.call(arguments, 1, 1)[0];
return $filter(filterName).apply(null, arguments);
};
});
I like the concept behind these answers, but don't think they provide the most flexible possible solution.
What I really wanted to do and I'm sure some readers will feel the same, is to be able to dynamically pass a filter expression, which would then evaluate and return the appropriate result.
So a single custom filter would be able to process all of the following:
{{ammount | picker:'currency:"$":0'}}
{{date | picker:'date:"yyyy-MM-dd HH:mm:ss Z"'}}
{{name | picker:'salutation:"Hello"'}} //Apply another custom filter
I came up with the following piece of code, which utilizes the $interpolate service into my custom filter. See the jsfiddle:
Javascript
myApp.filter('picker', function($interpolate ){
return function(item,name){
var result = $interpolate('{{value | ' + arguments[1] + '}}');
return result({value:arguments[0]});
};
});
One way to make it work is to use a function for the binding and do the filtering within that function. This may not be the best approach: Live demo (click).
<div ng-repeat="row in list">
{{ foo(row.value, row.filter) }}
</div>
JavaScript:
$scope.list = [
{"value": "uppercase text", "filter": "uppercase"}
];
$scope.foo = function(value, filter) {
return $filter(filter)(value);
};
I had a slightly different need and so modified the above answer a bit (the $interpolate solution hits the same goal but is still limited):
angular.module("myApp").filter("meta", function($filter)
{
return function()
{
var filterName = [].splice.call(arguments, 1, 1)[0] || "filter";
var filter = filterName.split(":");
if (filter.length > 1)
{
filterName = filter[0];
for (var i = 1, k = filter.length; i < k; i++)
{
[].push.call(arguments, filter[i]);
}
}
return $filter(filterName).apply(null, arguments);
};
});
Usage:
<td ng-repeat="column in columns">{{ column.fakeData | meta:column.filter }}</td>
Data:
{
label:"Column head",
description:"The label used for a column",
filter:"percentage:2:true",
fakeData:-4.769796600014472
}
(percentage is a custom filter that builds off number)
Credit in this post to Jason Goemaat.
Here is how I used it.
$scope.table.columns = [{ name: "June 1 2015", filter: "date" },
{ name: "Name", filter: null },
] etc...
<td class="table-row" ng-repeat="column in table.columns">
{{ column.name | applyFilter:column.filter }}
</td>
app.filter('applyFilter', [ '$filter', function( $filter ) {
return function ( value, filterName ) {
if( !filterName ){ return value; } // In case no filter, as in NULL.
return $filter( filterName )( value );
};
}]);
I improved #Jason Goemaat's answer a bit by adding a check if the filter exists, and if not return the first argument by default:
.filter('useFilter', function ($filter, $injector) {
return function () {
var filterName = [].splice.call(arguments, 1, 1)[0];
return $injector.has(filterName + 'Filter') ? $filter(filterName).apply(null, arguments) : arguments[0];
};
});
The newer version of ng-table allows for dynamic table creation (ng-dynamic-table) based on a column configuration. Formatting a date field is as easy as adding the format to your field value in your columns array.
Given
{
"name": "Test code",
"dateInfo": {
"createDate": 1453480399313
"updateDate": 1453480399313
}
}
columns = [
{field: 'object.name', title: 'Name', sortable: 'name', filter: {name: 'text'}, show: true},
{field: "object.dateInfo.createDate | date :'MMM dd yyyy - HH:mm:ss a'", title: 'Create Date', sortable: 'object.dateInfo.createDate', show: true}
]
<table ng-table-dynamic="controller.ngTableObject with controller.columns" show-filter="true" class="table table-condensed table-bordered table-striped">
<tr ng-repeat="row in $data">
<td ng-repeat="column in $columns">{{ $eval(column.field, { object: row }) }}</td>
</tr>
</table>
I ended up doing something a bit more crude, but less involving:
HTML:
Use the ternary operator to check if there is a filter defined for the row:
ng-bind="::data {{row.filter ? '|' + row.filter : ''}}"
JS:
In the data array in Javascript add the filter:
, {
data: 10,
rowName: "Price",
months: [],
tooltip: "Price in DKK",
filter: "currency:undefined:0"
}, {
This is what I use (Angular Version 1.3.0-beta.8 accidental-haiku).
This filter allows you to use filters with or without filter options.
applyFilter will check if the filter exists in Angular, if the filter does not exist, then an error message with the filter name will be in the browser console like so...
The following filter does not exist: greenBananas
When using ng-repeat, some of the values will be undefined. applyFilter will handle these issues with a soft fail.
app.filter( 'applyFilter', ['$filter', '$injector', function($filter, $injector){
var filterError = "The following filter does not exist: ";
return function(value, filterName, options){
if(noFilterProvided(filterName)){ return value; }
if(filterDoesNotExistInAngular(filterName)){ console.error(filterError + "\"" + filterName + "\""); return value; }
return $filter(filterName)(value, applyOptions(options));
};
function noFilterProvided(filterName){
return !filterName || typeof filterName !== "string" || !filterName.trim();
}
function filterDoesNotExistInAngular(filterName){
return !$injector.has(filterName + "Filter");
}
function applyOptions(options){
if(!options){ return undefined; }
return options;
}
}]);
Then you use what ever filter you want, which may or may not have options.
// Where, item => { name: "Jello", filter: {name: "capitalize", options: null }};
<div ng-repeat="item in items">
{{ item.name | applyFilter:item.filter.name:item.filter.options }}
</div>
Or you could use with separate data structures when building a table.
// Where row => { color: "blue" };
// column => { name: "color", filter: { name: "capitalize", options: "whatever filter accepts"}};
<tr ng-repeat="row in rows">
<td ng-repeat="column in columns">
{{ row[column.name] | applyFilter:column.filter.name:column.filter.options }}
</td>
</tr>
If you find that you require to pass in more specific values you can add more arguments like this...
// In applyFilter, replace this line
return function(value, filterName, options){
// with this line
return function(value, filterName, options, newData){
// and also replace this line
return $filter(filterName)(value, applyOptions(options));
// with this line
return $filter(filterName)(value, applyOptions(options), newData);
Then in your HTML perhaps your filter also requires a key from the row object
// Where row => { color: "blue", addThisToo: "My Favorite Color" };
// column => { name: "color", filter: { name: "capitalize", options: "whatever filter accepts"}};
<tr ng-repeat="row in rows">
<td ng-repeat="column in columns">
{{ row[column.name] | applyFilter:column.filter.name:column.filter.options:row.addThisToo }}
</td>
</tr>

Custom order using orderBy in ng-repeat

I have objects like this:
students = {name: 'Aa_Student', class: 'A_Class'},
{name: 'Ab_Student', class: 'A_Class'},
{name: 'Ac_Student', class: 'B_Class'},
{name: 'Ba_Student', class: 'B_Class'},
{name: 'Bb_Student', class: 'C_Class'},
{name: 'Bc_Student', class: 'C_Class'}
Let's say the students object is shuffled. I use ng-repeat to show the data. I want to sort the objects in the custom order.
For example, I want to show the data like this:
Name Class
-----------------------------
Ac_Student B_Class
Ba_Student B_Class
Aa_Student A_Class
Ab_Student A_Class
Bb_Student C_Class
Bc_Student C_Class
So basically, I want to order by student's class, but it B_Class comes first, then A_Class, then C_Class. Also, I want to order by students name in alphabetic order. How can I do this?
HTML:
<table>
<tr ng-repeat="student in students | orderBy:customOrder">
...
</tr>
</table>
Controller:
$scope.customOrder = function(student) {
$scope.students = $filter('orderBy')(student, function() {
});
};
Hi you can create custom sort filter please see here http://jsbin.com/lizesuli/1/edit
html:
<p ng-repeat="s in students |customSorter:'class'">{{s.name}} - {{s.class}} </p>
</div>
angularjs filter:
app.filter('customSorter', function() {
function CustomOrder(item) {
switch(item) {
case 'A_Class':
return 2;
case 'B_Class':
return 1;
case 'C_Class':
return 3;
}
}
return function(items, field) {
var filtered = [];
angular.forEach(items, function(item) {
filtered.push(item);
});
filtered.sort(function (a, b) {
return (CustomOrder(a.class) > CustomOrder(b.class) ? 1 : -1);
});
return filtered;
};
});
Know this is old but may come in handy for others...
You could also create a simple custom sort function. "Not quite a filter":
$scope.customOrder = function (item) {
switch (item) {
case 'A_Class':
return 2;
case 'B_Class':
return 1;
case 'C_Class':
return 3;
}
};
And then use like you wanted to:
<table>
<tr ng-repeat="student in students | orderBy:customOrder">
...
</tr>
to set the orderBy as a property of the objects just quote that property name within the markup:
ng-repeat="student in students |orderBy:'name' | orderBy:'class'"
DEMO

Calculate in ng-repeat from the entry before

I have a filtered list (which is filtered by time - so in a specific timeframe) and over these item I am iterating with ng-repeat. These items have a name and a price. So if I am iterating over them I want to achieve that I always show the "sub"-total like this:
DATE NAME PRICE SUBTOTAL
2014-05 T-Shirt 20.00 20.00
2014-05 Jeans 45.00 65.00
2014-05 Cap 15.00 80.00
These Items are sorted by date but might have a different ID (ids dont match the index!).
I am really not able to find out how I could always calculate the subtotal (the table can be filtered by date ranges, means I could also include the items from 2014-04 and it should recalculate dynamically.
I tried it with a function like this in the controller:
var curBalanceCounter2 = 0;
$scope.currentBalanceCalc = function(finance) {
curBalanceCounter2 = curBalanceCounter2 + finance.amount;
return curBalanceCounter2;
}
But this i being executed 10 times so I get wrong numbers. Any better solution?
Thank you.
Create a custom filter
myApp.filter('subtotal', function(){
return function(items, index){
var subtotal = 0;
for (var i = 0; i <= index; i++) {
subtotal += items[i].price
}
return subtotal || items[index].price;
}
});
and call it like so
<li ng-repeat="item in items">{{item.name}} - {{item.price}} -
{{ items | subtotal : $index}}</li>
Demo
Since you have access to the original list (e.g. items in the code above) inside of an ng-repeat, you can pass it, along with the index of the current item, into a custom filter. This filter can then loop through each item up to and including the index passed in, and then return a summed subtotal. If the subtotal is 0 (as it would be for a first item), instead return the price of that item.
Docs: Custom filters in Angular
This is similar to Marc's answer. Define a subtotal function in the controller:
$scope.subtotal = function(index){
var total = 0;
angular.forEach($scope.data, function(value, key){
if(key <= index)
total += value.Price;
});
return total;
}
Then use it like this in the view:
<tr ng-repeat="d in data">
<td>{{d.Date}}</td>
<td>{{d.Name}}</td>
<td>{{d.Price}}</td>
<td>{{subtotal($index)}}</td>
</tr>
Demo
Update
If the issue is that the data isn't already sorted on the client, but is being sorted by a filter on the ng-repeat, then here's the fix:
Pass in the orderBy parameter to the subtotal function, and execute the filter on the data before computing the subtotals:
$scope.orderBy = 'Date';
$scope.subtotal = function(index, orderBy){
var total = 0;
angular.forEach($filter('orderBy')($scope.data,orderBy), function(value, key){
if(key <= index)
total += value.Price;
});
return total;
}
I've updated my demo with this code. You can change the sort order by changing 'Date' to 'Name' or 'Price' on this line
$scope.orderBy = 'Date';
and see that the subtotals automatically recalculate.
I don't know of a way to do this in pure angular, perhaps someone will chime in.
What you need looks like a cumulative sum:
function cSum(arr) {
var cumsum = [];
for(var i=0;i<arr.length;i++) {
if(i==0) cumsum[i] = arr[0];
else cumsum[i] = cumsum[i-1] + arr[i];
}
return cumsum
}
Then just add that field into the array of objects that you are repeating over and you can display it in the table.
Not too hard to do http://jsfiddle.net/VAJ5S/3/
HTML
<div ng-app="myApp">
<table ng-controller="myController">
<thead>
<tr>
<th>DATE</th>
<th>NAME</th>
<th>PRICE</th>
<th>SUBTOTAL</th>
</tr>
</thead>
<tr ng-repeat="item in items">
<td>{{item.date}}</td>
<td>{{item.name}}</td>
<td>{{item.price}}</td>
<td>{{subtotal($index)}}</td>
</tr>
</table>
</div>
JS
var app = angular.module("myApp", []);
app.controller("myController", ["$scope", function($scope){
$scope.items = [
{
date: "2014-05",
name: "T-Shirt",
price: 20.00
},
{
date: "2014-05",
name: "Jeans",
price: 65.00
},
{
date: "2014-05",
name: "Cap",
price: 80.00
}
];
$scope.subtotal = function(ind){
var subtotal = 0;
for (var i = 0; i<=ind; i++){
subtotal += $scope.items[i].price;
}
return subtotal;
};
}]);

Angularjs: two ng-repeats not showing the correct information in table column

I'm trying to create a dynamic table that could hold search results with different amount of columns.
I created a table that should have a row for every entry and a column for every datafield both populated with ng-repeat -functions, but for some reason it doesn't show any information in the columns at all, although it does create correct amount of them.
If I try to show e in {{}} it shows the correct key that exists. If I try with i in {{}} it shows the following in each column (the information is same for all columns, but different for every row):
{"etunimi":"firstname","sukunimi":"lastname","optunnus":"010101010101011001"}
Here is the html:
<table id="raporttiTulos" class="resultTable">
<tr ng-repeat="i in raportointiLista">
<td ng-repeat=" e in raportointiAvaimet">{{i.e}}</td>
</tr>
</table>
Here is the function responsible for the incoming data:
$scope.haeMaksut = function(){
$scope.raportointiAvaimet = {};
$http.post('/maksuhaku')
.then(function(res){
x = 0;
$scope.raportointiLista = res.data.message;
for(i in $scope.raportointiLista[0]){
$scope.raportointiAvaimet[x] = i;
x+=1
}
console.log($scope.raportointiAvaimet);
$scope.maksamattomat = $scope.raportointiLista.length;
$scope.lataus = true;
}, function(error){
console.log(error);
});
}
This is how the key list looks like:
Object [ "etunimi", "sukunimi", "optunnus" ]
Here are some rows from the data list:
[…]
[0…99]
0: Object { etunimi: "firstname", sukunimi: "lastname", optunnus: "101010101010101010", … }
instead of doing that you can directly access object keys.
HTML
<tr ng-repeat="i in raportointiLista">
<td ng-repeat="key in raportointiAvaimet">{{i[key]}}</td>
</tr>
Controller
$scope.haeMaksut = function () {
$scope.raportointiAvaimet = {};
$http.post('/maksuhaku')
.then(function (res) {
$scope.raportointiLista = res.data.message;
$scope.raportointiAvaimet = Object.keys($scope.raportointiLista[0]);
}, function (error) {
console.log(error);
});
}

Resources