User:JDrewniak (WMF)/notes/MF InfiniteScroller architecture

From mediawiki.org

Mobilefrontend Gateway/component/infiniteScroller Architecture[edit]

The 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?[edit]

Promises 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.

Link to demo[edit]

Starting with the gateway[edit]

The 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[edit]

The 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[edit]

The 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(); 
}