Skip to content

Commit 664d950

Browse files
author
Florian F
authored
Merge pull request #1322 from marmelab/fix-infinite-scrolling
[RFR] Infinite scroll: avoid to load multiple time at the same time
2 parents d30d713 + 0ec36bc commit 664d950

File tree

3 files changed

+172
-77
lines changed

3 files changed

+172
-77
lines changed

package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"babel-preset-stage-2": "^6.13.0",
5353
"chai": "^3.3.0",
5454
"css-loader": "^0.25.0",
55+
"diff": "~3.2.0",
5556
"exports-loader": "^0.6.2",
5657
"extract-text-webpack-plugin": "^0.8.0",
5758
"fakerest": "^1.1.4",
@@ -62,16 +63,17 @@
6263
"jasmine-spec-reporter": "^3.2.0",
6364
"jshint-stylish": "~0.1.3",
6465
"json-server": "~0.8.8",
65-
"karma": "~0.13.21",
66-
"karma-chrome-launcher": "~0.2.2",
67-
"karma-jasmine": "~0.3.7",
68-
"karma-mocha-reporter": "^2.2.1",
69-
"karma-ng-html2js-preprocessor": "~0.2.1",
66+
"karma": "~0.13.22",
67+
"karma-chrome-launcher": "~0.2.3",
68+
"karma-jasmine": "~0.3.8",
69+
"karma-mocha": "~1.3.0",
70+
"karma-mocha-reporter": "~2.2.2",
71+
"karma-ng-html2js-preprocessor": "~0.2.2",
7072
"karma-ng-scenario": "~0.1.0",
71-
"karma-phantomjs-launcher": "~1.0.0",
73+
"karma-phantomjs-launcher": "~1.0.4",
7274
"karma-webpack": "~1.7.0",
7375
"lodash": "^4.17.2",
74-
"mocha": "^2.1.0",
76+
"mocha": "~2.5.3",
7577
"ng-annotate-loader": "0.0.2",
7678
"ngtemplate-loader": "^1.3.0",
7779
"node-sass": "^3.9.3",

src/javascripts/ng-admin/Crud/list/maDatagridInfinitePagination.js

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import angular from 'angular';
22

3-
export default function maDatagridInfinitePagination($window, $document) {
3+
const isScrollingDown = wheelEvent => {
4+
if (!wheelEvent) return true;
5+
6+
return wheelEvent.deltaY > 0;
7+
};
48

5-
var windowElement = angular.element($window);
6-
var offset = 100,
7-
body = $document[0].body;
9+
export default function maDatagridInfinitePagination($window, $document) {
10+
const offset = 100;
11+
const body = $document[0].body;
812

913
return {
1014
restrict: 'E',
@@ -13,28 +17,59 @@ export default function maDatagridInfinitePagination($window, $document) {
1317
totalItems: '@',
1418
nextPage: '&'
1519
},
16-
link: function(scope) {
17-
var perPage = parseInt(scope.perPage, 10) || 1,
18-
totalItems = parseInt(scope.totalItems, 10),
19-
nbPages = Math.ceil(totalItems / perPage) || 1,
20-
page = 1,
21-
loadedPages = [];
22-
function handler() {
23-
if (body.offsetHeight - $window.innerHeight - $window.scrollY < offset) {
24-
if (page >= nbPages) {
25-
return;
26-
}
27-
page++;
28-
if(page in loadedPages){
29-
return;
20+
link(scope) {
21+
scope.processing = false;
22+
const perPage = parseInt(scope.perPage, 10) || 1;
23+
const totalItems = parseInt(scope.totalItems, 10);
24+
const nbPages = Math.ceil(totalItems / perPage) || 1;
25+
const loadedPages = [];
26+
let page = 1;
27+
let interval;
28+
29+
const handler = (wheelEvent) => {
30+
if (!isScrollingDown(wheelEvent) || scope.processing || !!interval) {
31+
return;
32+
}
33+
34+
scope.processing = true;
35+
36+
interval = setInterval(() => {
37+
if (body.offsetHeight - $window.innerHeight - $window.scrollY < offset) {
38+
if (page >= nbPages) {
39+
return;
40+
}
41+
42+
page++;
43+
44+
if (page in loadedPages) {
45+
return;
46+
}
47+
48+
loadedPages.push(page);
49+
scope.nextPage()(page);
50+
} else {
51+
scope.processing = false;
52+
53+
if (interval) {
54+
clearInterval(interval);
55+
interval = null;
56+
}
3057
}
31-
loadedPages.push(page);
32-
scope.nextPage()(page);
58+
}, 100);
59+
};
60+
61+
// Trigger the scroll at least once
62+
// This way, it loads at least one screen of data to enable further scrolling
63+
// @see https://github.com/marmelab/ng-admin/issues/681
64+
handler();
65+
66+
$window.addEventListener('wheel', handler);
67+
scope.$on('$destroy', () => {
68+
$window.removeEventListener('wheel', handler);
69+
70+
if (interval) {
71+
clearInterval(interval);
3372
}
34-
}
35-
windowElement.bind('scroll', handler);
36-
scope.$on('$destroy', function destroy() {
37-
windowElement.unbind('scroll', handler);
3873
});
3974
}
4075
};
Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,65 @@
11
/*global angular,inject,describe,it,jasmine,expect,beforeEach,module*/
2+
const directive = require('../../../../ng-admin/Crud/list/maDatagridInfinitePagination');
3+
24
describe('directive: ma-datagrid-infinite-pagination', function () {
3-
var directive = require('../../../../ng-admin/Crud/list/maDatagridInfinitePagination'),
4-
$compile,
5-
scope,
6-
$window,
7-
$document,
8-
element,
9-
bodyHeightMock,
10-
pageSize = 2000,
11-
directiveUsage = '<ma-datagrid-infinite-pagination next-page="nextPage" total-items="{{ totalItems }}" per-page="{{ itemsPerPage }}"></ma-datagrid-infinite-pagination>';
12-
13-
function initializeBodyHeightMock(){
14-
if(!angular.element($document[0].querySelector('#mock')).length){
5+
let $compile;
6+
let $scope;
7+
let $window;
8+
let $document;
9+
let element;
10+
let bodyHeightMock;
11+
let handler;
12+
let pageSize = 2000;
13+
let directiveUsage = `<ma-datagrid-infinite-pagination
14+
next-page="nextPage"
15+
total-items="{{ totalItems }}"
16+
per-page="{{ itemsPerPage }}"
17+
></ma-datagrid-infinite-pagination>`;
18+
19+
function waitForProcessing(scope, callback) {
20+
const interval = setInterval(() => {
21+
if (!scope.processing) {
22+
clearInterval(interval);
23+
callback(null, true);
24+
}
25+
}, 100);
26+
}
27+
28+
function initializeBodyHeightMock() {
29+
if(!angular.element($document[0].querySelector('#mock')).length) {
1530
bodyHeightMock = angular.element(`<div id="mock" style="height:${pageSize}px"></div>`)[0];
1631
angular.element($document[0].body).append(bodyHeightMock);
17-
}else{
32+
} else {
1833
simulateLoadOnBodyHeight(1);
1934
}
2035
}
2136

22-
function simulateLoadOnBodyHeight(page){
37+
function simulateLoadOnBodyHeight(page) {
2338
angular.element($document[0].querySelector('#mock')).css('height',(pageSize*page) + 'px');
2439
}
2540

26-
function simulateScrollToPage(page){
27-
$window.scrollY = pageSize * (page-1) + 1500;
28-
angular.element($window).triggerHandler('scroll');
41+
function simulateScrollToPage(page, scope, callback) {
42+
const scrollSize = pageSize * (page - 1) + 1500;
43+
$window.scrollY = scrollSize;
44+
handler({ deltaY: scrollSize });
45+
46+
if (scope && callback) {
47+
waitForProcessing(scope, callback);
48+
}
2949
}
3050

31-
function initializeScope(){
32-
scope.nextPage = jasmine.createSpy('nextPage').and.callFake(function(page) {
51+
function initializeScope(scope) {
52+
scope.nextPage = jasmine.createSpy('nextPage').and.callFake(() => (page) => {
3353
simulateLoadOnBodyHeight(page);
3454
});
3555
scope.totalItems = 100;
3656
scope.itemsPerPage = 10;
3757
}
3858

39-
function initializeElement(){
40-
element = $compile(directiveUsage)(scope);
41-
scope.$digest();
59+
function initializeElement() {
60+
initializeScope($scope);
61+
element = $compile(directiveUsage)($scope);
62+
$scope.$digest();
4263
}
4364

4465
angular.module('testapp_DatagridInfinitePagination', [])
@@ -48,45 +69,82 @@ describe('directive: ma-datagrid-infinite-pagination', function () {
4869

4970
beforeEach(inject(function (_$compile_, _$rootScope_, _$window_, _$document_) {
5071
$compile = _$compile_;
51-
scope = _$rootScope_.$new();
72+
$scope = _$rootScope_.$new();
5273
$window = _$window_;
5374
$window.innerHeight = 759;
75+
spyOn($window, 'addEventListener').and.callFake((evt, callback) => {
76+
handler = callback;
77+
});
5478
$document = _$document_;
5579
initializeBodyHeightMock();
56-
initializeScope();
5780
initializeElement();
5881
}));
5982

60-
it('should trigger next-page when scrolling', function () {
61-
simulateScrollToPage(2);
62-
expect(scope.nextPage).toHaveBeenCalled();
63-
});
83+
it('should trigger next-page when scrolling', function (done) {
84+
const isolatedScope = element.isolateScope();
85+
initializeScope(isolatedScope);
6486

65-
it('should trigger next-page twice when scrolling twice', function(){
66-
simulateScrollToPage(2);
67-
simulateScrollToPage(3);
68-
expect(scope.nextPage.calls.count()).toEqual(2);
87+
waitForProcessing(isolatedScope, () => {
88+
simulateScrollToPage(2, isolatedScope, () => {
89+
expect(isolatedScope.nextPage).toHaveBeenCalled();
90+
done();
91+
});
92+
});
6993
});
7094

71-
it('should trigger next-page with right page number', function(){
72-
simulateScrollToPage(2);
73-
simulateScrollToPage(3);
74-
expect(scope.nextPage.calls.argsFor(0)).toEqual([2]);
75-
expect(scope.nextPage.calls.argsFor(1)).toEqual([3]);
95+
it('should trigger next-page twice when scrolling twice', function(done) {
96+
const isolatedScope = element.isolateScope();
97+
initializeScope(isolatedScope);
98+
99+
waitForProcessing(isolatedScope, () => {
100+
simulateScrollToPage(2, isolatedScope, () => {
101+
simulateScrollToPage(3, isolatedScope, () => {
102+
expect(isolatedScope.nextPage.calls.count()).toEqual(3);
103+
done();
104+
});
105+
});
106+
});
76107
});
77108

78-
it('should not trigger next-page if not scrolling', function () {
79-
expect(scope.nextPage).not.toHaveBeenCalled();
109+
it('should trigger next-page with right page number', function(done) {
110+
const isolatedScope = element.isolateScope();
111+
initializeScope(isolatedScope);
112+
113+
const argsForCall = [];
114+
115+
isolatedScope.nextPage = jasmine.createSpy('nextPage').and.callFake(() => (page) => {
116+
simulateLoadOnBodyHeight(page);
117+
argsForCall.push(page);
118+
});
119+
120+
waitForProcessing(isolatedScope, () => {
121+
simulateScrollToPage(2, isolatedScope, () => {
122+
simulateScrollToPage(3, isolatedScope, () => {
123+
expect(argsForCall[0]).toEqual(2);
124+
expect(argsForCall[1]).toEqual(3);
125+
done();
126+
});
127+
});
128+
});
80129
});
81130

82-
it('should not trigger next-page when scrolling up', function(){
83-
simulateScrollToPage(2);
84-
simulateScrollToPage(3);
85-
simulateScrollToPage(2);
86-
expect(scope.nextPage.calls.count()).toEqual(2);
131+
it('should not trigger next-page when scrolling up', function(done) {
132+
const isolatedScope = element.isolateScope();
133+
initializeScope(isolatedScope);
134+
135+
waitForProcessing(isolatedScope, () => {
136+
simulateScrollToPage(2, isolatedScope, () => {
137+
simulateScrollToPage(3, isolatedScope, () => {
138+
simulateScrollToPage(2, isolatedScope, () => {
139+
expect(isolatedScope.nextPage.calls.count()).toEqual(3);
140+
done();
141+
});
142+
});
143+
});
144+
});
87145
});
88146

89-
afterEach(function(){
90-
scope.$destroy();
147+
afterEach(function() {
148+
$scope.$destroy();
91149
});
92150
});

0 commit comments

Comments
 (0)