Gamajo

Better Organise your JavaScript with the Module Pattern

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:

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();
});
view raw main.js hosted with ❤ by GitHub

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:

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 );
view raw main.js hosted with ❤ by GitHub

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.