select2: Standard (native) JS events are no longer emitted in v4

We’d like to see Select2 emit standard events targeting the original <select> element.

Specifically, we needed focus and keydown events for use in our implementation of a field navigation component that helps us define application-specific navigation paths between fields declaratively without using tab indices.

While we realize that Select2 emits custom events, including select2-focus, we don’t wish to special-case some code in our field navigation component just for Select2 (we use a few other types of controls). Instead, it’d be great if Select2 would emit the standard events from the original <select> element alongside its custom events, whenever a one-to-one mapping exists and when Select2 doesn’t have any counterpart event type.

List of DOM3 Event Types (W3C DOM Level 3) Standard Events (MDN Web Technology Reference) Select2 Events (Select2 Documentation)

Note that Select2 doesn’t emit any keyboard events targeting the original <select> element, so this makes our use-case impossible to implement in any kind of supported way.

For those who may have the same issue, we’ve successfully worked around it; although reading this code is not a healthy thing to do. I’ll post it below in the hope that:

  1. It helps in understanding the issue and finding an appropriate solution.
  2. Someone can think of a more straightforward workaround in the meantime (or point us toward something that would remove the need for any workaround).
  3. It helps someone else who may have difficulties working around the issue.

There are a few things specific to our codebase (jQuery, AMD) but it should be fairly understandable. We’re trying to make a habit of documenting hideous hacks properly, so I include the lengthy comment intended for internal documentation, which should be useful.

Thank you.

define(function (require) {
    var $ = require('jquery');
    var key = require('keymap');
    require('select2');

    /*
     * Here be dragons.
     *
     * As of November 2013, select2 doesn't seem to play nice with the
     * other kids.  More specifically, it doesn't make use of standard
     * events on the select element.
     *
     * Instead, it provides control-specific events prefixed with
     * `select2-' [1].  Since we don't want to special-case the
     * navigation setup code just for select2, we won't assign handlers
     * for these events.  Instead, we'll add handlers for standard
     * events on the select element, and trigger the corresponding
     * actions on the control.
     *
     * This works fine for the focus event.  Since all select2 controls
     * are assigned the select2-container-multi class, we can setup the
     * necessary thunks at once rather easily.
     *
     * Key press events shouldn't be a problem, since we're hooking
     * actions unrelated to select2 (refocusing on the next field).
     * Of course, there's no point in making it that easy.  When
     * converting a select element to select2, the original element
     * gets pushed off-screen using CSS trickery, and select2 uses
     * a mix of div, span, anchor and input elements to build the
     * dropdown.
     *
     * The end-result is that the original select element (to which we
     * assigned a keydown handler using the fieldnav module) isn't
     * emitting any keydown events at all.  The internal auto-generated
     * input element is.
     *
     * Thus, we must assign a handler for the keydown event directly to
     * that input element, and programmatically trigger the same event
     * on the original select so that the navigation handlers can pick
     * it up.  This is kind of the reverse operation from what we had
     * to do with the focus event.
     *
     * The input element is inside a select2-container div, so they are
     * just as easy to query at once.  However, the auto-generated
     * container and the original select element nodes don't have any
     * kind of parent-child relationship in the DOM.  Supposedly, the
     * container always precedes the original element directly, so one
     * solution is to trigger the keydown handler on the next element to
     * the parent of the input that received it (simply by forwarding
     * the event object).
     *
     * We remarked that if the original select element is attributed an
     * ID, select2 will reuse it for the ID of the container itself,
     * simply prefixing it with `s2id_'.  We thus decided to parse that
     * ID to retrieve the original one instead, and trigger the handler
     * that way, which seems a bit more solid.
     *
     * Everything should be fine now, except it isn't.  This works as
     * long as the dropdown is closed, but when it opens, the input
     * mentioned above isn't used anymore.  Instead, another input
     * element is used (this is the search field).
     *
     * This time however, the element doesn't seem to have any kind of
     * relationship to neither the container mentioned above nor the
     * original select element.  No constructed ID from the original
     * ID, no parent-child node relationship -- they're not even close
     * to one another.  Indeed, the search input is under a
     * select2-search div, itself under a select2-drop div that lives
     * under the body element.
     *
     * Worse (for us), these elements are not created or at least are
     * not attached to the DOM until the dropdown is opened for the
     * first time, which means we can't even assign them any handlers
     * when we attach our own view to the DOM.
     *
     * Instead of diving in a complex idea of trying to manage state in
     * parallel to select2 (monitoring open/close events and assigning
     * handlers *only once* when the elements are available), we
     * decided to assign a delegated handler to the body element.
     *
     * Since all select2 controls have their internal elements on the
     * body root, this works fine.  The filtering of events is a little
     * long, but it is usable.
     *
     * Currently, we assign that handler when the select2-nav module
     * is first loaded (which happens only one time, requirejs doesn't
     * unload modules).
     *
     * "Ideally", we would register each view module that uses the
     * select2-nav module and remove that handler when no module uses
     * it anymore (re-adding it subsequently as necessary).  Simple
     * reference counting would do the trick, but for a huge hack like
     * that, this is clearly overkill.  Hence, the handler now lives
     * on the body element indefinitely once it is assigned.
     *
     * The end?  Well, we only managed to assign a handler capable of
     * catching key press events for the controls when they are open,
     * but we still need to rewire these events to the original select
     * element.  As mentioned above, we have no way of doing that,
     * short of keeping state ourselves (idea excluded above) or diving
     * into select2's internals using unsupported interfaces (if it's
     * even possible).
     *
     * The thing is, we saw that pressing the tab key has the native
     * effect of closing the dropdown.  Hence, pressing it twice will
     * work as expected since the handler for the input used when the
     * control is closed can find its corresponding original select
     * element itself (without state) as explained above (using the
     * reconstructed ID).
     *
     * From a UX point of view, it's not that bad, pressing tab to
     * close has an effect of explicitly validating the selection
     * before we go to the next field by pressing tab again, which may
     * even be desirable (who knows what the UX experts would say).
     *
     *
     * Usage:
     *     var rewireSelect2Events = require('integration/select2-nav');
     *     rewireSelect2Events(view);
     *
     * [1] http://ivaynberg.github.io/select2/#events
     * [2] http://ivaynberg.github.io/select2/#programmatic
     */

    $('body').on('keydown',
        'div'
        + '.select2-drop'
        + '.select2-display-none'
        + '.select2-with-searchbox'
        + '.select2-drop-active'
            + '> div.select2-search'
                + '> input.select2-input',
        function (e) {
            if (e.which == key.tab)
                e.preventDefault();
        });

    return function (view) {
        $('.select2-container > input', view).keydown(function (e) {
            $(['#', this.parentElement.id.split('_')[1]].join('')).trigger(e);
        });

        $('.select2-container-multi', view).focus(function () {
            $(this).select2('focus');
        });
    };
});

About this issue

  • Original URL
  • State: closed
  • Created 11 years ago
  • Comments: 30 (5 by maintainers)

Most upvoted comments

Today I ran into the same issue where native “change” event was not being fired and then found this one.

It really pisses me off when some developers do not follow standards and are thinking they are above others and smarter than the rest. Why the hell would you not fire events for a HTML element you coded a jQuery replacement for? As you may have noticed, there’s a lot of issues about this opened (and closed??) where we, developers, are having problems with it, but you’re still being ignorant and won’t do what we ask for. Is there any specific reason why not? I came to this thread because right here I should be able to read the “discussion” why the native events are not being fired, but have read nothing about that, just another issue of the same.

You did a great job developing this jQuery extension and I do appreciate it and use it quite frequently, but will never understood this ignorance of yours (and others). I would understand if we would be asking for something not standard, but you’re cancelling (or not triggering, the same end result) native events.

Would appreciate if you would reconsider you position on this.

This is not yet solved

For anyone that finds this thread, and feels disappointed at how far they got, with a ticket ending in stale (twice), well this is what Ive got to work.

It essentially fires out a native duplicate change event that other plugins and addEventListener calls will hear.

This SO answer explains you cant trigger native events via jquery’s trigger, you have to fire the event yourself. https://stackoverflow.com/questions/37113525/how-to-trigger-js-native-even-addeventlistenerchange-function-by-jquery

Although the example there is using initEvent, which is deprecated everywhere except IE (https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent)

The new way is using new Event('<event-name>'), which of course IE doesnt support, so here’s what I’m using to support both;

function addNativeEventTrigger(e) {
    if (window.document.documentMode) {
        // (IE doesnt support Event() constructor)
        // https://caniuse.com/?search=Event()
        var evt = document.createEvent('HTMLEvents');
        evt.initEvent('change', false, true);
        e.currentTarget.dispatchEvent(evt);
    } else {
        const event = new Event('change');
        e.currentTarget.dispatchEvent(event);
    }
    $(e.currentTarget).one('change', this.addNativeEventTrigger);
}

// note one(...), not on(...), which triggers once and detaches. Otherwise you get stackoverflow as 
// this triggers more on(change) events.
$("select").one("change", addNativeEventTrigger);

I only needed to deal with change events, replace that with other events as needed.

It looks like many developers are having problems with focus events. I really hope we get a solution soon, since using the tab key to navigate through forms is very handy for our users.

I found out that tapping the space bar opens the select2 search, but it would be more user-friendly if that happens automatically, even if it requires some extra configuration.

A simple fix is to go back to relaying blur/focus events by adding them to the relayEvents array like so…

EventRelay.prototype.bind = function (decorated, container, $container) {
  var self = this;
  var relayEvents = [
    'open', 'opening',
    'close', 'closing',
    'select', 'selecting',
    'unselect', 'unselecting',
    'blur', 'focus'
  ];
...

then you can do something like…

$el.on("select2:blur", function (e) {
  e.type = 'blur';
  ...
})

tested on multiselects only

Ouch! 10 years on this is still a bug.

Dam how has this not been resolved yet! Looking a solution in 2024…

At this point I’m pushing this off of the 4.0 milestone, so Select2 will not be triggering the native events. Select2 will also not be triggering non-native versions of the events, which is less of an issue as we still have the option to add the native events without breaking compatibility.

If anyone is interested in making this happen, feel free to make a pull request and we’ll gladly review it.

I was hours finding a solution and I found it, is as simple as the following:

document.querySelector(“#id_of_your_element”).onchange = element => { console.log(element.target.value) }

I’d love for this to be fixed as well. For now I solved it like below with inspiration from https://makandracards.com/makandra/71890-events-triggered-by-jquery-cannot-be-observed-by-native-event-listeners.

jQuery.fn.enableNativeEvent = function(eventNames, convertedPropNames = []) {
  eventNames.forEach(eventName => {
    this.on(eventName, function($event) {
      if (!$event.originalEvent) {
        $event.preventDefault();
        const newName = `$:${eventName}`; // event to listen for is now "$:change.select2"
        const eventData = _.pick($event, convertedPropNames) // Uses lodash.pick
        const event = new CustomEvent(newName, 
          { detail: eventData, bubbles: true, cancelable: true  }
        )
        $event.target.dispatchEvent(event)
      }
    })
  })
  return this
}

// Usage
$(".select2able").select2().enableNativeEvent(["change.select2"], ["params"]);