Home > OS >  How is jQuery.val able to bypass the get/set functions on the value property?
How is jQuery.val able to bypass the get/set functions on the value property?

Time:02-23

const { get, set } = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
let countryElement = $('select[name="country_id"]').get(0);
Object.defineProperty(countryElement, 'value', {
    get() {
        console.log('calling get');
        return get.call(this);
    },
    set(newVal) {
        console.log('New value assigned to selected[name="country_id"]: '   newVal);
        return set.call(this, newVal);
    }
});

If I run document.querySelector('select[name="country_id"]').value = 'CA' then the set function is called. If I run $('select[name="country_id"]').val('CA') the set function is not called.

My goal is to observe changes to a form field which are executed by a third party JS tool which I cannot change. However, I can't get it to work. As I dove into the issue, I found that I can even get jQuery to change the value without going through my set function. But I don't see how that's possible.

CodePudding user response:

Look into jQuery's source code. val does, eventually:

        hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];

        // If set returns undefined, fall back to normal setting
        if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
            this.value = val;
        }

So it accesses and calls jQuery.valHooks.select.set, which does, elsewhere in the source:

        set: function( elem, value ) {
            var optionSet, option,
                options = elem.options,
                values = jQuery.makeArray( value ),
                i = options.length;

            while ( i-- ) {
                option = options[ i ];

                /* eslint-disable no-cond-assign */

                if ( option.selected =
                    jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
                ) {
                    optionSet = true;
                }

                /* eslint-enable no-cond-assign */
            }

            // Force browsers to behave consistently when non-matching value is set
            if ( !optionSet ) {
                elem.selectedIndex = -1;
            }
            return values;
        }

It iterates through the options. The only lines that actually change the DOM in the above function is:

option.selected = jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1

or

elem.selectedIndex = -1;

So, jQuery isn't calling the value setter at all. You could emulate what it's doing and set the value of a <select> by assigning to the selected property of the option you want to make selected.

const { get, set } = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
let countryElement = $('select[name="country_id"]').get(0);
Object.defineProperty(countryElement, 'value', {
    get() {
        console.log('calling get');
        return get.call(this);
    },
    set(newVal) {
        console.log('New value assigned to selected[name="country_id"]: '   newVal);
        return set.call(this, newVal);
    }
});

countryElement.children[1].selected = true;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<select name="country_id">
  <option>foo</option>
  <option value="CA">CA</option>
</select>

My goal is to observe changes to a form field which are executed by a third party JS tool which I cannot change.

There are a number of ways to change a <select> value, which include .value, setting the .selected property of an option, and setting the .selectedIndex of the select. You'll need to implement all of them to do what you want reliably.

  • Related