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.
Lead Drupal Developer at @meetmiles
this.member
while private does notwindow
in a browser.
* In ECMAScript 2015 (ES6) let
and const
have block-level scope...
** Except for Function Constructors, which I won't cover.
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 Declarations:
* "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.
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.
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.
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.
Let's progressively introduce some concepts with code.
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.
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.
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.
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
.
To really see the power of the Module Pattern, there's a number of different ways to structure the code and imports/exports.
You want to extend a previously declared module.
var doSomething = (function (my) {
my.newProperty = 9;
return my;
}(doSomething));
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 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));
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.
These can be looked up as there are somewhat esoteric:
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);
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!
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));
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!
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));
// 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.
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
.
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.
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.
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.
See Getting Started
There's a number of standards you're going to run into, but here's one we've already seen:
'use strict';
just inside your function declarations."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');
.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
}
}
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.
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.
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_add_js
#attached
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.
#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.
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.
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.
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 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.
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.Use this to only add the JS to templates that need it.
{{ attach_library('my-component/my-js-module') }}
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.
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';
}
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';
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-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.
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: {
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.
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.
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.
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.
npm install -g bower
add bower_components to .gitignore
We're assuming you already have grunt installed, and are using git.In your module (AKA package) directory:
bower init
Then answer the questions - notable ones below:
* See Bower Module Types
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 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"
}
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
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!
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.
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'
]);
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.
bandc_js: {
options: {
separator : ';\n',
includeWithDependencies: true
},
include: [
'js-b',
'js-c'
],
dest: 'js_cat/bandc.js'
}
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
}
Lead Drupal Developer at @meetmiles
If you can read this you've gone too far.