User:JDrewniak (WMF)/notes/MF InfiniteScroller architecture
Mobilefrontend Gateway/component/infiniteScroller Architecture
editThe interaction between gateways, components, and the infiniteScoller component is a bit fuzzy in mobilefrontend. Gateways fetch the data and pass it to a component. The infiniteScroller extends the component and adds a custom event that triggers the components load method. The scroller then disables itself after the event is triggered, and the component has to re-enable the event. The component also has to manage the loading spinner.
Responsibilites here are mixed. The scroller disables itself but is re-enabled by an external component. With modifications to the gateways and InfiniteScroller instantiation, the scroller could enable/disable itself, as well as manage its own loading spinner.
How?
editPromises all the way down.
The main technique for separating these responsibilities is to build a promise chain across all three components. The chain starts with the gateway. The gateway creates a function that returns a promise. The component takes that promise and adds a then
to handle its own logic. The component then passes that chain to the InfiniteScroller, which adds it’s own then
to handle it’s logic. The infiniteScroller then executes the whole chain when necessary.
Gateway Component InfiniteScroller fetch = promise.then( res ) --> getContent = fetch.then( do stuff ) --> gotContent = getConent.then( )
"Snowball" architecture.
Starting with the gateway
editThe gateways main “fetch” method always returns a promise.
function Gateway() {
// returning only this object
const pub = {};
// some private methods
function processApiData( pages ) {
return Object.keys( pages ).map( page => {
return pages[page].imageinfo[0].thumburl
} )
}
// main fetch method, always returns a promise.
pub.fetch = function() {
return fetch( API_URL )
.then( response => response.json() )
.then( json => {
return {
meta: { continue: true },
content: processApiData( json.query.pages )
}
} )
}
// exposing public methods
return pub;
}
The infinite scroller
editThe infinite scroller is is instantiated with a “fetch” method and a DOM element.
function InfiniteScroller( contentLoaded, scrollEl = document.body ) {
// creating a spinner
const spinner = document.createElement("div"),
pub = {};
spinner.className = "spinner hidden";
scrollEl.appendChild( spinner )
// private methods
function scrolledToBottom( ev ) {
var el = ev.target;
if (el.scrollHeight - el.scrollTop === el.clientHeight)
{
public.triggerLoad();
}
}
function enable() {
scrollEl.addEventListener( 'scroll', scrolledToBottom )
}
function disable() {
scrollEl.removeEventListener( 'click', scrolledToBottom )
}
function toggleSpinner() {
spinner.classList.toggle('hidden');
}
// public method, adds it's own logic to the promise chain.
pub.triggerLoad = function() {
disable();
toggleSpinner();
return contentLoaded()
.then( reEnable => {
toggleSpinner();
return ( reEnable ) ? enable() : false;
} )
}
// enable on instatiation
enable();
// expose the public methods
return pub;
}
The component
editThe component combines the gateway and the infiniteScroller. The component has a wrapper function around the gateway “fetch”. The wrapper handles the gateway response, and returns the original promise. This wrapper is handed to the infiniteScroller (named contentLoaded
above), and the infiniteScoller takes care of executing that function when necessary. Because the wrapper returns the original promise, the infiniteScroller can attach a then
to that promise to handle it’s own enabling & disabling and loading spinner.
infinite scroller component architecture
function Component( id ) {
// defining a gateway and scroller for this instance.
const gateway = new Gateway();
const el = document.getElementById( id );
const scroller = new InfiniteScroller( contentLoaded, el );
// private methods
function renderStuff( content ){
content.forEach( url => {
const img = new Image();
img.src = url;
el.querySelector('.dynamic-content').appendChild( img )
} )
}
// adding logic to the promise chain
function contentLoaded() {
return gateway
.fetch()
.then( payload => {
const content = payload.content;
const meta = payload.meta;
renderStuff( content )
return meta.canContinue;
} )
}
scroller.triggerLoad();
}