how to show only 25 element at one time? - angularjs

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;

Related

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.

AngularJS : Why the data is not displayed in view may I use $scope.apply?

I am getting data from service and display on view using ng-repeat .Actually I am getting event when user scroll to bottom mean when user reached to bottom I will do something.When It reached to bottom I am changing the contend of my array .I am getting the correct contend in ng-repeat array (display array) but it is not reflect on view why ? May I use $scope.apply() or $scope.digest()
Here is my code
http://plnkr.co/edit/XgOxJnPXZk4DneJonlKV?p=preview
Here I am changing the contend of my display array which is not reflect on view
if (container[0].offsetHeight + container[0].scrollTop >= container[0].scrollHeight) {
if(scope.var<scope.arrays.length)
scope.display=[];
var nextvar =++counter;
var increment=counter+1
console.log("nextvar:"+nextvar+"increment:"+increment)
scope.display=scope.arrays[nextvar].concat(scope.arrays[increment]);
console.log(scope.display)
}
As #Claies mentioned you should use apply(). Though the digest() would probably have worked as well.apply() calls digest() internally. He also mentioned that your variable that seems to be storing the page number gets reset to 0 each time you scroll. You should store that in a scope variable outside that handler.
I tried to fix with minimum change
http://plnkr.co/edit/izV3Dd7raviCt4j7C8wu?p=preview
.directive("scrollable", function() {
return function(scope, element, attrs) {
var container = angular.element(element);
container.bind("scroll", function(evt) {
console.log('scroll called'+container[0].scrollTop);
var counter = scope.page;
if (container[0].scrollTop <= 0) {
if (scope.var > 0)
scope.display = scope.arrays[--scope.var].concat(scope.arrays[scope.var+1]);
}
if (container[0].offsetHeight + container[0].scrollTop >= container[0].scrollHeight) {
if (scope.var < scope.arrays.length)
scope.display = [];
var nextvar = ++counter;
var increment = counter + 1
console.log("nextvar:" + nextvar + "increment:" + increment)
scope.display = scope.arrays[nextvar].concat(scope.arrays[increment]);
console.log(scope.display)
scope.page = counter;
}
scope.$apply();
});
};
})
generally I would have implemented this differently. For example by having a spinning wheel on the bottom of the list that when displayed you get the rest of data.
It is difficult to give you a full working plunker. Probably you should have multiple JSON files in the plunker, each containing the data for one page so that we can add the data to the bottom of the display list.
After you modify display array you just have to call scope.$apply() so that it runs the $digest cycle and updates the view. Also you need the initialize scope.var either in your controller or the directive and modify it conditionally.
I dont if this is what you want. I have modified the plunker take a look.
http://plnkr.co/edit/J89VDMQGIXvFnK86JUxx?p=preview

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

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.

How to do service request with pagination inside ng-repeat in AngularJS

I have a list of items displayed with ng-repeat at my web app, to get the items I call a service (rest) with pagination enabled, at the moment I am able to display just the first page. Now I need to implement some mechanics to make futher calls to load page 2, 3 ... as soon as the user scroll to the end of the current page.
For example, if pages are 20 items long and I am at the first page and I scroll down till item number 20, app will call page 2 and the items array will be expanded with the new items.
To top it all, it would be also very nice if there is a "pre-load" area which is not yet visible but elements are already there, ready to be shown to provide a smooth navigation, similar to "PageAdaptar" in Android.
What the best approach to do this in angular-js?
Thank you !
That is what finally I've done
I am using jquery to detect when I am at 300 px or less close to the bottom, in that case I call onScrollBottomEvent() from my controller, notice that this function may be called n times during scroll if user is inside of the 300px margin and he keeps going down.
<script type="text/javascript">
var lastScrollTop = 0;
$(window).scroll(function(event) {
scrolled = $(window).scrollTop() + $(window).height();
scrollMax = $('#container').height() + 50;
if(scrolled >= (scrollMax-300)) {
angular.element('#container').scope().onScrollBottomEvent();
}
});
</script>
then onScrollBottomEvent() calls loadNextPage() using apply() [important!]
Inside loadNextPage I set the flag onLoadMoreRequested to true, because I just need to call just once in order to load the next page instead of calling on every pixel scroll
$scope.onScrollBottomEvent = function(){
$log.log("onScrollBottomEvent()");
$scope.$apply(loadNextPage);
}
function loadNextPage(){
$log.log("loadNextPage()");
// execute only if there is other request on fly
if (!$scope.onLoadMoreRequested){
$scope.onLoadMoreRequested = true;
$scope.appendNewItems($scope.search.data, $scope.lastLoadedPage + 1);
}
}
finally I call the method to invoke the service
$scope.appendNewItems = function(search, page){
$log.log("Loading items request for page " + page);
$scope.footerProgressVisible = true;
if ($scope.lastSearch != $scope.search.data){
page = 0; // if search has changed, set page to zero
}
srvItems.getItems(search, function(result, page){
$scope.footerProgressVisible = false;
if (result!=null){
$scope.items.push.apply($scope.items, result);
$scope.lastLoadedPage = page;
$scope.onLoadMoreRequested = false;
$scope.lastSearch = $scope.search.data;
}
},
$scope.currentCategoryId,
page);
};
The key line is
$scope.items.push.apply($scope.items, result);
which will expand my ng-repeat automatically

Resources