0

I have some code that fires a function on keyup inside an input field. To prevent this function from firing every literal keyup I set a timeout function of half a second so my code won't spam ajax requests.

But for some reason this timeout code isn't working.

This is what I had at first, which does work:

Form:

<form class="search-address">
  <input id="account_id" type="hidden" name="account_id" value="<?PHP echo $_SESSION['user']['id']; ?>">
  <input class="search-word" type="text" placeholder="Zoeken in adresboek...">
</form>

My trigger code:

$('.search-word').keyup(function(e) {
  clearTimeout($.data(this, 'timer'));
  if (e.keyCode == 13){
        search(true);
    }else{
        $(this).data('timer', setTimeout(search, 500));
    }
});

My function:

function search(force) {
  var zoekwaarde = $(".search-word").val();
    var account_id = $("#account_id").val();
    $.ajax({
     type:'post',
     url:"includes/searchaddress.php",
     data:({zoekwaarde: zoekwaarde, account_id: account_id}),
     success:function(data){
         $( "#adressenlist" ).show().empty().append( data );
         $( "#deleteresult" ).hide();
         $( "#adresresult" ).hide();
     }
 });
}

This works, but the problem is I have 2 forms with the same class of .search-word. So I tweaked my code a little like this:

$('.search-word').keyup(function(e) {
    searchword = $(this).val();
  clearTimeout($.data(this, 'timer'));
  if (e.keyCode == 13){
        search(true, searchword);
    }else{
        $(this).data('timer', setTimeout(search(true, searchword), 500));
    }
});

function search(force, searchword) {
  var zoekwaarde = searchword;
    var account_id = $("#account_id").val();
    $.ajax({
     type:'post',
     url:"includes/searchaddress.php",
     data:({zoekwaarde: zoekwaarde, account_id: account_id}),
     success:function(data){
         $( "#adressenlist" ).show().empty().append( data );
         $( "#deleteresult" ).hide();
         $( "#adresresult" ).hide();
     }
 });
}

I use searchword = $(this).val(); so I only post the value of $(this) input field, and not the last one in the dom tree. I thought this would work but this breaks my timeout function, it now triggers the search function instantly on every key press. What can I do to fix that?

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
twan
  • 2,450
  • 10
  • 32
  • 92
  • as my two cents, the handled event-type should not be 'keyup' but 'input' instead. – Peter Seliger Nov 02 '21 at 12:29
  • `setTimeout(search(true, searchword), 500)` will ***immediately call `search()`*** and assign its *return value* (`undefined`) to `setTimeout`. – Niet the Dark Absol Nov 02 '21 at 13:02
  • 1
    Does this answer your question? [How can I pass a parameter to a setTimeout() callback?](https://stackoverflow.com/questions/1190642/how-can-i-pass-a-parameter-to-a-settimeout-callback) – Heretic Monkey Nov 02 '21 at 13:06
  • @HereticMonkey Yes thank you! I fixed it with this answer: `setTimeout(yourFunctionReference, 4000, param1, param2, paramN);` – twan Nov 02 '21 at 14:19
  • Does this answer your question? [What is the JavaScript version of sleep()?](https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep) – Reporter Nov 02 '21 at 14:35

2 Answers2

1

The OP needs to create a throttled version of the event handler.

E.g. underscore and lodash both provide a throttle method.

function handleSearch(/*evt*/) {
  console.log('handleSearch :: this.value ...', this.value);
}
document
  .forms[0]
  .querySelector('[type="search"]')
  .addEventListener('input', handleSearch);

const elmSearch = document
  .forms[1]
  .querySelector('[type="search"]');

// - with underscore's `throttle` the handler's `event` argument
//   unfortunately gets lost/ommitted.
// - a viable workaround is to firstly bind the to be handled control
//   to `handleSearch` and then to create a throttled handler from it. 
elmSearch.addEventListener('input', _.throttle(handleSearch.bind(elmSearch), 500));
[type=search] { width: 17em; margin: 10px 0; }
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>

<form class="search-address">
  <input type="search" placeholder="immediate addressbook search ...">
</form>

<form class="search-address">
  <input type="search" placeholder="throttled addressbook search ...">
</form>

Edit

"So... they should completely rewrite their code, removing jQuery, but adding underscore? Seems a bit much for such a simple problem. – Heretic Monkey"

"@HereticMonkey ... no one prevents them from writing their own throttle abstraction which I would advise anyhow. And the example above is a use case boiled down to the most necessary, in order to demonstrate the differences. The OP has a 2k reputation. I presume the OP can cope with that. – Peter Seliger"

Due to the arguments of Heretic Monkey there is a custom throttle implementation which takes complexity off the OP's event handling scripts. The OP's code then can be refactored to something like the following example ...

function search(force, searchword) {
  var zoekwaarde = searchword;
  var account_id = $("#account_id").val();
  $.ajax({
   type: 'post',
   url: "includes/searchaddress.php",
   data: ({zoekwaarde: zoekwaarde, account_id: account_id}),
   success: function(data){
     $( "#adressenlist" ).show().empty().append( data );
     $( "#deleteresult" ).hide();
     $( "#adresresult" ).hide();
   }
 });
}

function handleSearch(evt) {
  const searchword = evt.currentTarget.value;

  // search(true, searchword)
  console.log(`search(true, ${ searchword })`);
}
$('.search-word').each((_, elmNode) => {
  // each element of cause needs its own
  // (unique) throttled handler version.
  $(elmNode).on('input', throttle(handleSearch, 500));
});
body { margin : 0; }
[type=search] { width: 17em; margin-bottom: 3px; }
.as-console-wrapper { min-height: 70%!important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<form class="search-address">
  <input type="hidden" name="account_id" id="account_id" value="sessionUserId" />
  <input type="search" placeholder="Zoeken in adresboek ..." class="search-word" />
</form>

<form class="search-another-address">
  <input type="hidden" name="account_id" id="account_id" value="sessionUserId" />
  <input type="search" placeholder="Zoeken in adresboek ..." class="search-word" />
</form>

<script>
  function isFunction(type) {
    return (
         (typeof type === 'function')
      && (typeof type.call === 'function')
      && (typeof type.apply === 'function')
    );
  }
  function getSanitizedTarget(target) {
    return target ?? null;  
  }

  function getSanitizedInteger(value) {
    return (Number.isSafeInteger(value = parseInt(value, 10)) && value) || 0;
  }
  function getSanitizedPositiveInteger(value) {
    return Math.max(getSanitizedInteger(value), 0);
  }

  function throttle(proceed, threshold, isSuppressTrailingCall, target) {
    if (!isFunction(proceed)) {
      return proceed;
    }
    target = getSanitizedTarget(target);
    threshold = getSanitizedPositiveInteger(threshold) || 200;

    isSuppressTrailingCall = !!isSuppressTrailingCall;

    let timeoutId, timeGap;
    let timestampRecent, timestampCurrent;
    let context;

    function trigger(...argsArray) {
      timestampRecent = timestampCurrent;

      proceed.apply(context, argsArray);
    }
    function throttled(...argsArray) {
      clearTimeout(timeoutId);

      context = target ?? getSanitizedTarget(this);
      timestampCurrent = Date.now();

      if (timestampRecent) {
        timeGap = (timestampCurrent - timestampRecent);

        if (isSuppressTrailingCall && (timeGap >= threshold)) {
          // trailing call will be suppressed.

          trigger(...argsArray);
        } else {
          timeoutId = setTimeout(trigger, Math.max((threshold - timeGap), 0), ...argsArray);
        }
      } else {
        // initial call.

        trigger(...argsArray);
      }
    }
    return throttled;
  }
</script>
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • So... they should completely rewrite their code, removing jQuery, but adding underscore? Seems a bit much for such a simple problem. – Heretic Monkey Nov 02 '21 at 13:09
  • @HereticMonkey ... no one prevents them from writing their own `throttle` abstraction which I would advise anyhow. And the example above is a use case boiled down to the most necessary, in order to demonstrate the differences. The OP has a 2k reputation. I presume the OP can cope with that. – Peter Seliger Nov 02 '21 at 14:25
0

Try wrapping your search function in an anonymous function (callback) like:

setTimeout(() => { search(param1, param2) }, 1500)
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
  • See the dupe (the question linked after my comment on the question after "Does this answer your question?") for other ways to do the same thing. – Heretic Monkey Nov 02 '21 at 13:07