Extending jQuery’s “is” with “before is” and “after is” event listeners

I happen to hate jQuery, mostly because it has the sloppiest, non-natural-language API imaginable.

is() exemplifies what’s wrong with the Framework That Changed The World™: jQueryWrappedElements.is(selector) doesn’t mean the collection of wrapped elements “is” the selector in the sense of equality (like JavaScript’s Object.is). Rather, it means at least one wrapped element is also in the collection of elements matched by a selector. The function should’ve been called hasCommonItemWith() or includesMemberOf()!*

Still, the fact Marketo forms are jQuery-powered — there’s a copy of jQuery embedded within the Forms 2.0 library — can help developers. If you need to alter default library behavior, and that behavior is done via jQuery wrappers as opposed to native DOM/Web API methods, you can isolate your hacks to Marketo’s jQuery (MktoForms2.$) without worrying about side effects.

For my upcoming blog post on inverting Checkbox values, the result of is(":checked") needs to be flipped when Marketo reads it.

So I figured: why not do it the pro-ish way and globally add events that fire before and after is(), allowing you to alter both the input and output of the method?

Here’s a function to extend any jQuery library with the new events:

/*
 * jQuery :: beforeIs/afterIs events
 * @author Sanford Whiteman
 * @version v1.0.0 2021-08-05
 * @copyright © 2021 Sanford Whiteman
 * @license Hippocratic 2.1: This license must appear with all reproductions of this software.
 *
 */
function installCustomIsHandlers(jQ) {
   let originalIs = jQ.fn.is;

   jQ.fn.is = function (initialSelector) {
      let selector = this.triggerHandler("fn.is.before", { selector: initialSelector}) || initialSelector;
      
      let is = originalIs.call(this, selector);
          
      let extendedIs = this.triggerHandler("fn.is.after", { selector: selector, was: is });

      if (typeof extendedIs == "boolean") {
         return extendedIs;
      } else {
         return is;
      }
   };
}

To extend Marketo’s bundled jQuery, you run:

 installCustomIsHandlers(MktoForms2.$);

Then add event listeners to jQuery wrapper objects.

  • For “before is” the event to listen for is fn.is.before.

    The listener function is passed a context object with a single property, selector, which is the selector originally passed to is(). If the listener returns any truthy value, that value becomes the new selector.

    Now you can modify the selector to match different elements:
jQueryWrappedElement.on("fn.is.before", function(event, context){
    if( context.selector == ":checked" ) {
        return ":selected"; // now matches different pseudo-class
    }
});
  • For “after is” the event is fn.is.after.

    The listener function is passed a context object with 2 properties, selector (the selector passed to is(), after optional preprocessing by fn.is.before) and was (the original result of the is() call). If the listener returns Boolean true or false, that becomes the final is() result, replacing the original.

    Now you can change the result that’s seen by code you don’t directly control, like forms2.js:
jQueryWrappedElement.on("fn.is.after", function (event, context) {
	if (context.selector == ":checked") {
    	return !context.was; // flip the result
    }
});

I already spoiled the fact that these events came about because one of them was needed for an upcoming Marketo blog post. So feel free to use them for your own purposes, but also look out for that post!


Notes

* Alas, the messed-up meaning of is() has now become fairly accepted, to the point there’s a new CSS is() selector that used to be called :matches(). Sigh.