Search code examples
vuejs2jquery-select2vue-component

Vue 2 custom select2: why is @change not working while @input is working


I created a custom select2 input element for Vue 2.

My question is: why is

<select2 v-model="vacancy.staff_member_id" @input="update(vacancy)"></select2>

working, but

<select2 v-model="vacancy.staff_member_id" @change="update(vacancy)"></select2>

not?

Since normal <input> elements in Vue have a @change handler, it would be nice if my custom select2 input has the same.


Some information on my custom element:

The purpose of this element is to not render all <option> elements but only those needed, because we have many select2 inputs on one page and many options inside a select2 input, causing page load to become slow.

This solution makes it much faster.

Vue.component('select2', {
    props: ['options', 'value', 'placeholder', 'config', 'disabled'],
    template: '<select><slot></slot></select>',
    data: function() {
        return {
            newValue: null
        }
    },
    mounted: function () {

        var vm = this;

        $.fn.select2.amd.require([
            'select2/data/array',
            'select2/utils'
        ], function (ArrayData, Utils) {

            function CustomData ($element, options) {
                CustomData.__super__.constructor.call(this, $element, options);
            }

            Utils.Extend(CustomData, ArrayData);

            CustomData.prototype.query = function (params, callback) {

                if (params.term && params.term !== '') {
                    // search for term
                    var results;
                    var termLC = params.term.toLowerCase();
                    var length = termLC.length;

                    if (length < 3) {
                        // if only one or two characters, search for words in string that start with it
                        // the string starts with the term, or the term is used directly after a space
                        results = _.filter(vm.options, function(option){
                            return option.text.substr(0,length).toLowerCase() === termLC ||
                                _.includes(option.text.toLowerCase(), ' '+termLC.substr(0,2));
                        });
                    }

                    if (length > 2 || results.length < 2) {
                        // if more than two characters, or the previous search give less then 2 results
                        // look anywhere in the texts
                        results = _.filter(vm.options, function(option){
                            return _.includes(option.text.toLowerCase(), termLC);
                        });
                    }

                    callback({results: results});
                } else {
                    callback({results: vm.options}); // no search input -> return all options to scroll through
                }
            };

            var config = {
                // dataAdapter for displaying all options when opening the input
                // and for filtering when the user starts typing
                dataAdapter: CustomData,

                // only the selected value, needed for un-opened display
                // we are not using all options because that might become slow if we have many select2 inputs
                data:_.filter(vm.options, function(option){return option.id === parseInt(vm.value);}),

                placeholder:vm.placeholder
            };

            for (var attr in vm.config) {
                config[attr] = vm.config[attr];
            }

            if (vm.disabled) {
                config.disabled = vm.disabled;
            }

            if (vm.placeholder && vm.placeholder !== '') {
                $(vm.$el).append('<option></option>');
            }

            $(vm.$el)
            // init select2
                .select2(config)
                .val(vm.value)
                .trigger('change')
                // prevent dropdown to open when clicking the unselect-cross
                .on("select2:unselecting", function (e) {
                    $(this).val('').trigger('change');
                    e.preventDefault();
                })
                // emit event on change.
                .on('change', function () {
                    var newValue = $(this).val();
                    if (newValue !== null) {
                        Vue.nextTick(function(){
                            vm.$emit('input', newValue);
                        });
                    }
                })
        });

    },
    watch: {
        value: function (value, value2) {

            if (value === null) return;

            var isChanged = false;
            if (_.isArray(value)) {
                if (value.length !== value2.length) {
                    isChanged = true;
                } else {
                    for (var i=0; i<value.length; i++) {
                        if (value[i] !== value2[i]) {
                            isChanged = true;
                        }
                    }
                }
            } else {
                if (value !== value2) {
                    isChanged = true;
                }
            }

            if (isChanged) {

                var selectOptions = $(this.$el).find('option');
                var selectOptionsIds = _.map(selectOptions, 'value');

                if (! _.includes(selectOptionsIds, value)) {
                    var missingOption = _.find(this.options, {id: value});

                    var missingText = _.find(this.options, function(opt){
                        return opt.id === parseInt(value);
                    }).text;

                    $(this.$el).append('<option value='+value+'>'+missingText+'</option>');
                }

                // update value only if there is a real change
                // (without checking isSame, we enter a loop)
                $(this.$el).val(value).trigger('change');
            }
        }
    },
    destroyed: function () {
        $(this.$el).off().select2('destroy')
    }

Solution

  • The reason is because you are listening to events on a component <select2> and not an actual DOM node. Events on components will refer to the custom events emitted from within, unless you use the .native modifier.

    Custom events are different from native DOM events: they do not bubble up the DOM tree, and cannot be captured unless you use the .native modifier. From the docs:

    Note that Vue’s event system is separate from the browser’s EventTarget API. Though they work similarly, $on and $emit are not aliases for addEventListener and dispatchEvent.

    If you look into the code you posted, you will see this at the end of it:

    Vue.nextTick(function(){
        vm.$emit('input', newValue);
    });
    

    This code emits a custom event input in the VueJS event namespace, and is not a native DOM event. This event will be captured by v-on:input or @input on your <select2> VueJS component. Conversely, since no change event is emitted using vm.$emit, the binding v-on:change will never be fired and hence the non-action you have observed.