lucisferre

“There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult. —Sir Charles Antony Richard Hoare”

jQuery Plugin for Really Tracking Text Input Changes

Permalink

It may come as a bit of surprise to some, but the change event on HTML text inputs does not actually get triggered when the text changes. It is only triggers after focus is lost (on the ‘blur’ event). This isn’t always a problem but it comes up often enough for me to get fed up and create a jQuery plugin for this.

This is the first real jQuery plugin I’ve really had the need to write. I’ve generally always found what I’ve needed by googling for it, or at least found something close enough to be easily modified for my purpose.

If anyone has suggestions for ways to improve this, or sees any pitfalls I’ve missed let me know, or, you know, just edit the Gist. On the other hand, if anyone wants to suggest it should be written in javascript you can go take a long walk of a short pier.

Gist: 3191297 View Gist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# $ contentChanged
# By: Chris Nicola
#
# Provides an attachable 'contentChanged' event for text inputs and allows an
# event to be fired whenever changed content is detected. For convenience the
# event incldues properties for `previous`, `current` and `hasContent`.
#

$.fn.extend
  contentChanged: (options) ->
    settings =
      pollPeriod: 500
    settings = $.extend settings, options

    checkForContentChanged = (target, stop) ->
      $this = $(target)
      previous = $this.data('lastValue')
      current = $this.val()
      $this.data('lastValue', current)
      e = $.Event('contentChanged', {
        previous: previous
        current: current
        hasContent: (current and current isnt '')
      })
      $this.trigger(e) if previous isnt current
      if not stop
        $this.data('pollTimeout', setTimeout((() => checkForContentChanged(target)), settings.pollPeriod))

    @each () ->
      if this.nodeName isnt 'INPUT' and this.nodeName isnt 'TEXTAREA'
        $.error 'contentChanged only works for input elements'

      $this = $(@)
      $this.data('content-changed', true)
      current = $this.val()
      $this.data('lastValue', current)

      onFocus = () =>
        pollTimeout = $this.data('pollTimeout')
        clearTimeout(pollTimeout) if pollTimeout
        $this.data('pollTimeout', setTimeout((() => checkForContentChanged(@)), settings.pollPeriod))

      onFocus() if $this.is(':focus')
      $this.focus onFocus

      $this.blur () ->
        pollTimeout = $(@).data('pollTimeout')
        clearTimeout(pollTimeout) if pollTimeout
        checkForContentChanged(@, true)

$ () ->
  # If the browser supports the 'input' event use that, otherwise we fallback
  # to polling for changes.
  if 'oninput' of document.body
    $('body').on 'input', (e) ->
      $this = $(e.target)
      previous = $this.data('lastValue')
      current = $this.val()
      $this.data('lastValue', current)
      e = $.Event('contentChanged', {
        previous: previous
        current: current
        hasContent: (current and current isnt '')
      })
      $this.trigger(e)
  else
    $('body').on 'focus.content-changed', 'input, textarea', (e) ->
      return if @nodeName isnt 'INPUT' and @nodeName isnt 'TEXTAREA'
      $this = $(@)
      $this.contentChanged() unless $this.data('content-changed')

It’s really pretty simple but unfortunately we are forced into using polling to detect the change. Fortunately, we only poll inputs we have explicitly attached the plugin to and we only poll while it is focused. So we don’t need to constantly poll. The default polling period is 500ms but you can set it to whatever you like. The contentChanged event also has some custom convenience properties like current, previous and hasContent.

So what would you use this for? Well detecting when an input actually has some content, or triggering a spellcheck or autocorrection or really just anything that you want to execute when the content of a textarea or text input has changed. For example if we wanted to disable a textarea when it has no content we could do this:

1
2
3
4
5
6
$('#my-textarea').contentChanged();
$('#my-textarea').on 'contentChanged', (e)->
  if e.hasContent
    this.removeAttr('disabled')
  else
    this.attr('disabled', 'disabled')

Edit: So it looks like there is a way (at least with most modern browsers) to track input changes using the HTML5 input event. The docs are available here on MDN though it is unlcear which browser versions support it. It was also surprisingly hard to find info on this event too.

I also found a crossbrowser jQuery library that uses this event as well providing some legacy fallback support. So in the end this appears to have been an entirely academic excercise in creating a jQuery plugin. Oh well…

Second Edit The crossbrowser plugin above does not seem to work well when binding further up the DOM to the event, something that I have to do to make things work with dynamically loaded content from PJAX requests (I’m writing another blog post about why I do this).

So it’s back to polling for changes, but I’ve decided to make a small change to the plugin above. I’ve just edited the GIST so you should already be seeing it (look at the GIST if you want to see the older version).

We now automatically attach to any focused textbox/input and begin polling. Polling stops as soon as blur is fired. As an added enhancement we catch don’t do this if the browser supports the input event correctly. This makes the ‘contentChanged’ event completely unobtrusive and it just works.

Comments