One of my favourite technical authors Tom McFarlin recently published a piece about how to Improve JavaScript in WordPress. In it he introduces the object pattern as a way to namespace code, to avoid clashes just like we do in PHP with actual namespaces, classes or function name prefixes.
It’s certainly better than a whole load of individual functions, or worse, a whole load of anonymous functions directly bound to events. But could we do better?
The Module Pattern
A quick search will provide more tutorials about the Module pattern, each with slightly different examples. To me, it’s a way of encapsulating (hiding) all functionality in a module, and only revealing some of it to the outside world as needed.
I like to go one further, and avoid anonymous functions. Named functions are potentially re-usable, and help to self-document the code through the function names themselves.
Example
Let’s look at a before and after example I recently did for a client. The important bit here isn’t what the code is doing (or not, as there’s a bug in the original code that apparently wasn’t fixed in my rewrite), but how it is structured.
Here’s the original code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jQuery(document).ready(function($) { | |
// Responsive menu items: | |
$('.nav-header .genesis-nav-menu').before('<li class="menu-icon"></li>'); | |
$(".menu-icon").on("click", function(){ | |
$(".nav-header .genesis-nav-menu").slideToggle(); | |
}); | |
// Declare variables for heights: | |
var topnavHeight, sliderHeight, headerHeight | |
// Calculate variables and size of displacements: | |
$(window).on("load resize", function() { | |
// Menu reset before calculating heights: | |
$(".nav-header .genesis-nav-menu").hide(); | |
// Assign values to variables: | |
topnavHeight = $(".nav-secondary").outerHeight() || 0; | |
sliderHeight = $(".slides li").outerHeight() || 0; | |
headerHeight = $(".site-header").height(); | |
}); | |
// Determine header position and displacements: | |
$(window).on("load scroll resize", function() { | |
var scrollYpos = $(document).scrollTop() | |
var windowWidth = $(window).width() | |
// Media query to detect slider onscreen for window size > 960 pixels: | |
if (windowWidth > 960 && $(".home-slider").length > 0 && scrollYpos < sliderHeight – 5) { | |
// Set displacements in flow: | |
$(".site-container").css('padding-top',topnavHeight); | |
$(".home-slider").css('margin-bottom',headerHeight); | |
// Unstick header and make header full height: | |
$(".site-header").addClass('unstuck').css('top',sliderHeight + topnavHeight); | |
$(".site-header .wrap").removeClass('narrow'); | |
return false | |
} else { | |
// Set displacements in flow: | |
$(".site-container").css('padding-top',topnavHeight + headerHeight); | |
$(".home-slider").css('margin-bottom', 0); | |
// Make header sticky: | |
$(".site-header").removeClass('unstuck').css('top',topnavHeight); | |
// Make header narrow on scroll: | |
if (windowWidth > 960 && scrollYpos > 5) { | |
$(".site-header .wrap").addClass('narrow'); | |
} else { | |
$(".site-header .wrap").removeClass('narrow'); | |
} | |
} | |
}); | |
// Fade out store notice soon after page load: | |
$(".demo_store").delay(4000).slideUp(); | |
}); |
We have everything wrapped in a jQuery document ready event (Line 1). A responsive menu icon is added (line 5), and this has an anonymous callback added to the click event (lines 7-9).
There’s a few variables declared (line 13) before doing some callback on the window load and resize events. We don’t know what functionality, until we’ve read the whole function (or the comment above the binding). We’re hiding an element, and then assigning some widths and heights to those variables we declared outside of this function.
Then there’s some more functionality attached (line 31) to three more window events that moves a bunch of things around. Finally (line 71) we wait for something and then slide it up.
That’s probably very typical code found in themes. Nothing even gets defined to the JavaScript engine until the document ready event fires, so it delays what actions need to happen.
Let’s see how it could be improved:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var envy = (function( $ ) { | |
'use strict'; | |
var topnavHeight, sliderHeight, headerHeight, | |
/** | |
* Calculate variables and size of displacements. | |
* | |
* @since 1.0.0 | |
*/ | |
calculateSizes = function() { | |
// Menu reset before calculating heights: | |
$( '.nav-header .genesis-nav-menu' ).hide(); | |
topnavHeight = $( '.nav-secondary' ).outerHeight() || 0; | |
sliderHeight = $( '.slides li' ).outerHeight() || 0; | |
headerHeight = $( '.site-header' ).height(); | |
}, | |
/** | |
* Determine header position and displacements. | |
* | |
* @since 1.0.0 | |
*/ | |
doDisplacements = function() { | |
var scrollYpos = $( document ).scrollTop(); | |
var windowWidth = $( window ).width(); | |
// Media query to detect slider onscreen for window size > 960 pixels: | |
if ( windowWidth > 960 && $( '.home-slider' ).length > 0 && scrollYpos < sliderHeight – 5 ) { | |
// Set displacements in flow: | |
$( '.site-container' ).css( 'padding-top',topnavHeight ); | |
$( '.home-slider' ).css( 'margin-bottom',headerHeight ); | |
// Unstick header and make header full height: | |
$( '.site-header' ).addClass( 'unstuck' ).css( 'top', sliderHeight + topnavHeight ); | |
$( '.site-header .wrap' ).removeClass( 'narrow' ); | |
return false; | |
} else { | |
// Set displacements in flow: | |
$( '.site-container' ).css( 'padding-top', topnavHeight + headerHeight ); | |
$( '.home-slider' ).css( 'margin-bottom', 0 ); | |
// Make header sticky: | |
$( '.site-header' ).removeClass( 'unstuck' ).css( 'top', topnavHeight ); | |
// Make header narrow on scroll: | |
if ( windowWidth > 960 && scrollYpos > 5 ) { | |
$( '.site-header .wrap' ).addClass( 'narrow' ); | |
} else { | |
$( '.site-header .wrap' ).removeClass( 'narrow' ); | |
} | |
} | |
}, | |
/** | |
* Add in responsive menu feature. | |
* | |
* @since 1.0.0 | |
*/ | |
responsiveMenu = function() { | |
$( '.nav-header .genesis-nav-menu' ).before( '<li class="menu-icon"></li>' ); | |
$( '.menu-icon' ).on( 'click.envy', function() { | |
$( '.nav-header .genesis-nav-menu' ).slideToggle(); | |
}); | |
}, | |
/** | |
* Fade out store notice soon after page load. | |
* | |
* @since 1.0.0 | |
*/ | |
fadeOutStoreNotice = function() { | |
$( '.demo_store' ).delay( 4000 ).slideUp(); | |
}, | |
/** | |
* Fire events on document ready, and bind other events. | |
* | |
* @since 1.0.0 | |
*/ | |
ready = function() { | |
calculateSizes(); | |
doDisplacements(); | |
responsiveMenu(); | |
fadeOutStoreNotice(); | |
$( window ).on( 'resize.envy', calculateSizes ); | |
$( window ).on( 'scroll.envy resize.envy', doDisplacements ); | |
}; | |
// Only expose the ready function to the world | |
return { | |
ready: ready | |
}; | |
})( jQuery ); | |
jQuery( envy.ready ); |
We start off by creating our module, envy
, under which everything else is created. We assign to it an immediately invoked function expression (IIFE), that in this case, takes a single argument of $
, to which we pass in jQuery
, so we can use the typical shortcut for referencing jQuery. From the return at the end (line 95), envy
ends up as a simple object that has properties referencing internal methods (more on that below).
We then add in the 'use strict';
statement, to tell browsers that we’re not doing anything silly, and it should use the faster parsing engine that doesn’t have to account for bad practice silliness. It applies to everything inside this function scope.
We then have those variables declared again (line 4), and then we have four named methods – calculateSizes
, doDisplacements
, responsiveMenu
and fadeOutStoreNotice
. Even without looking at the contents of these methods, the names make it a little clearer what they are concerned with than anonymous functions.
There’s also a fifth method, called ready
(line 84). This is what will be called on the document ready event. This is also where the advantage of named functions comes to light – the calculateSizes()
and doDisplacements()
functions are called immediately (on document ready), but are also bound to the window resize and resize & scroll events respectively (using namespaced events as well to make unbinding easier), without having to re-define the whole function again.
For the technically astute, the functions aren’t named. They are anonymous functions that are assigned to properties of the envy
object, but as can be seen on lines 85-88, the end result is the same.
The final part of this module is to return an object (lines 95-97). This object has properties (only one here, line 96) which has a key, ready
that becomes the public name of the method, and a value of the envy
property, ready
, which has the function assigned to it.
Finally, this public method is used as the callback for the short version of the jQuery document ready event. When the file is first referenced, it can be parsed and have the envy
IIFE called to define everything (saving time later), but no calculations or DOM adjustments are done until the document ready event is triggered.
The code is also more adherent to the WordPress JavaScript Coding Standards.
Summary
Everyone will have their own preferences, but this version of the Module pattern makes most sense to me. Others try to avoid it for good reasons too. The choice is yours. You can even mix and match the approaches. For instance, Genesis admin JavaScript uses a named object as in Tom’s example, but with named functions and a ready function callback.
The Module pattern is just one more approach to be aware of, that might be right for your situation.
Thank you for a concise and clean example. Cheers!
The final line, jQuery( envy.ready ), confused me for a bit, until I realized/remembered “if you pass jQuery a function as a selector, it uses document.ready, thus making it a shortcut for document.ready” …. Which is something I do all the time, but I’m always passing jQuery an actual function, never a variable whose value is a function definition (which is a concept that’s always tripped me up when I run across it in code). Anyways, I love before/after examples, so great job on this one.
This is an excellent example of javascript modularity. I am currently working on a large scale WordPress site and I will definitely suggest using this approach to the team.
Thanks
One question though. What is the purpose of reference the parent object in event functions in this way?
$( ‘.menu-icon’ ).on( ‘click.envy’, function() {})
Where you have click.envy?
@James,
The
click
is the event that we’re binding the function to for the.menu-icon
. Fairly standard behaviour.The
.envy
bit is an event namespace. That way, someone else can write JS that just targets our event binding, and not all click event bindings against a particular selector. On it’s own, it does nothing, but does allow us to play nicely with others should they need it.Think of it like providing a filter in a WP plugin that you may not make use of yourself, but does allow others to customise how your plugin works.