Javascript Module Pattern

The Module Pattern encapsulates privacy, state and organization using closures, and promotes namespacing, clean interfaces and decoupling, and code maintainability and reuse.

Note that "module" here is a chunk of JavaScript code and not a Drupal module nor ES6/AMD modules.

About Todd

Lead Drupal Developer at @meetmiles

So your theme's script.js is endless and unmanageable...

WAT? No!

What We'll Cover

  • Module Pattern - advantages & disadvantages
  • Quick Intro to Concepts
  • Module Pattern - illustrative code
  • Module Pattern - sub-patterns and variations
  • Module Pattern - real examples
  • JSHint - install and config
  • Code organization
  • Drupal 7 & Javascript
  • Drupal 8 & Javascript
  • Grunt concat
  • Bower & grunt-bower-concat

Why the Module Pattern?

Advantages

  • Privacy: functions create scope; everything is private unless exported
  • State: private variables' values are maintained
  • Organization: code structure is not accessible outside the closure
  • Namespacing: only the module (function) name is available, nothing else is leaked into the global scope
  • Interfaces: returned objects are the public API; everything else is private
  • Decoupling: modules only communicate via their public interfaces
  • Maintainability: modules are leaner and specific, and are bounded by their API
  • Reuse: modules are modular, heh!

Why not this Pattern?

Disadvantages

  • Accessing public members requires this.member while private does not
  • Changes in visibility require changing access method
  • Since functions create scope, extending modules with later functions prevents them from accessing earlier private members
  • Unit testing can't access private members
  • You can't hotfix a module's privates by extension (you have to actually fix the code)
  • Private members can't be extended

Quick Intro to Concepts

  • Scope
  • Immediately-Invoked Function Expressions (IIFE)
    • Function Declaration vs Function Expression
    • Forcing Function Expressions
    • Immediately-Invoked
  • Closures

Scope

  • The "global" scope is the top-level scope, AKA window in a browser.
  • A "local" scope is any non-global scope.
  • Only functions create a new local scope*.
  • "Child" scopes inherit the scope of their parent**.
  • Scope chain is the local scope, then their parent's scope, and their parent's parent, and so on, all the way up to the global scope.
  • Parent scopes have no access to their children's scope.
  • * In ECMAScript 2015 (ES6) let and const have block-level scope... ** Except for Function Constructors, which I won't cover.

Immediately-Invoked Function Expressions* (IIFE**)

Let's look at types of function statements:


function myFunc() { /* a function declaration */ };
foo = function() { /* a function expression */  };
foo = function myFunc() { /* a function expression */ };
            

There's also Function Constructors but we're not concerned with them here. Do NOT use old incorrect names, "self-executing anonymous function" or "self-invoked anonymous function". ** Allegedly pronounced "iffy", but I've never heard it not spelled out.

Function Declaration vs Function Expression

Function Declarations:

  • FD's are stand alone constructs and not part of a larger expression.
  • FD's get hoisted* to the top of the scope.
  • FE's are part of an expression, usually a variable assignment.**
  • FE's are not hoisted***

* "hoisted" statements get automatically declared (but not initialized) at the top of the scope. ** This is valid but don't do it: console.log((function(a){return a+1;})(4) + (function(a){return a+2;})(3)); *** In var foo = function /* etc */ the var foo is hoisted but not the FE itself.

Forcing Function Expressions

You can force a function statement to be an expression by wrapping it in "grouping operators" ()'s (aka paren's)


(function myFunc() { /* now a function expression */ });
foo = (function() { /* still a function expression */  });
foo = (function myFunc() { /* still a function expression */ });
            

So ALWAYS wrap function statements in parens to make FE's!*

*Even if not needed, as it's clearer and the preferred syntax. You may see other ways to force a function expression but don't do them - they do not make for readable code! FD's may also become FE's when not used as a "source element" but I won't get into that.

Immediately-Invoked

You execute a function expression by appending () like myFunction()


(function () {})(); // <- this trailing () here
isTheSameAsFunct();
						

This is exactly like calling isTheSameAsFunct(x) but instead of calling function isTheSameAsFunct, you're defining the function in the expression.

}()) vs })(): They are *exactly* the same. Crockford prefers the first, but Drupal, jQuery and most libs prefer second. There's also (function(){}).call(this); but I'm not going to address that here.

Closures

Anytime you use the function keyword* inside a function, you are potentially creating a closure**.

A closure means that the inner (child) function "memorizes" the outer (parent) local scope so that it persists between calls to the inner function.


var myFunc = function (x) {
  var myObj = {};
  myObj.myClosure = function () {
    console.log(x++);
  };
  return myObj;
};
var myInst = myFunc(10);
myInst.myClosure(); // 10
myInst.myClosure(); // 11
						

Or the => (["fat"] Arrow functions) keyword in ECMAScript 2015 (ES6) ** Some will argue that technically a closure doesn't exist until during run time when a scope is externalized, but we're not going to get that deep.

Introduction to code

Let's progressively introduce some concepts with code.

Module Imports

It's bad form and slower* to depend on JS' implied globals. jQuery is passed in and aliased as $ inside.


(function ($) {
  $().someFunction();
})(jQuery);
						
If you use JShint (described later) it won't let you not import globals. * Implied globals means whenever a variable is used, the interpreter has to walk the scope chain backwards looking for a declaration.

Module (Public) Export

Assign the IIFE to a variable, and return whatever you'd like. Externally use the "interface": doSomething.publicProperty *.


var doSomething = (function () {
  var my = {},
  my.publicProperty = 2;
  my.publicMethod = function () { /* ... */ };

  return my;
})();
						
Some prefer to return an object literal, but I think it's cleaner to do as above. *JShint will enforce importing this module into others before you can use it.

Public and Private

privateVariable: using var or not*, it's not global; it's private as long as it's not returned


var doSomething = (function () {
  var privateVariable = 1; 
  function privateMethod() { /* ... */ };

  var my = {},
  my.publicProperty = 2;
  my.publicMethod = function () { /* ... */ };

  return my;
}());
						
*JShint will enforce using var before your function name.

Internal Member Referencing

To reference publicProperty you must use this


var doSomething = (function (console) {
  var my = {},
  my.publicProperty = 2;
  my.publicMethod = function () {
    console.log(privateVariable);
    console.log(this.publicProperty);
  };
  return my;
}(console));
						

This is because publicProperty in not local to the publicMethod (anonymous) scope, and is part of the returned object my so JS needs to be told the correct scope, this.

Sub-patterns and Variations

To really see the power of the Module Pattern, there's a number of different ways to structure the code and imports/exports.

Augmentation

You want to extend a previously declared module.


var doSomething = (function (my) {
  my.newProperty = 9;

  return my;
}(doSomething));
						

Loose Augmentation

You want to extend a module, but want to be flexible in declaration order. If doSomething is false, an empty object "{}" is passed in.


var doSomething = (function (my) {
  my.newProperty = 9;

  return my;
}(doSomething || {}));
						
This || {} structure only works for the module itself; you couldn't do something like jQuery || {} as a second param. Loose Augmentation is also helpful in async and non-blocking loading of individual modules. You must be careful when accessing previously declared public members of the module.

Tight Augmentation

Tight enforces declaration order, but allows overrides of previously declared public members.


var doSomething = (function (my) {
  var oldMethod = my.aMethod; // "save" old method

  my.aMethod = function () {
    /* do something first */
    oldMethod(); /* do old thing second */
  }
  return my;
}(doSomething));
						

Revealing Module pattern

This variation dispenses with object literal notation for public members at the expense of not allowing overrides in later code.


var doSomething = (function (my) {

  var publicProperty = 29;
  var badlyNamedPublicProperty = 39;

  function publicMethod () { /* stuff */ }

  return {
    publicProperty: publicProperty,
    publicMethod: publicMethod,
    nicelyNamedPublicProperty: badlyNamedPublicProperty
 }
}(doSomething));
						
This can also be used to implement the Façade Pattern. Note that doSomethig as my does not happen to referenced in this example.

Advanced

These can be looked up as there are somewhat esoteric:

  • Cloning and Inheritance: new module inherits from another
  • Cross-File Private State: split modules across files
  • Sub-modules: objects of object

Drupal and the Module Pattern

Drupal 7 uses IIFE's, as this is standard practice:


(function ($) { /* ... */ })(jQuery);
					

While not exactly the Module Pattern, this code from atop D7 drupal.js shares the Loose Augmentation assignment concept:


var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'locale': {} };
					

D8 also uses IIFE's


(function (domready, Drupal, drupalSettings, drupalTranslations) {
  /* ... */
})(domready, Drupal, window.drupalSettings, window.drupalTranslations);
					

Some Real Examples?

My "main" js file

Is itself an IIFE


(function (_, $, Drupal, window, document, console) {
  'use strict';

  $('#footer-main > .block').equalHeight();
    
  $(window).resize(_.debounce(
    function() {
      $('#footer-main > .block').equalHeight();
    }
    , 250
  ));
	
}(_, jQuery, Drupal, window, document, console));
						

Keep it clean and brief!

Just a Module

with Imports


// ie10ie11-check.js
(function (window, document) {
  'use strict';

  if ('onpropertychange' in document && !!window.matchMedia) {
    document.documentElement.className+=' ie10';
  }
  if (!(window.ActiveXObject) && 'ActiveXObject' in window) {
    document.documentElement.className+=' ie11';
  }
  
}(window, document));
						

Loose Augmentation

Adds two methods to _, and if _ library is already loaded, that's OK.


// underscore-debounce.js
// borrowed from http://underscorejs.org/docs/underscore.html#section-71 etc
var _ = (function (underscore) {
  'use strict';

  underscore.now = Date.now || function() { /* ... */ };

  underscore.debounce = function(func, wait, immediate) { /* ... */ };
  
  return underscore;

}(_ || {}));
						
Debounce is key to performant event management!

Using jQuery in Modules

Just like "normal" as long as you Import jQuery


(function ($, window, document, console) {
  'use strict';
  
  $(document).ready(function() { /* do stuff here */ });

}(jQuery, window, document, console));
						

Extending jQuery with a custom Plugin


// equal-heights.js
var jQuery = (function ($, console) {
	'use strict';

  // allHeights and equalHeights are now jQuery methods
  $.fn.allHeights = function() { /* ... */ return something; };

  $.fn.equalHeight = function () { /* ... */ return something; };
    
  return $;

}(jQuery, console));
						
jQuery.fn.equalHeight = (function($) { return function() { }; })(jQuery) would probably also work.

Drupal behaviors, 1

When your module does more than just Drupal.behaviors:


var myModule = (function (Drupal) {
  var myObject = {};

  /* various code */

  Drupal.behaviors.myDrupalModule =  { /* ... */ };

  return myObject;
})(Drupal);
						
I use myDrupalModule to distinguish it from the JS myModule.

Drupal behaviors, 2

When you just want to define multiple Drupal.behaviors:


var Drupal.behaviors = (function (behaviors) {
  behaviors.myDrupalModule =  { /* ... */ };

  behaviors.myOtherDrupalModule =  { /* ... */ };

  return behaviors;
})(Drupal.behaviors);
						
You can define just 1 behavior this way but perhaps it would be confusing? If you need Drupal for any reason you'll still have to pass it in, just not as first param.

Drupal behaviors, 3

When you just want to define a single Drupal.behaviors:


var Drupal.behaviors.myDrupalModule = (function () {
  var myObject =  { /* ... */ };
 
  return myObject;
})();
						
If you need Drupal for any reason you'll still have to pass it in, just not as first param.

JShint

I've mentioned JShint a few times - take the hint: it's a great way to ensure you're following some best practices, have consistent structure, and catches some errors.

You'll need Grunt (or Gulp - but you're on your own for that) for most of the rest of this presentation so I'll cover it briefly.

D8 uses JShint.

Grunt: The JavaScript Task Runner

gruntjs.com

See Getting Started

There's a number of standards you're going to run into, but here's one we've already seen:

  • Put 'use strict'; just inside your function declarations.

JS Hint, install

grunt-contrib-jshint

jshint.com

"JSHint is a community-driven tool to detect errors and potential problems in JavaScript code and to enforce your team's coding conventions."

npm install grunt-contrib-jshint --save-dev
Then add this to your Gruntfile.js:
grunt.loadNpmTasks('grunt-contrib-jshint');

JS Hint, config

Edit your .jshint file so you have a globals key. This defines them as external. true means code can modify them, false not.

{
  "globals": {
    "jQuery": true,
    "Drupal": false,
    "window": false,
    "document": false
    }
}
						

JS Hint, grunt config

Add something like this to your Gruntfile.js:

jshint: {
  options: {
    jshintrc: '.jshintrc'
  },
  all: [
    paths.js + '/{,**/}*.js',
    '!' + paths.js + '/{,**/}*.min.js',
    '!' + paths.js + '/compiled_script.js'
  ]
},
						
Note that I'm ! excluding already minified and compiled files.

JS Hint, grunt task

Add something like this to your Gruntfile.js:


grunt.registerTask('js', [
    'jshint',
    /* put before uglify, minify, etc... */
  ]);
						

Now just use grunt js to run the JShint task.

Code Organization

You could use this pattern and put all the modules in one file but you'd be missing out on readability, improved maintainability, and code reuse.

So now you have a few or perhaps dozens of JS module files. How do you get them into the browser, given dependencies and all that? Your options:

  • Drupal 7's drupal_add_js
  • Drupal 7's #attached
  • Drupal 7's library functions
  • Drupal 8's Libraries
  • Grunt concat
  • Bower & grunt-bower-concat

Drupal 7's drupal_add_js

The simplest way to manage all these files is to use function drupal_add_js

There's no dependency management so you'll have to manage all that yourself using the verbose syntax:


drupal_add_js(
  'relative/path/to/my-module.js'
  , array(
    'type' => 'file',
    'scope' => 'footer',
    'group' => JS_THEME,
    'weight' => 5,
  )
);
						
Set weight, scope and group as appropriate.

Drupal 7's #attached

The better Drupal way is to use #attached, see Form API's and function drupal_process_attached.

Like drupal_add_js, there's no dependency management:


$element['#attached']['js'][] = array(
  'type' => 'file',
  'data' => 'relative/path/to/my-module.js',
  'scope' => 'footer',
  'group' => JS_THEME,
  'weight' => 5,
));
						
My understanding is that using #attached offers some improved file caching.

Drupal 7's Libraries

Perhaps the best native Drupal way is to define each module as a library using function hook_library and then function drupal_process_attached.

Unlike drupal_add_js and #attached, there IS dependency management:

Another benefit is that it is close, conceptually, to how D8 handles it. Note that this is NOT the Libraries Module, but core API.

Define the Library

Note that the hook instance, 'hook', and the key, 'my-module', make a unique key for reference this lib.


function hook_library() {
  $libraries['my-module'] = array(
    'title' => 'My Module',
    'js' => array(
      'relative/path/to/my-module.js' => array(),
    ),
    'dependencies' => array(
      // require jQuery UI core by System module.
      array('system', 'ui'),
      // require my-other-module
      array('hook', 'my-other-module'),
    ),
  );
  // define other libs...
  return $libraries;
}
						
You can also define css, website, and version if desired.

Attach the Library

You need only attach the libs you need and it will pull in dependencies as needed.


$element['#attached']['libraries_load'[] = array('hook', 'my-module');
						
You may also use function drupal_add_library

Drupal 8's Libraries

Drupal 8 does away with most of the D7 methods, except "Libraries", and now only has only one way of defining them, but a few ways of "attaching" them.

The difference is that libraries are defined in a yaml file, not code.

Below, my-component is the name of the theme or module. The .theme file is a PHP file which basically takes the place of D7's template.php files. Libraries also define CSS assets but we won't discuss those here.

Define the Library

D8 now uses yaml files extensively, so to define a JS library, you'll use something like this:


my-js-module:
  version: 1.x
  js:
    js/my-js-module.js: {}
  dependencies:
    - core/jquery.ui
            

This would be in a myModule.libraries.yml file in your myModule directory, or a myTheme.libraries.yml your myTheme folder.

Drupal 8 no longer loads jQuery (or any other JS) on pages by default, everything has to be loaded directly or by dependency. yaml files depend on indention for structure.

Attach the Library, via Twig

Use this to only add the JS to templates that need it.


{{ attach_library('my-component/my-js-module') }}
            

Attach the Library, via theme's info.yml

This adds to *all* pages on the site, by adding these lines to my-theme.info.yml


libraries:
  - my-component/my-js-module
            
Attaching to all pages is something probably done rarely.

Attach the Library, via THEME_preprocess_HOOK()

Add to all (or some, if placed in a conditional) pages, by placing this code in your my-theme.theme file:


function my_theme_preprocess_page(&$variables) {
  $variables['#attached']['library'][] = 'my-component/my-js-module';
}
            

Attach the Library, via the render array

Add to certain page elements, by placing this code in your my-theme.theme file:


// in some theme callback, probably in a conditional
$build['some-element']['#attached']['library'][] = 'my-component/my-js-module';
            

Attach the Library, via hook_page_attachments()

Add to all (or some, if placed in a conditional) pages, by placing this code in your my-theme.theme file:


function contextual_page_attachments_alter(array &$page) {
  $page['#attached']['library'][] = 'my-component/my-js-module';
}
            

Grunt concat

grunt-contrib-concat allows one to set up concatenation steps within your Gruntfile.js.

There's no dependency management, but the upside is you only need to add one combined JS file into Drupal.

Personally, I prefer controlling the preprocessing of scripts outside of Drupal.

The downside is you get ALL the JS ALL the time.

concat, install

npm install grunt-contrib-concat --save-dev
Then add this to your Gruntfile.js:
grunt.loadNpmTasks('grunt-contrib-concat');

We're assuming you already have grunt installed.

concat, config


concat: {
  options: {
    separator: ';' + grunt.util.linefeed, // defensive semicolon
    sourceMap: true, // for debug
    nonull: true,   // warn of missing files
  },
  dist: {
   src: [ 
      paths.js + '/ie10ie11-check.min.js',
      paths.js + '/underscore-debounce.min.js',
      paths.js + '/equal-heights.min.js',
      paths.js + '/script.min.js'
    ],
    dest: paths.js + '/compiled_script.js',
 },
},
						
Define src: []in dependency order - there's no "lifting" among modules. I'm also concatting the minified files.

concat, grunt task

Add something like this to your Gruntfile.js:


grunt.registerTask('js', [
  /* put after uglify, minify, jshint, etc... */
  'concat:dist'
]);
						
Now just use grunt js to run the task that includes concat.

concat, grunt, alternatives

Grunt offers a number of ways to "build" file lists, including multiple build targets, wildcards, dynamic filesnames, exclusions and even code to perform complex file selections.

  • You may wish to make multiple compiled "target" files, and split them by section or function of your website. Just ensure you don't load some dependency twice.

Bower & grunt-bower-concat

Bower is "A package manager for the web", and modules are packages, so...

This is where modularity, interfaces, and reuse really come into play. Why copy & paste code between projects, or worse, rewrite code again and again?

Conceptually, this is like Drupal's Libraries, but the meta-data, including dependencies, is maintained with the module, where it belongs.

Brace yourselves, this is a long one.

Bower etc., installing

npm install -g bower

add bower_components to .gitignore

We're assuming you already have grunt installed, and are using git.

Bower etc., creating a package

In your module (AKA package) directory:

bower init

Then answer the questions - notable ones below:

  • version: // Semantic versioning, ex 1.9.3-beta
  • main file: // in context, this is your js file
  • type of package: globals // this is Module Pattern*
  • mark this package as private? Yes // probably

* See Bower Module Types

Bower etc., git part

By specifying module versions you can also reduce acquired incompatibilities, as packages will continue to use old dependencies.

So now that you've recorded the version number in Bower, you need to do so in git:


git push -u origin master // code push before tag
git tag -a m.n.p -m "tag message"
git push origin m.n.p
						
m.n.p is Semantic versioning: major.minor.patch, but extended versions are also legal. More on Git tagging

Bower etc., registries

Bower searches registries, default and custom, for packages. By default it searches the Bower registry and Github, by "shorthand" name, and it will accept full git URLs.

To add a "shorthand" for your organization's internal git repo, see this example for gitlab. Add/edit .bowerrc:


{
  "registry": {
    "search": [
      "git@gitlab.{your-org}.com"
    ]
  },
  "shorthand-resolver": "git@gitlab.{your-org}.com:{{shorthand}}.git"
}
						

Bower etc., installing and other commands

To install a package use it's "shorthand" name:


bower install user/repo-name[m.n.b] -S // the [semver*] is optional
						
-S flag will save the dependency in bower.json (which must exist prior). Bower will also automatically include dependencies of that package. *SemVer is Semantic Versioning, which is also used by D8 & Composer.

Other commands:


bower info {package} // displays package info and version
bower list {displayes} // local packages and updates
bower update {package} // updates the package
bower uninstall {package} // is self explanatory
						

Using Bower packages

You could stop here and use any of Drupal's built-in methods to load your packages, or even plain grunt concat.

But there's a better way: use grunt-bower-concat to concat your packages - in dependency order, automatically!

bower-concat, install

npm install grunt-bower-concat --save-dev
Then add this to your Gruntfile.js:
grunt.loadNpmTasks('grunt-bower-concat');

bower-concat works by reading the bower.json files of all the Bower packages installed, determining their dependencies, and then concat's them in order.

bower-concat, config

The magic works out of the box IF you only want to make a *single* resulting js file. Here's an example Gruntfile.js:


grunt.initConfig({
  bower_concat: {
    dest: {
      options: {
        separator : ';\n' // defensive semicolon
      },
      dest: 'js_cat/dest.js'
    }
  }
});

grunt.registerTask('default', [
  // other tasks first
  'bower_concat:dest'
]);
						

bower-concat, going further

One of the disadvantages of bower-concat is it makes only one target file. There is an include parameter which has the unfortunate side effect of NOT including dependencies.

But when used with the parameter, includeWithDependencies you can now specify multiple targets, with automatic dependency inclusion, but the downside is you must ensure you don't load multiple targets on the same page which have the same dependencies.

Because of this caveat, the best use of this is dividing your targets not by component, but by page. *Provided by a patch by me, now merged.

bower-concat, includeWithDependencies


bandc_js: {
  options: {
    separator : ';\n',
    includeWithDependencies: true
  },
  include: [
    'js-b',
    'js-c'
  ],
  dest: 'js_cat/bandc.js'
}
						

output two files


bandc_js: {
  options: {
    separator : ';\n',
    includeWithDependencies: true },
  include: [ 'js-b', 'js-c' ],
  dest: 'js_cat/bandc.js'
}
dwithc_js: {
  options: {
    separator : ';\n'
    includeWithDependencies: true },
  include: [ 'js-d' ],
  dest: 'js_cat/dwithc.js'  // js-d has js-c as dependency
}
						

Further Reading

About Todd

Lead Drupal Developer at @meetmiles

If you can read this you've gone too far.