Limit shown entries of nested ng-repeat - angularjs

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.

Related

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 to show only 25 element at one time?

I am trying make table view in ionic using angular js .but I have lot of data to show around 5000 object .So after getting data my UI hang because it is printing the data on dom.So I want to implement lazy loading so that only 100 element is visible to me or only 100 element present in dom only .When user scroll it download another 100 objects or element and removing the uppers element so that my UI not hang .can we do in angular js
I will show you how my UI hang .take a look my example here
my loader hang for around 5 seconds ..
So I tried with infinite scroll in my demo so that I am able to show only 100 element at one time
here is my code of infinite scroll But I am not able to display 100 element at one time I didnot get any error also
<ion-infinite-scroll ng-if="!noMoreItemsAvailable" on-infinite="canWeLoadMoreContent()" distance="10%">
<div class="row" ng-repeat="column in _list | orderBy: sortval:reverse | filter: query">
<div class="col col-center brd collapse-sm" ng-repeat="field in column.columns" ng-show="invoice_column_name['index'].fieldNameOrPath===field.fieldNameOrPath">{{field.value}}</div>
<div class="col col-10 text-center brd collapse-sm"></div>
</div>
</ion-infinite-scroll>
</ion-content>
Updated Plunker
I took a little different approach then the others.
There is a variable at the top of the controller called showitems. This controls how many items you want to show at a time. The counter variable will keep track of where how many items are shown. Initially, the value is set to the showitems value since we're going to prepopulate the dataset with the first items immediately in the ShowViewAfterSuccess function.
var showitems = 100;
var counter = showitems;
In your ShowViewAfterSuccess function, I added the data to a scope variable called $scope.total_invoice_records.
Next, I run the first array slice on the data. This will pass the first x number of records to the $scope.invoice_records based on the value set in the showitems variable. This initializes the view with the first set of records.
$scope.total_invoice_records=data.records;
$scope.invoice_records = $scope.total_invoice_records.slice(0, showitems);
I added a loadMore function that simply grabs the next x number of records from the total record set and concatenates them to the current set in $scope.invoice_records and increments the counter. loadMore will also check if there are still more records to load and broadcasts the message to the ionic infinite scroll directive to remove the spinner.
$scope.noMoreItemsAvailable = false;
$scope.loadMore = function() {
var next = $scope.total_invoice_records.slice(counter, counter+showitems);
$scope.invoice_records = $scope.invoice_records.concat(next);
counter = counter+showitems;
if (counter>=$scope.total_invoice_records.length) {
$scope.noMoreItemsAvailable = true;
}
$scope.$broadcast('scroll.infiniteScrollComplete');
};
Most importantly, remember to set the immediate-check attribute on the infinite scroll element to false:
<ion-infinite-scroll immediate-check="false" ng-if="!noMoreItemsAvailable" on-infinite="loadMore()" distance="10%"></ion-infinite-scroll>
You need to set up a pagination rather than use infinite scroll; or keep the functionnality of infinite scroll as is.
but if you wish to still use infinite scroll, then when you load your next datas into _list, just before filling in the new elements, clean your _list with a _list.length= 0
But you will have sideeffects that I don't know such as :
- how to load the 100 first elements ?
- the page will jump from full with 100 elements, cleaned to 0, and filled with next 100. This will lead, I assume, to an unpleasant effect
My configuration of ion-infinite is the following :
<ion-infinite-scroll
ng-if="!myModel.maxItemLoaded"
icon="ion-loading-c"
on-infinite="loadMoreData()"
distance="10"
>
Which means :
- when user scroll gets on the 10 last % of the page height, loads loadMoreData
If user never scoll, then he has only the first data shown.
Edit:
Here is an updated plunkr
http://plnkr.co/edit/B5KCbc8hr66upCXMvXSR?p=preview
Few remarks :
- the infinite scroll directive is independent and shall not surroung your table
- accessing to to index is done by $index, not with 'index'
- the load functionnality has been modified to load the next 100 elements
$scope.populateList = function() {
var size = $scope._list.length;
var maxSize = size + 100;
if (maxSize > $scope.invoice_records.length)
maxSize = $scope.invoice_records;
console.log("populateList",size,maxSize,$scope.invoice_records)
for (var i = size; i <= maxSize; i++) {
console.log("push",$scope.invoice_records[i])
$scope._list.push($scope.invoice_records[i]);
}
console.log($scope._list.length);
$scope.$broadcast('scroll.infiniteScrollComplete');
}
Angular should work with 5000 object, if it is read only , you can use one time binding
An expression that starts with :: is considered a one-time expression.
One-time expressions will stop recalculating once they are stable,
which happens after the first digest if the expression result is a
non-undefined value (see value stabilization algorithm below).
<span ng-bind="::name"></span>
I don't know infinite scrolling, but if I had to implement something like that i would have used ng-filter 'limit'.
ng-repeat="column in _list | limit : limitValue"
And then bind any button/scroll event to
$scope.limitValue = $scope.limitValue + "10";
Ideally you should not have brought this much data in 1 call. Now, if you have, you can achieve this by a small tweak.
Break your data in chunk of 100. And set that 100 in $scope.
var brokenData = [];
var totalData = x; // Total 5000 records
var pages = parseInt(totalData.length/100);
if(totalData.length % 100 !== 0) {
pages += 1;
}
for (var i = 0; i < pages; i++){
if(i == pages - 1) {
brokenData.push(totalData);
} else {
brokenData.push(totalData.splice(0,100));
}
}
Now set
$scope.pages = pages;
And for 1st page or for 1st time,
$scope.pageData = brokenData[0];
$scope.currentPage = 0 + 1;
This will show only 100 records on your page and sets current page as 1.
Now you can choose any pagination tool or bind window scroll events and just update the above 2 things
Like for 2nd page
$scope.pageData = brokenData[1];
$scope.currentPage = 1 + 1;

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.

Something like a manual refresh is needed angularjs, and a $digest() iterations error

(post edited again, new comments follow this line)
I'm changing the title of this posting since it was misleading - I was trying to fix a symptom.
I was unable to figure out why the code was breaking with a $digest() iterations error. A plunk of my code worked fine. I was totally stuck, so I decided to make my code a little more Angular-like. One anti-pattern I had implemented was to hide my model behind my controller by adding getters/setters to the controller. I tore all that out and instead put the model into the $scope since I had read that was proper Angular.
To my surprise, the $digest() iterations error went away. I do not exactly know why and I do not have the intestinal fortitude to put the old code back and figure it out. I surmise that by involving the controller in the get/put of the data I added a dependency under the hood. I do not understand it.
edit #2 ends here.
(post edited, see EDIT below)
I was working through my first Error: 10 $digest() iterations reached. Aborting! error today.
I solved it this way:
<div ng-init="lineItems = ctrl.getLineItems()">
<tr ng-repeat="r in lineItems">
<td>{{r.text}}</td>
<td>...</td>
<td>{{r.price | currency}}</td>
</tr
</div>
Now a new issue has arisen - the line items I'm producing can be modified by another control on the page. It's a text box for a promo code. The promo code adds a discount to the lineItem array. It would show up if I could ng-repeat over ctrl.getLineItems().
Since the ng-repeat is looking at a static variable, not the actual model, it doesn't see that the real line items have changed and thus the promotional discount doesn't get displayed until I refresh the browser.
Here's the HTML for the promo code:
<input type="text" name="promo" ng-model="ctrl.promoCode"/>
<button ng-click="ctrl.applyPromoCode()">apply promo code</button>
The input tag is writing the value to the model. The bg-click in the button is invoking a function that will apply the code. This could change the data behind the lineItems.
I have been advised to use $scope.apply(...). However, since this is applied as a matter of course by ng-click is isn't going to do anything. Indeed, if I add it to ctrl.applyPromoCode(), I get an error since an .apply() is already in progress.
I'm at a loss.
EDIT
The issue above is probably the result of me fixing of symptom, not a problem. Here is the original HTML that was dying with the 10 $digest() iterations error.
<table>
<tr ng-repeat="r in ctrl.getLineItems()">
<td>{{r.text}}</td>
<td>...</td>
<td>{{r.price | currency}}</td>
</tr>
</table>
The ctrl.getLineItems() function doesn't do much but invoke a model. I decided to keep the model out of the HTML as much as I could.
this.getLineItems = function() {
var total = 0;
this.lineItems = [];
this.lineItems.push({text:"Your quilt will be "+sizes[this.size].block_size+" squares", price:sizes[this.size].price});
total = sizes[this.size].price;
this.lineItems.push({text: threads[this.thread].narrative, price:threads[this.thread].price});
total = total + threads[this.thread].price;
if (this.sashing) {
this.lineItems.push({text:"Add sashing", price: this.getSashingPrice()});
total = total + sizes[this.size].sashing;
}
else {
this.lineItems.push({text:"No sashing", price:0});
}
if(isNaN(this.promo)) {
this.lineItems.push({text:"No promo code", price:0});
}
else {
this.lineItems.push({text:"Promo code", price: promos[this.promo].price});
total = total + promos[this.promo].price;
}
this.lineItems.push({text:"Shipping", price:this.shipping});
total = total + this.shipping;
this.lineItems.push({text:"Order Total", price:total});
return this.lineItems;
};
And the model code assembled an array of objects based upon the items selected. I'll abbreviate the class as it croaks as long as the array has a row.
function OrderModel() {
this.lineItems = []; // Result of the lineItems call
...
this.getLineItems = function() {
var total = 0;
this.lineItems = [];
...
this.lineItems.push({text:"Order Total", price:total});
return this.lineItems;
};
}
The problem is that with each $digest cycle, a new array is returned (even if it contains objects with equal values, new objects are created).
To circumvent this, you could associate ngRepeat with a lineItems property and call getLineItems() only when something might have changed.
A possible implementation is the following:
<!-- The VIEW -->
<table>
<tr ng-repeat="r in ctrl.lineItems">...</tr>
</table>
/* The CONTROLLER */
.controller('myCtrl', function (OrderModel) {
this.orderModel = OrderModel;
this.lineItems = this.orderModel.lineItems;
this.reloadItems = this.orderModel.getLineItems;
// Initialization
this.reloadItems();
});
/* The SERVICE */
app.service('OrderModel', function () {
this.lineItems = [];
this.getLineItems = function () {
var total = 0;
this.lineItems.splice(0, this.lineItems.length);
...
for (var i = 0; i < 10; i++) {
total++;
this.lineItems.push({text: 'Order Total', price: total});
}
};
});
See, also, this short demo.

Ng-Repeat array to rows and columns

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!

Resources