Angular Material - infinite scroll not working - angularjs

I'm using Angular Material's infinite scroll, which is defined here: https://material.angularjs.org/latest/demo/virtualRepeat
My first question is: does this virtualRepeat demand to be inside a div with a scrollbar, or can it be applied to the whole page? I actually don't want to have my content inside another div with additional scrollbar (besides the browser's one).
So, I'm using $http and my service returns 30 items if I provide a value of 0, and 60 if I provide value of 1, etc.
var _page = 0;
$scope.infiniteItems = {
numLoaded_: 0,
toLoad_: 0,
// Required.
getItemAtIndex: function(index) {
if (index > this.numLoaded_) {
this.fetchMoreItems_(index);
return null;
}
return index;
},
// Required.
// For infinite scroll behavior, we always return a slightly higher
// number than the previously loaded items.
getLength: function() {
return this.numLoaded_ + 5;
},
fetchMoreItems_: function(index) {
// For demo purposes, we simulate loading more items with a timed
// promise. In real code, this function would likely contain an
// $http request.
if (this.toLoad_ < index) {
this.toLoad_ += 30;
postFactory.getPastPosts(_page).then(angular.bind(this, function(data) {
this.numLoaded_ = this.toLoad_;
_page++;
}));
}
}
};
Here's my HTML
<md-content flex layout-padding>
<div class="virtualRepeatdemoInfiniteScroll">
<md-list>
<md-virtual-repeat-container id="vertical-container">
<div md-virtual-repeat="post in infiniteItems" md-on-demand="" class="repeated-item" flex="">
<md-divider></md-divider>
{{post}}
<past-post model="post" action="reaction(currentPost, candidate, type)"></past-post>
</div>
</md-virtual-repeat-container>
</span>
</md-list>
</div>
</md-content>
The problem is that nothing happens. I get no values. The problem is probably in the postFactory.getPastPosts(_page).then(angular.bind(this, function(data) { as well, since the data is actually in data.data, but there is nothing in the documentation that would show where and how to set the data.
UPDATE
The code for getPastPosts is pretty straightforward: a basic $http request:
function getPastPosts(page) {
return $http.get(baseUrl + '/api/content/past-posts/' + page)
}
I'm using this in various part of the application so there's no doubt that it's working.

You could got an object in getPastPosts() if data returns a value.
The infinite scroll would not be work because it is not an array.
infiniteItems must be type of array.
try like this,
if(data.size == 1){ // I don't know your data structure.
this.push(data.data);
}else{
this = data.data
}

Related

AngularJS ng repeat performance

I have a ng-repeat on my html view. The array it's repeating is populated each time a user selects a checkbox. It works like a filter way.
However, I am having issues with performance. It seems to create the DOM elements again and thus doing this there is a 1-1.5 second 'freeze' delay before the results are populated on the user interface again.
The array it's repeating isnt that big - probably around 50-60 entries. Each entry in the array has two objects which do have a lot of properties. Will this affect the performance? From what I've read it seems to be because it's creating the DOM elements again.
I have tried using track by $index, which speeds it up drastically but this causes problems on the div boxes I'm displaying. Text is on the wrong results, gets mixed up etc. I have also tried using track by ($index + item). No luck - same problem. I've also tried using track by item.id - but this has the same effect of not using track by - slow.
Is there anything I can do to optimize this? Or do I just bite the bullet?
Here is my code below:
<div ng-if="$ctrl.hasDataProcessed() && $ctrl.resultsAvailable()">
<div class="acca-builder-content">
<div class="acca-builder-header" style="border: 1px solid #1393ED;">{{"RESULTS" | translate}} ({{$ctrl.accaBuilderResultsCount}})</div>
<ul class="tips-list-group-matches">
<li ng-repeat="result in $ctrl.accaBuilderResults | orderBy: $ctrl.getSort" ng-class="{'match-has-link': $ctrl.canViewMatch(result.match)}" class="tip-list-group-match">
<tf-competition-header ng-if="result.match.CompMasterID" competition="result.match"></tf-competition-header>
<match-header match="result.match" tracking-screen="Tips"></match-header>
</li>
</ul>
</div>
</div>
</div>
And within the controller:
var buildAccaResultsFromFilter = function () {
var results = [];
var tips = ctrl.tips;
for (var i = 0; i < tips.length; i++) {
var tip = tips[i];
if(valueInFilter("COMPETITIONS", tip.match.CompID) &&
valueInFilter("DATES", tip.match.MatchDateConverted) &&
valueInFilter("SHOW", tip.tip.TipType)) {
results.push(tip);
}
}
if(results.length > 0) {
ctrl.accaBuilderResults = results;
ctrl.accaBuilderResultsCount = results.length;
ctrl.resultsFound = true;
} else {
clearAccaBuilderResults();
ctrl.resultsFound = false;
}
};
// Function called when a checkbox is clicked
ctrl.onCheckboxChange = function (option, item) {
item.checkState = !item.checkState;
if(item.checkState) {
addToFilter(option.optionKey, item.textKey);
}
else {
removeFromFilter(option.optionKey, item.textKey);
}
if(option.onChange) {
option.onChange(item.checkState, item.checkId);
}
if(ctrl.canBuildAccaResults()) {
buildAccaResultsFromFilter();
} else {
clearAccaBuilderResults();
}
};
// Checks if a value is present within the filter by it's key
var valueInFilter = function (filterKey, value) {
return ctrl.filter[filterKey].includes(value);
};
ctrl.resultsAvailable = function () {
return ctrl.accaBuilderResults && ctrl.accaBuilderResults.length > 0;
};
ctrl.hasDataProcessed = function () {
return ctrl.tips && ctrl.competitions;
};
Without seeing some code it is tough to optimize.
You can try and eliminate any ng-show and ng-hide and use ng-if only. It will remove a lot of watchers if you have those present.
Additionally you can use the syntax below in your directives if you don’t need two-way binding. This will also remove a watcher. The more watchers you can remove in ng-repeat, the better.
{{:: expression }}

App Fails First Load When Listing Array's Item from a DataSnapshot from Firebase

I am storing a DataSnapshot into a $scope array, so the ng-repeat into HTML div updates my "result list"
The problem is, when I run the code, input a value and click to run the DataSnapshot function, the first result doesn't appear on the app screen, only on the app's log on browser's console. If I run the function again, the result appears. If I change the input and click the button (run the function) again, this input appears in the first try.
So, here's what you will probably need:
Print from first attempt(data appears on console but not on app):
Print from second attempt(data appears twice on console and once on app):
Print from third attempt with another input(data appears once on console and once on app):
Codes:
index.html
<div class="list card">
<div class="item item-body" ng-repeat="locker in lockers">
<a href="#" class="item item-icon-right text-center">
<img ng-src="{{imageSrc}}" style="width: 35px; height: auto;">
<p style="text-align: center">
Locker - {{locker.number}}
<br>
{{key}}
</p>
<i class="icon ion-chevron-right"></i>
</a>
</div>
app.js
angular.module('starter', ['ionic', 'firebase'])
.controller('LockerCtrl', ["$scope", "$firebaseArray", function($scope,$firebaseArray, snapshot){
var lockersRef = new Firebase('https://ifrjarmariosdb.firebaseio.com/armarios');
$scope.getButtonClicked = function() {
var lockernumber = document.getElementById('lockerNumberInput').value;
if(lockernumber.length > 0){
lockerInputedNumber = parseInt(lockernumber);
lockersRef.on('value', function(snapshot){
snapshot.forEach(function(data){
var number = data.val().number;
if(number == lockerInputedNumber){
$scope.key = data.key();
$scope.lockers = [data.val()];
console.log(data.val());
if(number == 101){ -
$scope.imageSrc = "img/locker_test2.png";
}
else{
$scope.imageSrc = "img/locker_test.jpg";
}
}
});
});
}
As you could see by the prints, I'm also facing some trouble to change the image source according to the number value from Firebase. If you could help me to solve that, it would be a great help.
Please, I'm not asking for the solution for this method, if you know a different method to do this, I ask you to post it too.
Thanks!
This code starts loading data from Firebase:
lockersRef.on('value', function(snapshot){
snapshot.forEach(function(data){
var number = data.val().number;
if(number == lockerInputedNumber){
$scope.key = data.key();
$scope.lockers = [data.val()];
console.log(data.val());
if(number == 101){ -
$scope.imageSrc = "img/locker_test2.png";
}
else{
$scope.imageSrc = "img/locker_test.jpg";
}
}
});
The loading happens asynchronously. Since it takes time before the data is available, the browser continues executing the JavaScript after this code.
When the data comes back from the server, it executes your callback function. But at that point, AngularJS is no longer expecting any changes to the $scope.
The solution is to make AngularJS aware of the fact that you've changed the scope. The easiest way to do that, is to wrap the callback into a $timeout() call:
lockersRef.on('value', function(snapshot){
$timeout(function() {
snapshot.forEach(function(data){
var number = data.val().number;
if(number == lockerInputedNumber){
$scope.key = data.key();
$scope.lockers = [data.val()];
console.log(data.val());
if(number == 101){ -
$scope.imageSrc = "img/locker_test2.png";
}
else{
$scope.imageSrc = "img/locker_test.jpg";
}
}
});
});
Some people that ran into the same problem:
Angular JS firebase.(child_added) not rendering on page
ng-show and ng-change unwanted delay
A few other things I note about your code:
you're downloading all lockers and then filtering for the one the user entered client-side. This is wasting bandwidth that your user might be paying for. A better way would be to leave the filtering to Firebase with a Query:
var query = lockersRef.orderByChild('number').equalTo(lockerInputedNumber);
query.on('value', function(snapshot){
there is a binding library for AngularJS+Firebase called AngularFire, which handles the $timeout() thing automatically. It's built on top of Firebase's JavaScript SDK that you're now using, so they interoperate perfectly.

How can I get ng-click to function with ng-repeat and ng-bind-html?

I am trying to get a ng-click directive to function within an ng-repeat and ng-bind-html. The ng-click code is added to a string of html from data pulled from the server (hence the ng-bind-html). The setup has a controller, a base template that is put onto the page with Drupal, and a partial that is loaded via the template from Drupal.
The controller looks like this at the moment:
var guideListController = angular.module('app')
.controller('guideListController', [
'$scope',
'$sce',
'$compile',
'ViewService',
'ToolKit',
function($scope, $sce, $compile, ViewService, ToolKit) {
// Storage for which rows need more/less links
this.rowIndex = [];
this.showFull = false;
this.showFullClick = function() {
alert('Showfull');
};
this.trustIntro = function(code) {
return $sce.trustAsHtml(code);
};
// Fetch the guide list view from services
var data = ViewService.get({view_endpoint:'guide-service', view_path: 'guide-list'}, function(data) {
//console.log(data);
// Update/process results
for (var row in data.results) {
// Create short intro w/ truncate.js
data.results[row].Intro_short = $sce.trustAsHtml($scope.guideList.getShortIntro(data.results[row], row));
//data.results[row].Intro_short = $scope.guideList.getShortIntro(data.results[row], row);
// Update intro
data.results[row].Introduction = $sce.trustAsHtml($scope.guideList.updateIntro(data.results[row], row));
//data.results[row].Introduction = $scope.guideList.updateIntro(data.results[row], row);
}
$scope.guideList.guides = data.results;
});
// Add a read less anchor tag at the end of the main intro
this.updateIntro = function(row, row_index) {
var intro = row['Introduction'].trim();
if ($scope.guideList.rowIndex[row_index]) { // only apply Less link if needed
var index = intro.length - 1;
var tag = [];
if (intro.charAt(index) === '>') { // we have a tag at the end
index--;
do {
tag.push(intro.charAt(index));
index--;
} while (intro.charAt(index) != '/'); // the closing tag
index--; // we move the index one more for the "<"
tag.reverse(); // Reverse
tag = tag.join('');
}
var inserts = ['div', 'p']; // we insert the Less link here.
if (jQuery.inArray(tag, inserts) >= 0) { // insert into the tag
intro = intro.substr(0, index) + ' <a class="less" ng-click="$parent.guideList.showFull = false">Less</a>' + intro.substr(index);
}
else { // insert at the end of the html
intro = intro + '<a class="less" ng-click="this.showFull = false">Less</a>';
}
}
return intro;
};
// Truncate the long intro into a shorter length blurb
this.getShortIntro = function(row, row_index) {
// Truncate if necc.
var short_intro = jQuery.truncate(row['Introduction'], {
length: 250,
words: true,
ellipsis: '\u2026 <a class="more moreish" attr-ng-click="guideList.showFullClick()">Read on</a>'
});
var more = jQuery('.more', short_intro); // select more link
if (more.length) { // do we have a more link
$scope.guideList.rowIndex[row_index] = true;
}
else { // no more link
$scope.guideList.rowIndex[row_index] = false;
}
$compile(short_intro)($scope);
return short_intro;
};
}]);
As you can see in the ViewService.get() call, data is fetched and then processed. The processing simply involves putting a link at the end of the "Intro" field that is intended to be clickable.
For a while I was having a tough time to even get the ng-click directive to even show (it was being filtered out w/out $sce.trustAsHtml). Now it is there but clicking it has no effect.
The main template (from Drupal) currently looks like:
<div class="guide-listing" ng-controller="guideListController as guideList">
<a ng-click="guideList.showFullClick()">Click me</a>
<div class="guide-teaser"
ng-repeat="guide in guideList.guides"
ng-include src="'/sites/all/themes/ngTheme/ngApp/partials/guide_teaser.html'">
<!-- See guide_teaser.html partial for guide teasers -->
</div>
</div>
The ng-click as placed in the Drupal template above works as expected.
And for the partial that is used in the ng-repeat, it looks like so:
<div ng-controller="guideListController as guideList">
<h2 class="guide-teaser-title">{{guide.node_title}}</h2>
<div class="guide-teaser-intro" ng-bind-html="guide.Introduction" ng-show="guide.showFull">
{{guide.Introduction}}
</div>
<div class="guide-teaser-intro-short" ng-bind-html="guide.Intro_short" ng-show="!guide.showFull">
{{guide.Intro_short}}
</div>
</div>
So far I have only been working on getting the ng-click to work on the short_intro and have had no success so far. Any ideas as to what I am doing wrong would be greatly appreciated!
Ok, So I did get some traction! I used the ngHtmlCompile (http://ngmodules.org/modules/ng-html-compile) directive that was created by https://github.com/francisbouvier (thanks!)
The issue was that the new (dynamic) html wasn't being compiled.
At first it didn't work. I had two issues that prevented it from firing:
A: I stopped using $sce.trustAsHtml. Using this in conjunction with the directive caused the content to disappear!
B: The other issue was one of scope. After I changed the directive such that transclude was set to false it worked just fine!

AngularJS Filter throws infdig error when it creates new array

i am about to bang my head to walls. i thought i had an understanding of how angular works (filters too). but i just cant find the problem about my filter. it causes infdig. and i even dont change source array in filter.
(function () {
angular.module('project.filters').filter('splitListFilter', function () {
return function (data, chunk) {
if(!data || data.length === 0){
return data;
}
var resultArray = [];
for (var i = 0, j = data.length; i < j; i += chunk) {
resultArray.push(data.slice(i, i + chunk));
}
return resultArray;
};
});
})();
i have lists where i need to split data to x columns. it is complicated to solve with limitTo.
(limitTo: $index*x | limitTo: $last ? -z : -x)
it causes a dirty template file. so i decided to create a filter which splits an array to groups.
[1,2,3,4,5,6,7,8] -> [[1,2,3],[4,5,6],[7,8]]
so i can easily use it in my template.
Can u help me about what causes infdig in this filter?
Edit: the error message itself looks strange with some numbers in that don't appear anywhere in the code, which can be seen at http://plnkr.co/edit/pV1gkp0o5KeimwPlEMlF
10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: regularInterceptedExpression","newVal":23,"oldVal":20}],[{"msg":"fn: regularInterceptedExpression","newVal":26,"oldVal":23}],[{"msg":"fn: regularInterceptedExpression","newVal":29,"oldVal":26}],[{"msg":"fn: regularInterceptedExpression","newVal":32,"oldVal":29}],[{"msg":"fn: regularInterceptedExpression","newVal":35,"oldVal":32}]]
HTML Template
<div class="row" ng-repeat="chunk in docProfile.SysMedicalInterests | splitListFilter: 3">
<div class="col-md-4" ng-repeat="medInterest in chunk">
<label style="font-weight:normal;">
<input type="checkbox" value="{{medInterest.ID}}" ng-click="docProfile.saveInterest(medInterest.ID)" ng-checked="docProfile.isMedChecked(medInterest.ID)"> {{medInterest.Name}}
</label>
</div>
</div>
Controller Code
var me = this;
me['SysMedicalInterests'] = null;
var loadMedicalInterests = function(){
var postData = { 'Data': me['data']['subData'] };
return docService.loadMedicalInterests(postData).then(function(resp) {
me['SysMedicalInterests'] = resp['data'];
}, function(){});
};
loadMedicalInterests();
so array starts with a null reference and loads data from server. which changes array causes a second filter run. but it doesnt stop after that
Edit: here is plunkr http://plnkr.co/edit/OmHQ62VgiCXeVzKa5qjz?p=preview
Edit: related answer on so https://stackoverflow.com/a/21653981/1666060 but this still doesn't explain angular built in filters.
here is angularjs limitTo filter source code
https://github.com/angular/angular.js/blob/master/src/ng/filter/limitTo.js#L3
About what exactly causes it, I suspect is something to do with the fact that every time you run the filter a new array reference is created and returned. However, Angular's built-in filter filter does the same thing, so I'm not sure what is going wrong. It could be something to do with the fact that it's an array of arrays that is being returned.
The best I have come up with is a workaround/hack, to cache the array reference manually as an added property, which I've called $$splitListFilter on the array, and only change it if it fails a test on angular.equals with the correct results calculated in the filter:
app.filter('splitListFilter', function () {
return function (data, chunk) {
if(!data || data.length === 0){
return data;
}
var results = [];
for (var i = 0, j = data.length; i < j; i += chunk) {
results.push(data.slice(i, i + chunk));
}
if (!data.$$splitListFilter || !angular.equals(data.$$splitListFilter, results)) {
data.$$splitListFilter = results;
}
return data.$$splitListFilter;
};
});
You can see this working at http://plnkr.co/edit/vvVJcyDxsp8uoFOinX3V
The answer uses Angular 1.3.15
The JS fiddle works fine: http://jsfiddle.net/3tzapfhh/1/
Maybe you use the filter wrongly.
<body ng-app='app'>
<div ng-controller='ctrl'>
{{arr | splitListFilter:3}}
</div>
</body>

Is there a way I can speed up the way my grid reacts when I have many rows?

I have code that creates a grid like this:
<div ng-repeat="row in home.grid.data track by row.examId">
<div>{{ row.examId }}</div>
<div>xxxx</div>
</div>
I have more columns after these.
Is there a way I can speed up the way my page reacts? It seems that when
I have a lot of data in the grid then the pages reacts slowly. Would it
make a difference if I used ng-model in an input type field for the row.examId. Note that
some of the fields that follow can be edited but most are just display only.
I believe bindonce does exactly what you need.
By reducing the number of watchers it allows the page to become more responsive. Check their demos.
This is what I have done. There are two ways. Irrespective of both solutions, use bindOnce. Keep a look out on the number of watchers on the page. Look at the end of this solution - how to keep track of watchers on a page.
I have added a solution 3 and this is working awesome, styling is a bit difficult
Solution 1:
Use a pagination control with bind once.
Solution 2
This is what worked for me and it is very elegant. You repeat with bindonce and then implement infinite scrolling. I have followed this blog post and it works like a charm. The idea is you limit the number of rows and change the limit as you scroll.
ng-repeat="item in items | orderBy:prop | filter:query | limitTo:limit"
Essentially, your html would look like this. I have modified the OP's code to use bindonce.
<div id="estates-listing" extend-height>
<div class="content" infinite-scroll="addMoreItems()" infinite-scroll-distance="2">
<div class="content-wrapper">
<div class="house" bindonce="estate" ng-animate="'animate'" ng-class="{inactive: (selectedEstate != null || selectedEstate != undefined) && estate.id!=selectedEstate.id , active:(selectedEstate != null || selectedEstate != undefined) && estate.id==selectedEstate.id}" ng-repeat="estate in estates | orderBy: orderProp : orderReverse | limitTo: config.itemsDisplayedInList track by estate.id" ng-mouseover="highlightMarker(estate)" ng-mouseleave="leaveMarker(estate)" ng-click="showDetailview(estate.id)" >
<div id="l-el{{estate.id}}">
</div>
</div>
</div>
</div>
</div>
Here is the infinite scroll directive from the post. Add this to your app, please don't use the standard infinite scroll using bower install.
app.directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
var checkWhenEnabled, handler, scrollDistance, scrollEnabled;
$window = angular.element($window);
elem.css('overflow-y', 'scroll');
elem.css('overflow-x', 'hidden');
elem.css('height', 'inherit');
scrollDistance = 0;
if (attrs.infiniteScrollDistance != null) {
scope.$watch(attrs.infiniteScrollDistance, function(value) {
return scrollDistance = parseInt(value, 10);
});
}
scrollEnabled = true;
checkWhenEnabled = false;
if (attrs.infiniteScrollDisabled != null) {
scope.$watch(attrs.infiniteScrollDisabled, function(value) {
scrollEnabled = !value;
if (scrollEnabled && checkWhenEnabled) {
checkWhenEnabled = false;
return handler();
}
});
}
$rootScope.$on('refreshStart', function(event, parameters){
elem.animate({ scrollTop: "0" });
});
handler = function() {
var container, elementBottom, remaining, shouldScroll, containerBottom;
container = $(elem.children()[0]);
elementBottom = elem.offset().top + elem.height();
containerBottom = container.offset().top + container.height();
remaining = containerBottom - elementBottom ;
shouldScroll = remaining <= elem.height() * scrollDistance;
if (shouldScroll && scrollEnabled) {
if ($rootScope.$$phase) {
return scope.$eval(attrs.infiniteScroll);
} else {
return scope.$apply(attrs.infiniteScroll);
}
} else if (shouldScroll) {
return checkWhenEnabled = true;
}
};
elem.on('scroll', handler);
scope.$on('$destroy', function() {
return $window.off('scroll', handler);
});
return $timeout((function() {
if (attrs.infiniteScrollImmediateCheck) {
if (scope.$eval(attrs.infiniteScrollImmediateCheck)) {
return handler();
}
} else {
return handler();
}
}), 0);
}
};
}
]);
Solution 3:
Be adventurous and use UI-Grid, UI Grid is the new ng-grid. It is not production ready, but we are playing around in production in a table where we have over 1000 records- out of the box it is awesome. The tutorials are extensive but not much SO support. It has virtualization in built and since it is an extension of ng-grid, it has a lot of backward compatibility. Here is a example with 10,000 rows
Number of watchers on the page:
Here is a function to track the number of watchers on the page. The thumb rule is never exceed 2500 watchers, but we restrict ourselves to < 1000.
$scope.TotalWatchers = function () {
var root = $(document.getElementsByTagName('body'));
var watchers = 0;
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
watchers += (element.data().$scope.$$watchers || []).length;
}
angular.forEach(element.children(), function (childElement) {
f(angular.element(childElement));
});
};
f(root);
return watchers;
};
The biggest thing I've found to help with performance of large tables is to limit event binding to the parent object and make use of bubbling to capture the events of the children.
In the event of the parent you can get which target was hit. I use the following code.
obj.onclick = function (e) {
e = window.event || e;
var t = e.target || e.srcElement;
}
in this event e is your regular event object and t is the object that was the initial target before the event bubbled. You need to use t as 'this' references the object that the event is bound to not the object that triggered the event.
Where I was using the code which was a really large table it reduced the rendering time of the table by almost 80% by moving the events to a parent node that was static. This also helps if you need to update the contents as you don't have to re-bind any events.
Hope this helps.

Resources