Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing organic list browsing using arrow keys and intelligent lazy loading

Simple Scenario

I have a list, i implemented browsing with the arrow keys (up, down) and on each change of the current list item, a database object gets loaded via AJAX.

Sweet.

The Problem

When a user browses through the list very fast, i don't want every request to go off. But the original request should go off instantly, of course.

My idea was to set a timeout using a variable as delay, and after the initial load of an item, increase that variable.

That works, but when the user stops browsing for a short moment, but then continues, i still don't want every request to go off.

So i figured, the delay variable must be reasonably increased with every browsing event, until a threshold is reached.

This organic approach would successfully minimize the amount of unnecessary loading of items.

My Solution

I came far. This piece of code (explanations below) will do the job, with one major culprit:

After the first time browsing is done and then stopped, the delay will automatically stay at a (2nd step) minimum value of 150ms.

Of course i tried to fix this, but as you will see, this is an interesting yet probably fairly common logical problem – and i think my overall approach is wrong.

But i don't see how. Brain does not compute. Computer says no.

Code

You can either sift through my given example or go here for a fully functional simulator in jsFiddle.

If you choose the jsFiddle:

Click the button, the item load appears immediately. Now wait a bit, click the button again, the initial load will be delayed. If you continuously press the button quickly, the item load will appear only when you're finished with your click-spree.

Code Sample

We're inside an object literal, just so you know.

_clickTimer: false,                     // holds what setTimeout() returns
_timerInc: 0,                           // the your timer delay are belong to us

/**
 * Function is triggered whenever the user hits an arrow key
 * itemRef is the passed list item object (table row, in this case)
 */

triggerItemClick: function(itemRef){

    var that=this;
    var itemId=$(itemRef).data('id');   // Get the item id

    if(this._clickTimer){               // If a timeout is waiting

        clearTimeout(this._clickTimer); // we clear it
        this._itemClickTimer=false;     // and reset the variable to false

        /**
         * Note that we only arrive here after the first call
         * because this._clickTimer will be false on first run
         */

        if(this._timerInc == 0){        // If our timer is zero
            this._timerInc = 150;       // we set it to 150
        } else {                        // otherwise
            if(this._timerInc <= 350)   // we check if it is lower than 350 (this is our threshold)
                this._timerInc += 15;   // and if so, we increase in steps of 15
        }

    } 

    /**
     * Regardless of any timing issues, we always want the list
     * to respond to browsing (even if we're not loading an item.
     */

    this.toggleListItem(itemId);

    /**
     * Here we now set the timeout and assign it to this._clickTimer
     */

    this._clickTimer=setTimeout(function(){

        // we now perform the actual loading of the item
        that.selectItem(itemId);

        // and we reset our delay to zero
        that._timerInc=0;

    }, this._timerInc); // we use the delay for setTimeout()

}

Explanation

On first call: _clickTimer is false, _timerInc is 0, so the first call will lead to a delay of 0 for setTimeout() and _clickTimer will be set. The item will be instantly loaded.

Second call - given that our timeout is still waiting to be triggered, the _clickTimer gets cleared, the delay set to 150 if 0 or increased by 15 if lower than 350 (threshold).

This works great if you keep browsing. The timer increases, the load only gets fired once you stop browsing for a good moment.

But after you stopped, the next time you continue, _clickTimer will not be false (because setTimeout() assigns a counter to it), so in turn _timerInc will be set to 150 right away. So the first browse will lead to a delay of 150ms before anything gets loaded.

Call me crazy or fastidious, but the goal is to not have this delay.

Of course you'll say: simple, set _clickTimer to false at the end of the setTimeout() closure, so it gets reset once browsing is done and an item gets loaded. Great, but this results in the delay never going above 0ms. Think it through, you'll see.

I hope this was explained properly and that someone's brain is more capable of finding a solution to this than mine.

like image 962
SquareCat Avatar asked Mar 03 '17 07:03

SquareCat


1 Answers

It is probably possible to do this in a very sophisticated manner using Promises. Since that is mostly sugar coating, I however thought that it must be possible to fix this directly, and I think I did.

Updated fiddle. I added the delay in text, so it was easier for me to debug stuff, and also did some minor tidying up, but my actual changes are very small. This is detailed below.

Your remark near the end was my first intuition:

Of course you'll say: simple, set _clickTimer to false at the end of the setTimeout() closure, so it gets reset once browsing is done and an item gets loaded. Great, but this results in the delay never going above 0ms.

Indeed, this would make it so that the delay would never get above 0, because, well, we cannot click that fast (or browse that fast, in your actual application). But... what if we only reset if the delay was not 0? So, if the timeout goes off, but it went off after only 0 milliseconds, we remember that there was a timeout. If it went off later than that, then an actual pause in the browsing must have occurred. This is implemented easily, by adding a couple of lines in the timeout callback as follows.

this._clickTimer = setTimeout(function() {

  // we now perform the actual loading of the item
  that.selectItem();

  // and we reset our delay to zero
  if (that._timerInc > 0) {
    that._clickTimer = false;
  }
  that._timerInc = 0;

}, this._timerInc); // we use the delay for setTimeout()

It seems to work exactly as you want it to, except that now, the delay will be 0ms, then 150ms, then 0ms, et cetera, if you wait between clicks long enough. This can be addressed by adding an extra timeout in case the delay is 0ms that will still reset the delay. Whenever a trigger happens (click in the demo, browsing in your app), this timeout is cancelled.

That together makes everything work the way you want it to, I believe. For completeness, I also include the above mentioned fiddle as a snippet here.

var _simulator = {

  _clickTimer: false, // holds what setTimeout() returns
  _cancelClickTimer: false,
  _timerInc: 0, // the your timer delay are belong to us

  /**
   * Function is triggered whenever the user hits an arrow key
   * itemRef is the passed list item object (table row, in this case)
   */
  triggerItemClick: function() {

    var that = this;
    
    // always cancel resetting the timing, it can never hurt
    clearTimeout(that._cancelClickTimer);
    that._cancelClickTimer = false;

    if (this._clickTimer) { // If a timeout is waiting
      clearTimeout(this._clickTimer); // we clear it
      this._clickTimer = false; // and reset the variable to false

      /**
       * Note that we only arrive here after the first call
       * because this._clickTimer will be false on first run
       */
      if (this._timerInc == 0) { // If our timer is zero
        this._timerInc = 150; // we set it to 150
      } else { // otherwise
        if (this._timerInc <= 350) // we check if it is lower than 350 (this is our threshold)
          this._timerInc += 15; // and if so, we increase in steps of 15
      }
    }

    /**
     * Regardless of any timing issues, we always want the list
     * to respond to browsing (even if we're not loading an item.
     */
    this.toggleListItem();

    /**
     * Here we now set the timeout and assign it to this._clickTimer
     */
    this._clickTimer = setTimeout(function() {

      // we now perform the actual loading of the item
      that.selectItem();

      // and we reset our delay to zero
      if (that._timerInc > 0) {
      	that._clickTimer = false;
      } else {
      	that._cancelClickTimer = setTimeout(function() {
        	that._clickTimer = false;
        }, 150);
      }
      that._timerInc = 0;

    }, this._timerInc); // we use the delay for setTimeout()

  },

  /** the following functions are irrelevant for the problemsolving above **/

  toggleListItem: function() {
    $('#status').prepend($('<div />').text('You toggled a list item ... in ' + this._timerInc + ' ms'));
  },

  selectItem: function(id) {
    $('#loader').show();
    setTimeout(function() {
      $('#loader').hide();
    }, 800);
  }

};

$('#clickZone').on('click', function() {
  _simulator.triggerItemClick();
});
#clickZone {
  background: #369;
  color: #fff;
  width: 420px;
  height: 80px;
  text-align: center;
  line-height: 80px;
  cursor: pointer;
  -ms-user-select: none;
  -moz-user-select: -moz-none;
  -webkit-user-select: none;
  user-select: none;
  font-family: Arial;
}

#status {
  line-height: 20px;
  margin-top: 10px;
  font-family: Arial;
  font-size: 12px;
  background: #936;
  color: #fff;
  padding: 7px 10px;
}

#status > div {
  padding: 2px 0 4px;
  border-bottom: 1px dashed #ddd;
}

#status > div:last-child {
  border-bottom: 0;
}

#loader,
#notice {
  display: none;
  margin-top: 10px;
  width: 320px;
  padding: 10px 15px;
  background: #ddd;
  font-family: Arial;
  font-size: 11px;
  text-align: center;
}

#notice {
  background: lightblue;
  font-size: 14px;
  color: #333;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="clickZone">
  CLICK ME TO SIMULATE LIST BROWSING
</div>
<div id="loader">
  &#10003; &nbsp;Browsing ended, loading item!
</div>
<div id="status">
  <div>
    Waiting for something to happen ...
  </div>
</div>
like image 67
Just a student Avatar answered Sep 22 '22 09:09

Just a student