Creating jQuery Function Parity With Umbrella JS
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.
var
vs. let
Epilogue on 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
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.@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 onfetch()
: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.I just added another simple one for some refactoring that I'm doing:
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →