Ng-Repeat array to rows and columns - angularjs

Thanks for taking the time to read this, I was wondering how I might be able to use ng-repeat to create a grid like box of options. I would like to take an array repeat nth number of items and then move to the next row or column until all items are listed. e.g.
assuming I had an array like [opt1,opt2,opt3,opt4,opt5,opt6,opt7] I would like to display it like this:
opt1 opt2 opt3
opt4 opt5 opt6
opt7

This is more a styling/markup problem than an AngularJS one. If you really want to, you can do:
<span ng:repeat="(index, value) in array">
{{value}}<br ng:show="(index+1)%3==0" />
</span>
http://jsfiddle.net/JG3A5/

Sorry for my HAML and Bootstrap3:
.row
.col-lg-4
%div{'ng:repeat' => "item in array.slice(0, array.length / 3)"}
{{item}}
.col-lg-4
%div{'ng:repeat' => "item in array.slice(array.length / 3, array.length * 2/3)"}
{{item}}
.col-lg-4
%div{'ng:repeat' => "item in array.slice(array.length * 2/3, array.length)"}
{{item}}
There is another version, with possibility to use filters:
<div class="row">
<div class="col-md-4" ng-repeat="remainder in [0,1,2]">
<span ng-repeat="item in array" ng-if="$index % 3 == remainder">{{item}}</span>
</div>
</div>

If all of your items are in one single array, your best bet is to make a grid in CSS. This article should be helpful: http://css-tricks.com/dont-overthink-it-grids/
You can use $index from ng-repeat to apply the correct class for your column (in this case a 4 column grid):
<div class="col-{{ $index % 4 }}"></div>
If you have a 2 dimensional array (split into rows and columns) that opens up more possibilities like actually using an HTML table.

I find it easier to simply use ng-repeat combined with ng-if and offsetting any indexes using $index. Mind the jade below:
div(ng-repeat="product in products")
div.row(ng-if="$index % 2 === 0")
div.col(ng-init="p1 = products[$index]")
span p1.Title
div.col(ng-if="products.length > $index + 1", ng-init="p2 = products[$index + 1]")
span p2.Title
div.col(ng-if="products.length <= $index + 1")

Between Performance, Dynamics and Readability
It seems putting the logic in your JavaScript is the best method. I would just bite-the-bullet and look into:
function listToMatrix(list, n) {
var grid = [], i = 0, x = list.length, col, row = -1;
for (var i = 0; i < x; i++) {
col = i % n;
if (col === 0) {
grid[++row] = [];
}
grid[row][col] = list[i];
}
return grid;
}
var matrix = listToMatrix(lists, 3);
console.log('#RedPill', matrix);
# Params: (list, n)
Where list is any array and n is an arbitrary number of columns desired per row
# Return: A matroid
# Note: This function is designed to orient a matroid based upon an arbitrary number of columns with variance in its number of rows. In other words, x = desired-columns, y = n.
You can then create an angular filter to handle this:
Filter:
angular.module('lists', []).filter('matrical', function() {
return function(list, columns) {
return listToMatrix(list, columns);
};
});
Controller:
function listOfListsController($scope) {
$scope.lists = $http.get('/lists');
}
View:
<div class="row" ng-repeat="row in (lists | matrical:3)">
<div class="col col-33" ng-repeat="list in row">{{list.name}}</div>
</div>
With this, you can see you get n number of rows -- each containing "3" columns. When you change the number of desired columns, you'll notice the number of rows changes accordingly (assuming the list-length is always the same ;)).
Here's a fiddle.
Note, that you get the ol' Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!. This is because Angular is recalling the matrical function upon every iteration. Allegedly, you can use the as results alias to prevent Angular from reevaluating the collection, but I had no luck. For this, it may be better to filter the grid inside of your controller and use that value for your repeater: $filter('matrical')(items) -- but please post back if you come across an elegant way of filtering it in the ng-repeat.
I would stress, again, you're probably heading down a dark alley by trying to write the logic in your view -- but I encourage you to try it in your view if you haven't already.
Edit
The use of this algorithm should be combined with a Matrical Data-Structure to provide methods of push, pop, splice, and additional methods -- in tandem with appropriate logic to complement Bi-Directional Data-Binding if desired. In other words, data-binding will not work out of the box (of course) as when a new item is added to your list, a reevaluation of the entire list must take place to keep the matrix's structural integrity.
Suggestion: Use the $filter('matrical')($scope.list) syntax in combination with $scope.$watch and recompile/calculate item-positions for the matrix.
Cheers!

Related

Limit shown entries of nested ng-repeat

I have an object like this:
data = {
element1: ["content11", "content12"],
element2: [],
element3: ["content31"],
element4: ["content41", "content42", "content43"]
}
Displaying everything with nested ng-repeat is straightforward:
<div ng-repeat="element in data">
<div ng-repeat="content in element">
{{content}}
</div>
</div>
Which gives me the expected output:
content11
content12
content31
content41
content42
content43
The number of elements is known, but the size of the arrays varies.
Now I'm struggling to limit the list of displayed elements to 4.
To spice things up, I want to show the first array entries first, then continue with the second, and so on. Which leads to the following code:
iteration = [0,1,2,3] //because I want to limit everything to 4 elements and don't care for more
<div ng-repeat="i in iteration">
<div ng-repeat="content in data">
{{content[i]}}
</div>
</div>
Again the expected output, but still struggling with the limit:
content11
content31
content41
content12
content42
content43
I tried to work with $parent.$index and $index, but was not successful to build a working counting function.
Unfortunately, I have to evaluate this data structure in each line of a large table and must keep an eye on performance.
Refactoring the object is for legacy reasons not possible.
I came up with this:
<div ng-repeat="i in iteration">
<div ng-repeat="(type, content) in data" ng-show="content[i] && countPreviousElements(data, i, $index) <= limit">
{{content[i]}}
</div>
</div>
and
$scope.countPreviousElements = function(data, iteration, index){
var sum = 0;
var i = 0;
for(var key in data){
if(i<=index){
sum += data[key].length > iteration+1 ? iteration+1 : data[key].length;
}else{
sum += data[key].length > iteration ? iteration : data[key].length;
}
i+=1;
}
return sum;
};
http://plnkr.co/edit/0mzTBZIDuk39BFjJ2wRi?p=preview
This seems to work as I expected and the 'limit' can be changed at runtime.
Let's discuss the performance. This function is called for each array element in data. After an analysis of the productive data, the sum of all elements within the arrays in data does not grow beyond 20 in every row, usually much smaller.
Inside the function we loop once over the length of data, which is fixed right now to 9 and not expected to grow.
If I have to do this several times in a table the costs still remain linear.
Hopefully there is no problem for this "on the fly" solution.

Displaying 12 records per row with bootstrap, the remainder should go to the next row

I am trying to display a dynamic number of users in the bootstrap grid:
<div class="row">
<div ng-repeat="Contact in Contacts" class="col-md-{{12 / Contacts.Count}}">
<img ng-src="../../Images/{{Contact.ContactImage}}.png" alt="{{Contacts.Name}}" />
<h4 class="subtitle">{{Contacts.FullName}}</h4>
</div>
</div>
The above works while there are less than 12 contacts. But if there are more, I want them to go to the next row. Is there a way to do that?
As you must be aware that bootstrap scales up to 12 columns.
So, in order to include 12 columns in a row, you need to divide your row in 12 equal sized columns. And col-md-1 does just that. Additionally, you can also use col-sm-1 col-xs-1 as per your viewport requirement.
Change your code to:
<div ng-repeat="Contact in Contacts" class="col-md-1">
You can skip this class="col-md-{{12 / Contacts.Count}}", as you are already aware of you 12-column requirement. In case your Contacts.Count is 5, then in that case, 12/5 = 2.4, and there is no such class as col-md-2.4 in bootstrap.
Bootstrap's grid system automatically wraps overflowing columns to the next row by default, so you don't technically need to change your current code at all. For instance...
<div class="row">
<div class="col-md-6">I'm in the first row</div>
<div class="col-md-6">I'm in the first row</div>
<div class="col-md-6">I'm in the second row!!!!!!</div>
</div>
If you want to actually use a separate div row for each set of 12 columns; that's easily doable as well. Just create two loops like so:
var contacts = [{...}, {...}, ...]; // assume this is an array of 20 objects
for (var i = 0, n = Math.ceil(contacts.length/12); i < n; i++) {
// print <div class="row"> here
for (var j = i*12; j < Math.min(i*12+12, contacts.length); j++) {
console.log('row', i, 'col', j);
// print <div class="col-md-1">contacts[j].RANDOM_DATA</div> here
}
// print </div> here
}
Here's a super basic/quick fiddle to demonstrate: https://jsfiddle.net/67L1h1v5/

How do I set a class based on a child element's state with Angular?

I am trying to display a results 'table' (built with DIVs as it happens) using Angular. Data looks somethingof it like this:
[['sydney','hotel','2','5','1'],
['sydney','bar','6','5','2'],
['sydney','stand','2','7','3'],
['melbourne','hotel','2','5','1'],
['melbourne','bar','8','0','1']]
What I want firstly is to suppress the repeating city name so that the first row says 'sydney' at the start but the second row and third row don't. Then the fourth says 'melbourne' and the fifth says nothing.
I've achieved this using markup like this:
<div class="row-container"
ng-repeat="row in resultsRows"
<div
ng-repeat="cell in row track by $index"
ng-bind="showValue( cell )">
</div>
</div>
The showValue() function in the controller looks like this:
$scope.currentCityName = '';
function showValue( val ) {
var outValue = '';
if (this.$index === 0) {
if (val === $scope.currentCityName) {
outValue = '';
} else {
$scope.currentCityName = val;
outValue = val;
}
} else {
outValue = val;
}
return outValue;
}
Maybe that's a bit clunky but it works and I get:
sydney hotel 2 5 1
bar 6 5 2
stand 2 7 3
melbourne hotel 2 5 1
bar 8 0 1
Now, though, I want rows that have the city name in them to have a different background colour.
What I think I want is for any 'TR' DIV (I call it that because it contains the left-floated 'TD' DIVs with the data points in them) to check if its first child DIV is not empty (because it has the city name in it) and, if so, to colour its background.
My question is: how do I do that with Angular? Or am I missing another trick..?
How do I get an item in an ng-repeat loop to interrogate a child element?
You are using ng-repeat, which has built-in values like $even and $odd:
$even boolean true if the iterator position $index is even (otherwise false).
$odd boolean true if the iterator position $index is odd (otherwise false).
Use ng-class to give different classed depending on $even and $odd.

How to set class to first item in ng-repeat that has been sorted with orderBy?

I have a list of items, which comes in unsorted, I use orderBy to sort by name alphanumerically.
<li class="ticker-li"
ng-repeat="ticker in tickers | orderBy:'ticker'"
ng-class="{'selected':ticker.selected}">
<div class="ticker"
ng-click="unselectAll(); ticker.selected = !ticker.selected;
selectTicker(ticker);
revealTickerOptions()">
{{ticker.ticker}}
</div>
Now in my controller this is how I'm currently setting the first items selected class:
var vs = $scope;
vs.tickers = data;
vs.tickers[0].selected = true;
^ This worked perfectly until I needed to add the orderBy so that items appear by alpha order:
I found this answer here, however it locks the first item to always have the class.
Modifying my code a bit, I was able to have other buttons gain that class on click, however the $first item still stayed with the class.
ng-class="{'selected':$first || ticker.selected}"
In my controller this is my unselectAll function, which doesn't work with 'selected':$first:
vs.unselectAll = function() {
for (var i = 0; i < vs.tickers.length; i++) {
vs.tickers[i].selected = false;
}
};
How should the code either in the markup or controller need to be updated to fix this issue?
Give this a shot, I'm not sure how it reads the $index on the sort by, but get rid of the $first thing and put this init statement in there.
<li class="ticker-li"
ng-repeat="ticker in tickers | orderBy:'ticker'"
ng-init="$index ? ticker.selected = false : ticker.selected = true"
ng-class="{'selected':ticker.selected}" ng-click="unselectFirst($index)">
I think this is a grey area between a hack or not, you aren't technically aliasing a property in the ng-init, but i think it is a fine line. The other solution would be sort the array in your controller, there is an example in the docs that sort on alphabetic order, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

AngularJS ng-repeat with limitTo

Is there a way to type:
<div ng-repeat="item in items | limitTo:what">
where I can substitute "what" with something that will make it iterate through the whole list of items. (note items.length is not what I am searching for.. or it must be with some ugly if inside the html).
In the source for limitTo there is support for an infinite number (Infinity):
if (Math.abs(Number(limit)) === Infinity) {
limit = Number(limit);
} else {
limit = int(limit);
}
Looks like you should be able to set to Number.POSITIVE_INFINITY.
However, the resulting code would probably be no better than using items.length. And would certainly be less understandable.
you don't need {{ }}, here is the documentation
<div ng-repeat="item in items | limitTo:what">
in the controller
$scope.what = 3; // iterate only 3
This is simple man
<div ng-repeat ="item in items | limitTo:10">
or $scope.what = '10'; and use what in limit.

Resources