Read more

Events triggered by jQuery cannot be observed by native event listeners

Henning Koch
May 24, 2019Software engineer at makandra GmbH

jQuery has a function $.fn.trigger(). You can use it to dispatch an event on a jQuery object:

let $element = $('.foo')
$element.trigger('change')
Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

A caveat is that such an event will be received by jQuery event listeners, but not by native event listeners:

let $element = $('.foo')

$element.on('change', event => console.log('I will be called'))
$element[0].addEventListener('change', event => console.log("I WON'T be called"))

$element.trigger('change')

This is not an issue when your entire app is written in jQuery. But once you mix jQuery and non-jQuery code, events from the jQuery code will not be observable by non-jQuery code.

This is currently a WONTFIX for jQuery (see closed issues #2476 Show archive.org snapshot , #3347 Show archive.org snapshot ).

Native events always work for everyone

Note that if you trigger a native event instead, it will be received both by jQuery event listeners ($.fn.on()) and native event listeners (Element#addEventListener()):

let $element = $('.foo')

$element.on('change', event => console.log('I will be called'))
$element[0].addEventListener('change', event => console.log('I will also be called'))

let event = new CustomEvent('change')
$element[0].dispatchEvent(event)

Is this a problem for my code?

This is an issue if and only if:

  1. Your app has native event listeners registered using Element#addEventListener().
  2. Your app triggers event listeners using jQuery's trigger(). Note that you might use third-party libraries like select2 that use trigger().

If your entire app is written in jQuery and you don't plan to use native libraries (like modern Unpoly Show archive.org snapshot versions), this does not affect you.

Fixing your own code

If possible, change your code so it doesn't use jQuery's trigger() function. Instead trigger native events using Element#dispatchEvent() Show archive.org snapshot or Unpoly's up.emit() Show archive.org snapshot .

To simplify this refactoring, here is a jQuery function triggerNative() that you can use as a drop-in replacement for trigger() (requires Unpoly):

jQuery.fn.triggerNative = function(...args) {
  this.each(function() {
    up.emit(this, ...args)
  })
  return this
}

With this you can replace a call like $element.trigger('change') with $element.triggerNative('change'). You may also pass event properties as a second argument.

Fixing library code

jQuery plugins like select2 or bootstrap-datepicker use trigger('change') to emit change events. These events are not observable by native listeners.

This case is a little harder to fix since we do not want to change library code.

The function can be applied to a jQuery element known to emit pure jQuery events. If it observes a pure jQuery event without an underlying native event, the jQuery event is stopped from propagation and bubbling. A native event is then emitted on the same element:

jQuery.fn.enableNativeEvent = function(eventName, convertedPropNames = []) {
  this.on(eventName, function($event) {
    // Only convert when there is not already a native event
    if (!$event.originalEvent) {
      let eventProps = up.util.only($event, ...convertedPropNames)
      up.emit($event.target, eventName, eventProps)
      up.event.halt($event)
    }

  })

  return this
}

Here is an example usage:

$element.select2().enableNativeEvent('change')

Make sure this function is applied before any other event listeners are registered to the given jQuery element, or events might be received twice.

The native event is emitted without additional properties outlining event details. If you need properties to be copied from the jQuery event to the native event, pass an array of property names as a second argument:

$element.enableNativeEvent('mousemove', ['clientX', 'clientY'])
Henning Koch
May 24, 2019Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2019-05-24 14:52)