If have implemented a nice little template to render lazy lists as proof of concept. It uses the IntersectionObserver
api and I would like to show you how it works.
Consider you have some list of data, in this case some array with thousand entries, where each entry is 0,1,2,3,…,999:
main.js
Template.body.onCreated(function () {
let allElements = []
allElements.length = 1000
allElements.fill(0)
allElements = allElements.map((val, index) => index)
this.allElements = new ReactiveVar(allElements)
})
Now you want to render this “lazy”, say in 50 element steps, depending on how far the end of the current list is close to be visible on the viewport. You pass this data to a template, let’s say lazyList
:
main.html
{{#lazyList data=allElements step=50}}
<h1>Entry {{this}}</h1> {{!-- "this" refers to the entry at the current index --}}
{{/lazyList}}
Getting the allElements
from a simple helper:
main.js
Template.body.helpers({
allElements () {
return Template.instance().allElements.get()
}
})
So our list should look like this in the end:
Entry 0
Entry 1
Entry 2
Entry 3
Entry 4
Entry 5
Entry 6
Up to 50 elements.
In order to achieve that our lazyList
tempate needs to render the list of elements and pass to the content block the current element:
lazyList.html
<template name="lazyList">
{{#each element in elements}}
<div class="lazy-list-element" data-index="{{@index}}">
{{> Template.contentBlock element }} {{!-- pass the data back to the h1 html content --}}
</div>
{{/each}}
{{#if loadComplete}}
<div class="lazy-list-end"></div>
{{/if}}
</template>
At the end of the list is en empty div
with class name lazy-list-end
. It will be our indicator to render the next 50 elements. The following code contains all important hints as comments:
lazyList.js
import { ReactiveDict } from 'meteor/reactive-dict'
import './lazyList.html'
Template.lazyList.onCreated(function () {
const instance = this
instance.state = new ReactiveDict()
instance.state.set('length', instance.data.step)
instance.autorun(() => {
// we fill a shallow copy of the array up to the max length
// and use it as data source for our list rendering
// this part will automactically re-activate when the
// length value has been extended in our handleIntersect function
const currentStep = instance.state.get('length')
const elements = []
elements.length = currentStep
// copy elements / references
for (let i = 0; i < currentStep; i++) {
elements[i] = instance.data.data[i]
}
// we use loadComplete to delay the rendering
// of the end element, so it won't appear before
// we have copied our list
instance.state.set({ elements, loadComplete: true })
})
})
Template.lazyList.helpers({
elements () {
return Template.instance().state.get('elements')
},
loadComplete () {
return Template.instance().state.get('loadComplete')
}
})
Template.lazyList.onRendered(function () {
const instance = this
const options = {
root: null,
rootMargin: '0px',
threshold: 1.0
}
const observer = new IntersectionObserver(function handleIntersect (entries, observer) {
entries.forEach((entry) => {
// once the end element intersects with the viewport
// we can extend the current length by the value of step
// which is on our case 50
if (entry.isIntersecting) {
const { step } = instance.data
const length = instance.state.get('length')
instance.state.set('length', length + step)
}
})
}, options)
observer.observe(document.querySelector('.lazy-list-end'))
})
Now it should add 50 more elements each time you reach the end of the list. Please let me know if there are any issues with the code.
If you like to see this in a package in atmosphere please leave a I think this has the potential for a generic lazy-render package
Edit: I forgot to mention, that this is just an example for a fixed size list but can easily be changed for lists, where the next data is fetched from the sever on end.