14

I have a form which is submitted remotely when the various elements change. On a search field in particular I'm using a keyup to detect when the text in the field changes. The problem with this is that when someone types "chicken" then the form is submitted seven times, with only the last one counting.

What would be better is something like this

  • keyup detected - start waiting (for one second)

  • another keyup detected - restart waiting time

  • waiting finishes - get value and submit form

before I go off and code my own version of this (I'm really a backend guy with only a little js, I use jQuery for everything), is there already an existing solution to this? It seems like it would be a common requirement. A jQuery plugin maybe? If not, what's the simplest and best way to code this?

UPDATE - current code added for Dan (below)

Dan - this may be relevant. One of the jQuery plugins I'm using on the page (tablesorter) requires this file - "tablesorter/jquery-latest.js", which, if included, leads to the same error with your code as before:

jQuery("input#search").data("timeout", null) is undefined http‍://192.168.0.234/javascripts/main.js?1264084467 Line 11

Maybe there's some sort of conflict between different jQuery definitions? (or something)

$(document).ready(function() {
  //initiate the shadowbox player
//  Shadowbox.init({
//    players:  ['html', 'iframe']
//  });
}); 

jQuery(function(){
  jQuery('input#search')
    .data('timeout', null)
    .keyup(function(){
      jQuery(this).data('timeout', setTimeout(function(){
          var mytext = jQuery('input#search').val();
          submitQuizForm();
          jQuery('input#search').next().html(mytext);
        }, 2000)
     )
     .keydown(function(){
       clearTimeout(jQuery(this).data('timeout'));
     });
    });
});

function submitQuizForm(){
  form = jQuery("#searchQuizzes");
  jQuery.ajax({
    async:true, 
    data:jQuery.param(form.serializeArray()), 
    dataType:'script', 
    type:'get', 
    url:'/millionaire/millionaire_quizzes',
    success: function(msg){ 
     // $("#chooseQuizMainTable").trigger("update"); 
    }
  }); 
  return true;
}
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Max Williams
  • 32,435
  • 31
  • 130
  • 197

5 Answers5

24

Sorry i haven't tested this and it's a bit off the top of my head, but something along these lines should hopefully do the trick. Change the 2000 to however many milliseconds you need between server posts

<input type="text" id="mytextbox" style="border: 1px solid" />
<span></span>

<script language="javascript" type="text/javascript">
    jQuery(function(){
      jQuery('#mytextbox')
        .data('timeout', null)
        .keyup(function(){
            clearTimeout(jQuery(this).data('timeout'));
            jQuery(this).data('timeout', setTimeout(submitQuizForm, 2000));
        });
    });
</script>
dan richardson
  • 3,871
  • 4
  • 31
  • 38
  • inside the setTimeout function you MIGHT be able to change jQuery('#myelement') to jQuery(this) – dan richardson Jan 20 '10 at 13:15
  • Thanks dan - it's falling over at the first .data('timeout', null), saying it's not a function. Do i have to do something else before i can call .data? I'm calling it on a text field in this instance. – Max Williams Jan 20 '10 at 17:26
  • Max, have you made sure you have included the jQuery library and changed #myelement to your own element selector? I have just given it a quick test and it's working spot on :) Dan – dan richardson Jan 21 '10 at 10:03
  • Also you will want to make sure you have it in a jQuery load function. My orignal answer has been updated with the full code. Dan – dan richardson Jan 21 '10 at 10:08
  • ah, that's working great now, thanks. I was calling your previous entry from inside another function, before. I've not encountered jquery load functions before, like i say i'm a bit of a js novice. thanks again! max – Max Williams Jan 21 '10 at 10:57
  • Actually dan, i spoke too soon. I think the logic is a bit messed up. I replaced the comment (// Run some more logic - ajax post/get?) with a call to another function which submits the form remotely. And now, when typing 'mathematics', fairly quickly, into the empty search box, i see 12 different calls. Looking in my server log i can see the calls, which have a sequence of terms from the search box - "mathema", "mathemat", "mathemat", "mathematic", "mathematic", "mathematics", "mathematics", "mathematics", "mathematics", "mathematics", "mathematics", "mathematics" – Max Williams Jan 21 '10 at 11:53
  • Can you update your question with your current code? Just paste it into the textarea (in your question), select the code and hit the code button to auto format it all. :) Dan – dan richardson Jan 21 '10 at 12:59
  • Hmm, i think you might be able to remove the jQuery-latest.js, as you are probably including the jQuery library elsewhere on the page? If it's literally just including the library again you can remove it. Though in your submitQuizForm function i would rename "form" to something like "quizform", as form might be picked up as a reserved word in javascript. – dan richardson Jan 21 '10 at 16:07
  • Hi Dan. Removed that call to jquery-latest and replaced the form varname with quizForm but the essential problem remains of it making 12 calls when i type in 'mathematics' into the empty search box. I guess that the timeout isn't being reset by the keydowns for some reason? – Max Williams Jan 21 '10 at 17:41
  • Doh! I have sorted it for you :) All you need to do is remove the ".keydown(function(){})" function, and place the "clearTimeout(jQuery(this).data('timeout'));" inside of the keyup function as the first thing, i will edit my post :) Dan – dan richardson Jan 22 '10 at 12:13
  • Thanks dan. I actually ended up rewriting it to do just that and it seems to work pretty well. I factored it into a method - see my answer below. cheers, max – Max Williams Jan 22 '10 at 17:33
7

Here's your fancy jquery extension:

(function($){

$.widget("ui.onDelayedKeyup", {

    _init : function() {
        var self = this;
        $(this.element).keyup(function() {
            if(typeof(window['inputTimeout']) != "undefined"){
                window.clearTimeout(inputTimeout);
            }  
            var handler = self.options.handler;
            window['inputTimeout'] = window.setTimeout(function() {
                handler.call(self.element) }, self.options.delay);
        });
    },
    options: {
        handler: $.noop(),
        delay: 500
    }

});
})(jQuery);

Use it like so:

    $("input.filterField").onDelayedKeyup({
        handler: function() {
            if ($.trim($(this).val()).length > 0) {
                //reload my data store using the filter string.
            }
        }
    });

Does a half-second delay by default.

Sam Dutton
  • 14,775
  • 6
  • 54
  • 64
Steven Francolla
  • 377
  • 6
  • 14
  • I'd trim down the use to (can't seem to get the formatting right though?): `var trimmedVal = $.trim($(this).val()); if (trimmedVal.length > 0) { //reload my data store using the filter string. }` – Johny Skovdal Jan 11 '12 at 08:54
4

As an update, i ended up with this which seems to work well:

function afterDelayedKeyup(selector, action, delay){
  jQuery(selector).keyup(function(){
    if(typeof(window['inputTimeout']) != "undefined"){
      clearTimeout(inputTimeout);
    }  
    inputTimeout = setTimeout(action, delay);
  });
}

I then call this from the page in question's document.ready block with

  afterDelayedKeyup('input#search',"submitQuizForm()",500)

What would be nice would be to make a new jquery event which uses this logic, eg .delayedKeyup to go alongside .keyup, so i could just say something like this for an individual page's document.ready block.

  jQuery('input#search').delayedKeyup(function(){
    submitQuizForm();
  });

But, i don't know how to customise jquery in this way. That's a nice homework task though.

Max Williams
  • 32,435
  • 31
  • 130
  • 197
  • Hey Max, good the idea about clearing the timeout using the typeof without having to explicitly create the variable globally :) – will824 Jan 22 '13 at 17:20
3

Nice job, Max, that was very helpful to me! I've made a slight improvement to your function by making it more general:

function afterDelayedEvent(eventtype, selector, action, delay) {
    $(selector).bind(eventtype, function() {
        if (typeof(window['inputTimeout']) != "undefined") {
            clearTimeout(inputTimeout);
        }
        inputTimeout = setTimeout(action, delay);
    });
}

This way you can use it for any type of event, although keyup is probably the most useful here.

Nicolas Connault
  • 464
  • 6
  • 12
0

I know this is old, but it was one of the first results when I was searching for how to do something like this so I though I would share my solution. I used a combination of the provided answers to get what I needed out of it.

I wanted a custom event that worked just like the existing jQuery events, and it needed to work with keypress + delete, backspace and enter.

Here's my jQuery plugin:

$.fn.typePause = function (dataObject, eventFunc)
    {
        if(typeof dataObject === 'function')
        {
            eventFunc = dataObject;
            dataObject = {};
        }
        if(typeof dataObject.milliseconds === 'undefined')
            dataObject.milliseconds = 500;
        $(this).data('timeout', null)
            .keypress(dataObject, function(e)
            {
                clearTimeout($(this).data('timeout'));
                $(this).data('timeout', setTimeout($.proxy(eventFunc, this, e), dataObject.milliseconds));
            })
            .keyup(dataObject, function(e)
            {
                var code = (e.keyCode ? e.keyCode : e.which);
                if(code == 8 || code == 46 || code == 13)
                    $(this).triggerHandler('keypress',dataObject);
            });
    }

I used $.proxy() to preserve the context in the event, though there could be a better way to do this, performance-wise.

To use this plugin, just do:

$('#myElement').typePause(function(e){ /* do stuff */ });

or

$('#myElement').typePause({milliseconds: 500, [other data to pass to event]},function(e){ /* do stuff */ });    
Kyro
  • 627
  • 2
  • 6
  • 14