I am attempting to create infinite scrolling on my web page using an example I found. However, the page fills up completely with all the items instead of just showing several items at a time. In other words it is not doing infinite scrolling. I noticed in some of the examples they parsed out data in chunks but in the real world how are you supposed to do that?
Below is my html code:
<table class="table table-striped table-bordered"><tr>
<th style="text-align:center;">User ID</th> <th>Username</th><th>Rank</th>
<th>Posts</th><th>Likes</th> <th>Comments</th> <th>Flags</th><th>Status</th><th>Action</th></tr>
<tr><td class="center">
<div ng-app='scroll' ng-controller='Scroller'>
<div when-scrolled="loadMore("")">
<div ng-repeat='item in items'>
<span>{{item.id}}
<span style="position:absolute;left:140px;">{{item.username}}</span>
<span style="position:absolute;left:290px;">{{item.rank}}</span>
<span style="position:absolute;left:360px;">{{item.posts}}</span>
<span style="position:absolute;left:440px;">{{item.likes}}</span>
<span style="position:absolute;left:530px;">{{item.comments}}</span>
<span style="position:absolute;left:640px;">{{item.flags}}</span>
<span class="label label-success" style="position:absolute;left:710px;">Active</span>
<a style="position:absolute;left:790px;" class="btn btn-info" style="width:30px" ng-href='/admin/userDetail?userid={{item.id}}'>
View Detail</a>
<hr>
</div>
</div>
</div>
</td></tr>
</table>
Below is my angularjs code:
function Scroller($scope, $http, $q, $timeout) {
$scope.items = [];
var lastuser = '999999';
$scope.loadMore = function(type) {
todate = document.getElementById("charttype").value;
var url = "/admin/getusers?type=" + todate + "&lastuser=" + lastuser;
var d = $q.defer();
$http({
'url': url,
'method': 'GET'
})
.success(function (data) {
var items = data.response;
for (var i = $scope.items.length; i < items.length; i++) {
$scope.items.push(items[i]);
count++;
if (count > 100)
{
lastuser = $scope.items[i].id;
break;
}
d.resolve($scope.items);
d.promise.then(function(data) {
});
}
)
.error(function (data) {
console.log(data);
});
return d.promise;
};
$scope.loadMore();
}
angular.module('scroll', []).directive('whenScrolled', function() {
return function(scope, elm, attr) {
var raw = elm[0];
alert("scroll");
elm.bind('scroll', function() {
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
scope.$apply(attr.whenScrolled);
}
});
};
});
My question is why does my web page show all 3200 lines initially rather than allowing me to do infinite scrolling. You will notice I put an alert in the scroll module and it is never displayed. Do I have to incrementally read my database? Any help is appreciated.
You are adding all of the items returned from your API call into $scope.items.
for (var i = 0; i < items.length; i++) {
$scope.items.push(items[i]);
}
Don't you want to add only a subset of those items?
P.S. Might help if you create a Plunkr to show the specific problem.
EDIT:
Based on your comment about the directive not working, I put together this Plunkr, which is a copy of your code but with the $http get code ripped out. The "scroll" alert fires here. I think you're just missing a closing bracket on your for loop (since I don't have your API endpoint to test against, I can't actually run your code live).
EDIT 2:
I'm not sure why you aren't seeing the function fire correctly on scroll. I've set up another plunker where I've changed the result of the scroll event firing to show an alert and load more items from a data variable, so you can see that the scroll event is firing correctly and it will load more items.
Related
It is not like it is slow on rendering many entries. The problem is that whenever the $scope.data got updated, it adds the new item first at the end of the element, then reduce it as it match the new $scope.data.
For example:
<div class="list" ng-repeat="entry in data">
<h3>{{entry.title}}</h3>
</div>
This script is updating the $scope.data:
$scope.load = function() {
$scope.data = getDataFromDB();
}
Lets say I have 5 entries inside $scope.data. The entries are:
[
{
id: 1,
title: 1
},
{
id: 2,
title: 2
},
......
]
When the $scope.data already has those entries then got reloaded ($scope.data = getDataFromDB(); being called), the DOM element for about 0.1s - 0.2s has 10 elements (duplicate elements), then after 0.1s - 0.2s it is reduced to 5.
So the problem is that there is delay about 0.1s - 0.2s when updating the ng-repeat DOM. This looks really bad when I implement live search. Whenever it updates from the database, the ng-repeat DOM element got added up every time for a brief millisecond.
How can I make the rendering instant?
EDITED
I will paste all my code here:
The controller:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = [];
$scope.loadViewModels($scope.orderBy, table);
}
$scope.loadViewModels = function (orderBy, table, cb) {
if (!$scope.endOfPage) {
let searchKey = $scope.page.searchString;
let skip = ($scope.currentPage - 1) * $scope.itemsPerPage;
let searchClause = '';
if (searchKey && searchKey.length > 0) {
let searchArr = [];
$($scope.vmKeys).each((i, key) => {
searchArr.push(key + ` LIKE '%` + searchKey + `%'`);
});
searchClause = `WHERE ` + searchArr.join(' OR ');
}
let sc = `SELECT * FROM ` + table + ` ` + searchClause + ` ` + orderBy +
` LIMIT ` + skip + `, ` + $scope.itemsPerPage;
sqlite.query(sc, rows => {
$scope.$apply(function () {
var data = [];
let loadedCount = 0;
if (rows != null) {
$scope.currentPage += 1;
loadedCount = rows.length;
if (rows.length < $scope.itemsPerPage)
$scope.endOfPage = true
for (var i = 0; i < rows.length; i++) {
let item = rows.item(i);
let returnObject = {};
$($scope.vmKeys).each((i, key) => {
returnObject[key] = item[key];
});
data.push(returnObject);
}
$scope.viewModels = $scope.viewModels.concat(data);
}
else
$scope.endOfPage = true;
if (cb)
cb(loadedCount);
})
});
}
}
The view:
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
<div class="row note-list" ng-if="showList">
<h3>Notes</h3>
<input ng-model="page.searchString" id="search"
ng-keyup="search('notes')" type="text" class="form-control"
placeholder="Search Notes" style="margin-bottom:10px">
<div class="col-12 note-list-item"
ng-repeat="data in viewModels track by data.id"
ng-click="edit(data.id)"
ontouchstart="touchStart()" ontouchend="touchEnd()"
ontouchmove="touchMove()">
<p ng-class="deleteMode ? 'note-list-title w-80' : 'note-list-title'"
ng-bind-html="data.title"></p>
<p ng-class="deleteMode ? 'note-list-date w-80' : 'note-list-date'">{{data.dateCreated | displayDate}}</p>
<div ng-if="deleteMode" class="note-list-delete ease-in" ng-click="delete($event, data.id)">
<span class="btn fa fa-trash"></span>
</div>
</div>
<div ng-if="!deleteMode" ng-click="new()" class="add-btn btn btn-primary ease-in">
<span class="fa fa-plus"></span>
</div>
</div>
<div ng-if="!showList" class="ease-in">
<div>
<div ng-click="back()" class="btn btn-primary"><span class="fa fa-arrow-left"></span></div>
<div ng-disabled="!isDataChanged" ng-click="save()" class="btn btn-primary" style="float:right">
<span class="fa fa-check"></span>
</div>
</div>
<div contenteditable="true" class="note-title"
ng-bind-html="selected.title" id="title">
</div>
<div contenteditable="true" class="note-container" ng-bind-html="selected.note" id="note"></div>
</div>
</div>
<script src="../js/pages/note.js"></script>
Calling it from:
$scope.loadViewModels($scope.orderBy, 'notes');
The sqlite query:
query: function (query, cb) {
db.transaction(function (tx) {
tx.executeSql(query, [], function (tx, res) {
return cb(res.rows, null);
});
}, function (error) {
return cb(null, error.message);
}, function () {
//console.log('query ok');
});
},
It is apache cordova framework, so it uses webview in Android emulator.
My Code Structure
<html ng-app="app" ng-controller="pageController">
<head>....</head>
<body>
....
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
....
</div>
</body>
</html>
So there is controller inside controller. The parent is pageController and the child is noteController. Is a structure like this slowing the ng-repeat directives?
Btw using track by is not helping. There is still delay when rendering it. Also I can modify the entries as well, so when an entry was updated, it should be updated in the list as well.
NOTE
After thorough investigation there is something weird. Usually ng-repeat item has hash key in it. In my case ng-repeat items do not have it. Is it the cause of the problem?
One approach to improve performance is to use the track by clause in the ng-repeat expression:
<div class="list" ng-repeat="entry in data track by entry.id">
<h3>{{entry.title}}</h3>
</div>
From the Docs:
Best Practice: If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance, e.g. item in items track by item.id. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
For more information, see
AngularJS ngRepeat API Reference -- Tracking and Duplicates
In your html, try this:
<div class="list" ng-repeat="entry in data">
<h3 ng-bind="entry.title"></h3>
</div>
After thorough research, I found my problem. Every time I reset / reload my $scope.viewModels I always assign it to null / empty array first. This what causes the render delay.
Example:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = []; <------ THIS
$scope.loadViewModels($scope.orderBy, table);
}
So instead of assigning it to null / empty array, I just replace it with the new loaded data, and the flickering is gone.
I have offers table and users table on parse server. I did a query for he offers table and it worked great (both console log and html - I had issues with async and the Q.promise helped). Now I'm trying to add two elements that are in the users table. I get it on the console, but not on the page. Here is what I have on the offers.service:
this.getAllOffers = function () {
var Q = $q.defer();
console.log('getAllOffers called');
//all offers filter is selected
this.allOffersFilter = false;
var offers = Parse.Object.extend("Offer");
var exchanges = Parse.Object.extend("Exchanges");
var users = Parse.Object.extend("User");
var query = new Parse.Query(offers);
var userQuery = new Parse.Query(users);
var results = [];
query.descending("createdAt");
query.limit(4);
userQuery.find().then(function(users) {
for (i = 0; i < users.length; i++) {
foundUsers = users[i];
query.find().then( function(offers){
for(i = 0; i < offers.length; i++){
found = offers[i];
var result = {};
result.date = found.get("createdAt");
result.price = found.get("price");
result.status = found.get("accepted");
result.lastName = foundUsers.get("lastName");
result.companyName = foundUsers.get("companyName");
console.log(result.companyName);
console.log(result.price);
}
});
results.push(result);
}
Q.resolve(results);
});
return Q.promise;
};
Then my HTML:
<!--List of offers-->
<div class="col-md-3">
<h4>List of offers</h4>
<div ng-if="offersList">
<div ng-repeat="offer in offersList">
<div class="offer card">
<div>{{offer.username}}</div>
<div>{{offer.companyName}}</div>
<div>{{offer.date}}</div>
<div>{{offer.price}}</div>
<div>{{offer.status}}</div>
</div>
</div>
</div>
<div ng-if="!(offersList)">There are no offers</div>
</div>
Then my component:
angular.module('offersPage')
.component('offersPage', {
templateUrl: 'pages/offers-page/offers-page.template.html',
controller: function(AuthService, PageService, OffersService,
$scope) {
// Functions for offers-page
// Check if user is logged in and verified on page load
AuthService.userLoggedin(function(loggedIn, verified) {
if(!verified) {
PageService.redirect('login');
}
});
this.$onInit = function() {
OffersService.getAllOffers().then(function(offersList) {
$scope.offersList = offersList;
});
}
}
});
THANKS IN ADVANCE !
You are resolving $q before results is populated, so, you list is empty.
I don't know about Parse server, but if userQuery.find().then is async, then need to move Q.resolve(results); inside it, or probably inside query.find().then.
When you do an ng-if in angularjs it literally takes out the element and when it puts it in it is as a child scope. To fix this you need to make sure and put $parent on any child element inside an ng-if. See below. Make sure to use track by $index to when you are doing repeats its good practice. Also notice you dont need to $parent anything in the repeat since it is referencing offerwhich is defined.
Code:
<div ng-if="offersList">
<div ng-repeat="offer in $parent.offersList track by $index">
<div class="offer card">
<div>{{offer.username}}</div>
<div>{{offer.companyName}}</div>
<div>{{offer.date}}</div>
<div>{{offer.price}}</div>
<div>{{offer.status}}</div>
</div>
</div>
</div>
I am NEW to MVC as well as AngularJs and have been toiling over this for days. Although I feel that I am getting closer....still no cigar.
The problem: I have a list of reports that are grouped on the report type(name). I am trying to use an accordion to show and hide the list of reports in each group.
My controller.js looks like this (I know that it is wrong):
window.app.controller('relatedReportsController', ['$scope', '$timeout', 'relatedReportsService',
function ($scope, $timeout, relatedReportService) {
initialize();
function initialize()
{
$scope.relatedReports = [];
$scope.rollupVisible = false;
}
function sortOn(collection, name)
{
collection.sort(
function (a, b) {
if (a[name] <= b[name]) {
return (-1);
}
return (1);
});
}
$scope.groupBy = function (attribute) {
$scope.Groups = [];
sortOn($scope.relatedReports, attribute);
for (var i=0; i< $scope.relatedReports.length; i++)
{
var report = $scope.relatedReports[i];
}
}
$scope.toggleRollup = function($event)
{
if (angular.element($event.targe).hasClass('glyph')) return;
relatedReportService.$promise.then(function (data) {
$scope.relatedReports = data;
})
}
}]);
My page looks like this:
<li class="fruitRollup header row" ng-controller="relatedReportsController">
<div class="suitcaseheader">
<span class="col-xs-10 zero firstlabel">{{group.Name}}</span>
<span class="col-xs-3 zero datepad">Date</span>
<span class="floatR2">View</span>
<span class="clear"></span>
</div>
<div class="eaten">
<ul class="data">
#*#foreach (var reportResult in resultGroup.OrderByDescending(r=>r.Date))
{*#
<li class="data row" ng-repeat="report in group.reports" ng-controller="relatedReportsController">
<div class="suitcase">
<span class="col-xs-10 zero accountNumberColumn"></span>
#*<span class="middle zero">#reportResult.Date.Replace("12:00:00","")</span>*#
<span class="middle zero">{{report.Date}}</span>
<span class="floatR2">
<a class="icon-view glyph" target="_blank" href="#Url.ActionEncodedParameters("ViewDocument", "DocumentSearch", new { id = reportResult.Id })"></a>
</span>
<span class="clear"></span>
</div>
As you can see I need a lot of help. Thanks in advance!
The code that I had written in the controller.js was not properly getting the data. In trying to use a combination of samples that I found on the internet, I was thoroughly confused. I figured this out yesterday. I didn't need to get the data through the controller.js because the data was already being fetched through my page controller.cs and viewmodel. All I ended up needing was to use ng-show to show and hide the sections.
Thanks for taking the time to try to help.
I followed information on this answer
But it doesn't work in my situation.
Chrome Inspector console says "ReferenceError: dataResponse is not defined"
maybe that is the problem?
I am trying to GET this JSON from url:
[{"app_id":1,"app_name":"oh yeeah","app_description":"desc","app_price":111,"is_activated":false,"video":"videolink"},{"app_id":2,"app_name":"oh yeaaaeah","app_description":"ewaewq","app_price":222,"is_activated":false,"video":"fuck off"},{"app_id":3,"app_name":"oh yeaaaeah","app_description":"ewaewq","app_price":333,"is_activated":false,"video":"fuck off"}]
This is my javascript code
var appstore = angular.module('appstore', []);
appstore.service('dataService', function($http) {
delete $http.defaults.headers.common['X-Requested-With'];
this.getData = function(callbackFunc) {
$http({
method: 'GET',
url: '/administrator/components/com_apps/loadAppsJson.php'
}).success(function(data){
callbackFunc(data);
}).error(function(){
alert("error");
});
}
});
appstore.controller('app_Ctrl', function($scope, dataService) {
$scope.apps = [
{app_id:1, app_name:'oh yeah', app_description:'$app_description', app_price:111, is_activated:false, video:'$videolink'},
{app_id:2, app_name:'oh yeah', app_description:'$app_description', app_price:111, is_activated:false, video:'$videolink'},
{app_id:3, app_name:'oh yeah', app_description:'$app_description', app_price:111, is_activated:false, video:'$videolink'},
];
//$scope.apps = null;
dataService.getData(function(dataResponse) {
$scope.apps = dataResponse;
alert(dataResponse);
});
console.log(dataResponse);
console.log($scope.apps);
//get images thumbs
for(app = 0; app <= $scope.apps.length-1; app++) {
$scope.apps[app].thumb = ("000" + $scope.apps[app].app_id).slice(-3);
}
//separate apps to columns
var columns = [];
for (var i = 0; i < $scope.apps.length; i++ ) {
if (i % 3 == 0) columns.push([]);
columns[columns.length-1].push($scope.apps[i]);
}
$scope.columns = columns;
});
My HTML view
<div ng-controller="app_Ctrl">
<div class="row"></div>
<div class="row">
<div class="row" ng-repeat="apps in columns">
<div id="app_id_{{ app.app_id }}" class="col-lg-4" ng-repeat="app in apps | filter:search">
<div class="thumbnail" ng-class="app.is_activated ? 'activated' : ''">
<!-- -->
<img ng-src="/images/apps/app_images/{{ app.thumb }}_thumb.jpg" alt="{{ app.app_name }}" title="{{ app.app_name }}">
<div class="caption">
<h3>{{ app.app_name }}</h3>
<p class="app_price">{{ app.app_price }} €</p>
<div style="clear:both;"></div>
<p class="app_card_description">{{ app.app_description | limitTo:100 }}...</p>
Info
Video <span class="glyphicon glyphicon-facetime-video"></span>
{{ app.is_activated ? 'Aktivované' : 'Aktivovať' }}
</div>
</div>
</div>
</div>
To elaborate on what #Mritunjay said in the comments; review this code with comments:
dataService.getData(
// this is your callback function which has an argument for dataResponse
// the dataResponse variable will only be defined within the Call back function
function(dataResponse) {
$scope.apps = dataResponse;
alert(dataResponse);
// The Curly Brackets that follow mark the end of the callback handler method
});
// This log statement is not in the callback handler and there is no defined dataResponse variable which is probably why you got an error in the console
console.log(dataResponse);
You can fix this by moving the dataResponse log into the callback method, like this:
dataService.getData(function(dataResponse) {
$scope.apps = dataResponse;
alert(dataResponse);
console.log(dataResponse);
});
There appear to be other problems with your code, in that you are trying to access $scope.apps before the data is returned; which will hinder your processing. Easiest approach would be to move that processing into the result handler:
// define $scope.columns outside of the result handler
$scope.columns = [];
// call to method in service
dataService.getData(function(dataResponse) {
$scope.apps = dataResponse;
alert(dataResponse);
console.log(dataResponse);
// inside the result handler; you run this code after $scope.apps is defined:
for(app = 0; app <= $scope.apps.length-1; app++) {
$scope.apps[app].thumb = ("000" + $scope.apps[app].app_id).slice(-3);
}
//separate apps to columns
var columns = [];
for (var i = 0; i < $scope.apps.length; i++ ) {
if (i % 3 == 0) columns.push([]);
columns[columns.length-1].push($scope.apps[i]);
}
$scope.columns = columns;
});
That's what promises and asynchronous calls are all about.
console.log(dataResponse);
console.log($scope.apps);
The first one won't work because dataResource is a private variable and not part of the same scope you're trying to print.
The second one won't work either because that get's populated at future time (after X seconds), after the $http request is finished so it will only be availableat that point.
One way to do something after the object is populated is to use
$scope.$watch("apps", function (){
// do stuff
});
So, I have observable array with sites, which is shown via template. If I'll add site to this array, template is not updated, but if I'll remove site from array – voila! template became updated and all previously added sites became displayed too.
If I'll use nifty hack (commented in code) with replacement of whole array to new one then everything works.
BTW, I load template via AJAX and use "ko.applyBindings(viewModel)" after. I assume that works fine, because initial sites are displayed correctly.
$(function(){
//site entry in user's sites list
var siteObject = function(url, lastChecked, status){
this.url = url;
this.lastChecked = (lastChecked == 'undefined') ? '' : lastChecked;
this.status = (status == 'undefined') ? 'not_checked_yet' : status;
this.toDelete = false;
this.remove = function() {viewModel.sites.remove(this)};
};
viewModel = {
//=========== sites list managment ==========================
sites: ko.observableArray(),
//on "add" click in "add site" form
addSite: function(){
var $form = $('#add_site_form');
var siteUrl = $form.find('input[name="site"]').val();
/*nifty hack <----
var sites = this.sites();
sites.push(new siteObject(siteUrl));
this.sites(sites);*/
this.sites.push(new siteObject(siteUrl));
},
//on "remove sites" button click
removeSites: function() {
var sitesToRemove = [];
$.each(this.sites(), function(){
if (this.toDelete) sitesToRemove.push(this);
});
if (sitesToRemove.length == 0)
alert("Ни одного сайта не было выбрано для удаления.");
else {
var message = "Вы точно хотите перестать отслеживать";
for (var i in sitesToRemove) {
message += "\n\"" + sitesToRemove[i].url + "\"";
}
message += "?";
if (confirm(message)) {
$.each(sitesToRemove, function(){this.remove()});
//save new sites list to db
this.saveSitesListToDb();
}
}
//hide form
$('#remove_sites_form').slideToggle();
//toggle checkboxes
$('#content_sites_list .site_info input[type="checkbox"]').slideToggle();
};
And the template:
<!-- end of menu -->
<div id="content_sites_list"
class="grid_12"
data-bind="template: {name: 'sites_list_template', foreach: sites}"></div>
<!-- Templates -->
<script id="sites_list_template" type="text/x-jquery-tmpl">
<div class="site">
<div class="site_panel grid_12">
<div class="site_info">
–
<input type="checkbox" value="${url}"
class="delete_checkbox" data-bind="checked: toDelete" />
${url.substr(7)}
{{if status == "200"}}
<img src="img/green_light.png" alt="ok"/>
{{/if}}
</div>
<div class="site_stat">
<div class="site_last_check">Последняя проверка: ${dateTimestamp}</div>
</div>
</div>
</div>
</script>
I've tried this on latest beta on knockoutjs and on stable one.
I have made a jsFiddle which works fine.
There were some problems that JSLint was complaining about in the removeSites function of the viewModel. I fixed those and added a button and input field to be able to give some input, and everything ran smooth.
So you could try updating your removeSites function and see if it helps you,