EN
JavaScript - infinite scrolling (Vanilla JS)
9 points
In this article, we would like to show how to create infinite scrolling using JavaScript.
The presented solution uses a barrier element to detect when data should be loaded during scrolling. A barrier element is an element that, when visible, loads new data. So, by fetching data and placing it on the page, the barrier is shifted outside of screen causing stopping data fetching until next scrolling occurs.
The main advantages:
- This approach simplifies scroll position detection when we have nested additional elements below the display data or are designing a responsive layout.
- This approach is resist on situations when received data is not able to fill screen. Script causes additional data fetching until screen is filled or data ends.
Hint: check this article to know how to detect when barrier element is visible.
Simple preview:

Practical example:
xxxxxxxxxx
1
2
<html>
3
<head>
4
<style>
5
6
body { height: 300px }
7
8
#content { background: #fff6c8; }
9
#barrier { background: #ffc8c8; }
10
11
</style>
12
<script>
13
14
// Calculates element's absolute position.
15
//
16
function calculateOffset(element) {
17
var result = 0;
18
while (element) {
19
var value = element.offsetTop;
20
if (value) {
21
result += value;
22
}
23
element = element.offsetParent; // jump to the next proper parent element
24
}
25
return result;
26
}
27
28
// Calculates space between elements.
29
//
30
function calculateSpace(aElement, bElement) {
31
var aOffset = calculateOffset(aElement);
32
var bOffset = calculateOffset(bElement);
33
return bOffset - aOffset;
34
}
35
36
// Calculates container's scroll position.
37
//
38
function calculateScroll(container) {
39
return container.scrollY || container.scrollTop || 0;
40
}
41
42
// Calculates container's inner height.
43
//
44
function calculateHeight(container) {
45
return container.innerHeight || container.clientHeight || 0;
46
}
47
48
// Checks barrier visibility inside container.
49
//
50
function checkVisibility(container, barrier, space) {
51
var barrierTop = calculateSpace(container, barrier);
52
var containerScroll = calculateScroll(container);
53
var containerHeight = calculateHeight(container);
54
if (barrierTop < containerScroll + containerHeight + space) {
55
return true;
56
}
57
return false;
58
}
59
60
// Creates logic that lets to track and control data loading using infinite scolling logic.
61
//
62
function InfinityScroll(config) {
63
var _space = config.space || 0;
64
var _loader = config.loader;
65
var _container = config.container, _content = config.content, _barrier = config.barrier;
66
var _onLoading = config.onLoading, _onLoaded = config.onLoaded, _onError = config.onError;
67
var _destroyed = false;
68
var _enabled = false;
69
var _loading = false;
70
var _offset = 0;
71
var signal = function() {
72
if (_loading) {
73
return;
74
}
75
if (checkVisibility(_container, _barrier, _space)) {
76
_loading = true;
77
if (_onLoading) {
78
_onLoading();
79
}
80
const callback = function(offset, data, error) {
81
if (_enabled) {
82
_loading = false;
83
if (error) {
84
_onError(error);
85
} else {
86
_offset = offset;
87
if (_onLoaded) {
88
_onLoaded(data);
89
if (_enabled === false) {
90
return;
91
}
92
}
93
if (data && data.length > 0) {
94
setTimeout(signal);
95
}
96
}
97
}
98
};
99
try {
100
_loader(_offset, callback);
101
} catch (e) {
102
_loading = false;
103
if (_onError) {
104
_onError('Loading error.');
105
}
106
}
107
}
108
};
109
this.signal = function() {
110
if (_destroyed) {
111
throw new Error('Object has been destroyed.');
112
}
113
if (_enabled) {
114
signal();
115
}
116
};
117
this.enable = function() {
118
if (_destroyed) {
119
throw new Error('Object has been destroyed.');
120
}
121
if (_enabled) {
122
return;
123
}
124
_enabled = true;
125
_container.addEventListener('scroll', signal, false);
126
signal();
127
};
128
this.disable = function() {
129
if (_destroyed) {
130
throw new Error('Object has been destroyed.');
131
}
132
if (_enabled) {
133
_enabled = false;
134
_container.removeEventListener('scroll', signal, false);
135
}
136
};
137
this.destroy = function() {
138
if (_destroyed) {
139
return;
140
}
141
_destroyed = true;
142
_enabled = false;
143
_container.removeEventListener('scroll', signal, false);
144
};
145
}
146
147
</script>
148
</head>
149
<body>
150
<div id="content"></div>
151
<div id="barrier"></div>
152
<script>
153
154
// Helper utils:
155
156
function fetchData(requestUrl, onSuccess, onError) {
157
var xhr = new XMLHttpRequest();
158
xhr.onreadystatechange = function () {
159
if (xhr.readyState == XMLHttpRequest.DONE) {
160
if (xhr.status == 200) {
161
var data = JSON.parse(xhr.responseText);
162
onSuccess(data);
163
} else {
164
onError('Loading error.')
165
}
166
}
167
};
168
xhr.open('GET', requestUrl, true);
169
xhr.send(null);
170
}
171
172
173
// Usage example:
174
175
var PAGE_SIZE = 20;
176
var SPACE_SIZE = 100; // less than 100px scrolled to barrier causes data fetching
177
178
var content = document.querySelector('#content');
179
var barrier = document.querySelector('#barrier');
180
181
barrier.addEventListener('click', function() {
182
scroll.signal();
183
});
184
185
var counter = 0;
186
187
function updateBarrier(messsage) {
188
barrier.textContent = messsage;
189
}
190
191
function removeBarrier() {
192
var parent = barrier.parentNode;
193
if (parent) {
194
parent.removeChild(barrier);
195
}
196
}
197
198
function appendItems(snippets) {
199
var html = '';
200
for (var i = 0; i < snippets.length; ++i) {
201
var snippet = snippets[i];
202
html += (++counter) + '. ' + snippet.name + '<br />';
203
}
204
content.innerHTML += html;
205
}
206
207
var scroll = new InfinityScroll({
208
container: window,
209
content: content,
210
barrier: barrier,
211
space: SPACE_SIZE,
212
loader: function(offset, callback) {
213
var pageNumber = offset + 1;
214
var requestUrl = 'https://dirask.com/api/snippets?pageNumber=' + pageNumber + '&pageSize=' + PAGE_SIZE + '&dataOrder=newest&dataGroup=batches';
215
var onSuccess = function(data) {
216
callback(pageNumber, data.batches, null);
217
if (data.pageNumber === data.pagesCount) {
218
callback(pageNumber, [], null);
219
}
220
};
221
var onError = function(error) {
222
callback(null, null, 'Loading error.')
223
};
224
fetchData(requestUrl, onSuccess, onError);
225
},
226
onLoading: function() {
227
updateBarrier('Loading...');
228
},
229
onLoaded: function(snippets) {
230
if (snippets.length > 0) {
231
appendItems(snippets);
232
}
233
if (snippets.length < PAGE_SIZE) {
234
scroll.disable();
235
removeBarrier();
236
} else {
237
updateBarrier('Click me to retry loading...');
238
}
239
},
240
onError: function(error) {
241
updateBarrier('Loading error! (click me to retry)');
242
}
243
});
244
245
scroll.enable();
246
247
</script>
248
</body>
249
</html>