Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam

Creating jQuery Function Parity With Umbrella JS

By
Published in Comments (3)

Yesterday, I talked about replacing jQuery with Umbrella JS and cutting my JavaScript bundle size (uncompressed) by 91%. And, while Umbrella JS has a very similar API to jQuery, it's not exactly the same. In order to ease the transition from jQuery to Umbrella JS, I created some Umbrella JS plugins that fill-in some of the function parity gaps between the two DOM (Document Object Model) manipulation libraries. This meant less refactoring in my main JavaScript modules.

As we looked at yesterday, the Umbrella JS object is structured just like the jQuery object in that it uses a constructor function - u() - and that all of the instance methods are defined on said constructor function's prototype, u.prototype. And, just as with jQuery, a "plugin" in Umbrella JS is nothing more than a function being injected into that u.prototype object.

What this means is that we can achieve some degree of jQuery function parity by simply adding new methods to the Umbrella JS prototype. To do this, I created a "Custom Umbrella" module that imports the u constructor, re-exports it for consumption, and then applies the modifications to the prototype chain. Then, in my main modules, I just make sure to import from this custom version instead:

import u from "./umbrella-custom.js";

And, here's what that custom module looks like - note that I am not trying to achieve complete jQuery function parity, I'm only filling in the functions that I actually needed on my blog. Of course, you could easily extend this to fit your own needs.

// I want the module import signature for my customized version of Umbrella JS to look
// just like the umbrella module. As such, I am going to export both the named and
// default exports.
export * from "umbrellajs";
export { default } from "umbrellajs";

// I also need to import the local reference so that I can modify it.
import u from "umbrellajs";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// First, I want to add a number of event-type convenience methods. Meaning, adding a
// .click() method instead of an .on(click) method. This just makes the transition from
// jQUery to Umbrella JS a bit easier.
var eventTypes = [
	"click",
	"error",
	"focus",
	"keydown",
	"keyup",
	"load",
	"mousedown",
	"mouseenter",
	"mouseleave",
	"mouseup",
	"scroll",
	"submit"
];

for ( let eventType of eventTypes ) {

	// u.prototype.mouseenter = function proxyFunction( handler ) { .. }
	u.prototype[ eventType ] = function proxyFunction( ...restArguments ) {

		if ( restArguments.length ) {

			return( u.prototype.on.apply( this, [ eventType, ...restArguments ] ) );

		}

		// If the convenience method is invoked with NO ARGUMENTS, we're going to
		// consider that a request to trigger the native method on the underlying nodes.
		// Some events use native DOM methods; others simulate the event using trigger.
		switch ( eventType ) {
			case "blur":
			case "click":
			case "focus":
			case "submit":

				// Use the native DOM methods.
				this.each(
					function iterator( node ) {

						node[ eventType ]();

					}
				);

			break;
			default:

				// Use the event simulation.
				this.trigger( eventType );

			break;
		}
		return( this );

	};

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

u.prototype.appendTo = function( target ) {

	u( target ).append( this );
	return( this );

};

u.prototype.css = function( styleProps ) {

	this.each(
		function iterator( node ) {

			Object.assign( node.style, styleProps );

		}
	);
	return( this );

};

// CAUTION: The hide/show methods bake in the assumption that showing and hiding are
// always based on block elements. Meaning, these methods do not store the previous
// display value and reinstate it, they always use "block" to restore visibility.
u.prototype.hide = function() {

	return( this.css({ display: "none" }) );

};

u.prototype.prop = function( name, value ) {

	// Getter mode, only acts on the first element.
	if ( value === undefined ) {

		return( this.first()[ name ] );

	}

	// Setter mode, applies value to all elements.
	this.each(
		function iterator( node ) {

			node[ name ] = value;

		}
	);
	return( this );

};

// CAUTION: The hide/show methods bake in the assumption that showing and hiding are
// always based on block elements. Meaning, these methods do not store the previous
// display value and reinstate it, they always use "block" to restore visibility.
u.prototype.show = function() {

	return( this.css({ display: "block" }) );

};

u.prototype.val = function( value ) {

	return( this.prop( "value", value ) );

};

Since I am building jQuery function parity for my own site, and not for general consumption, it means that I can bake some assumptions into my augmentation that may not apply to everyone. For example, my hide() and show() methods always use block as the display value. And, I don't have protection against performing reads on an empty collection. Meaning, attempting to call .val() on a non-matching element will throw a null-reference error. But, this is OK for my situation.

And that's all there is to it. Having this in place meant that I could keep my consuming JavaScript code almost exactly the same (other than swapping $() for u() and adding a first .first() calls to access the underlying node). All in all, it allowed me to cut my bundle size drastically with almost no effort.

Epilogue on var vs. let

If you've followed this blog for any length of time, you'll likely have noticed that I love using var for variable declarations. I use var because it works really well, it's very flexible, and it behaves according to a predictable set of rules (just like the rest of JavaScript). But, var doesn't always work the way I want it to; and so, in those situations, I will gladly use let in order to get the job done.

Case in point, look at the for-of iteration at the top of my custom Umbrella JS module. Notice that I am using:

for ( let eventType of eventTypes )

If that expression were using a var instead, my code would break. The reason for this is that the var declaration is hoisted to the top of the Function block. Which means, each iteration of the for-loop is writing the eventType value to the same, shared variable declaration (which, in turn, shares the same scoping of the for-loop itself). This, in turn, means that each instance of proxyFunction() that I generate closes over the same shared variable declaration. Which, in turn, means that the inner-workings of each proxyFunction() will actually be using the same eventType value at runtime, which is scroll - the last value assigned to the variable.

By using let in this situation, I am using a variable declaration that is scoped to for-loop block. It feels a little bit magical in this case; but, what this means is that each iteration of the for-loop actually traps its own value for eventType locally to the iteration. Which, in turn, means that each proxyFunction() that I generate closes over a unique iteration value for eventType.

This technique also works for normal for-loops, with the caveat being that you can use const in a for-of loop but not in other forms of for-loops (since the value needs to be overwritten).

Anyway, I get flack from time-to-time for using var. But, I just wanted to say that I am more than happy to use let when it solves a problem that var can't.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

It looks like your added functions are for convenience, to mash up existing umbrella functions with slightly different semantics. Have you had to re-implement anything from scratch that is just totally missing from umbrella? For example I had seen somewhere that a different jQuery followup had neglected to include an .ajax() method, and the author had to re-implement the basic functionality from scratch. Just curious.

15,880 Comments

@John,

Great question! You are correct that what you are seeing here is really just a mix-n-match of existing functionality in the Umbrella JS library. Really, the only thing that I've had to write from scratch is the jQuery.ajax() functionality. I've replaced those calls with my own, opinionated API client based on fetch():

www.bennadel.com/blog/4179-building-an-api-client-with-the-fetch-api-in-javascript.htm

Basically, I just create a wrapper around the fetch() method that makes it easier to invoke and then provides a normalized error structure should anything go wrong. Other than that, it's been smooth sailing. Though, keep in mind that this is just for a simple blog - it's not like I'm applying Umbrella JS to a massive, legacy application. So, even these refactors were relative small efforts.

15,880 Comments

I just added another simple one for some refactoring that I'm doing:

u.prototype.prependTo = function( target ) {

	u( target ).prepend( this );
	return( this );

};

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel