Scalable JavaScript Design Patterns in Drupal
Ryan McVeigh
Watch as we go at:
Ryan McVeigh
In the beginning, themes generally start out small.
With time, we add a few more files to our theme and it begins to grow.
Soon enough, we have so many files it becomes difficult to handle structure and organization.
What if all of this grinds to a halt because an unknown dependency goes offline? Can everything keep on functioning?
We can introduce a central way of controlling this chaos, solving these problems.
If a file goes down, the task runner can respond and react accordingly. e.g. Tell the dev where the error lies via a linter or test suite.
Think about the future. You should be able to change themes or infrastructure if you find something better...
The Tools For Our Theme
Design Patterns
JavaScript
Scalable Application Architecture
We’re Individuals
"You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist."
- Friedrich Nietzsche
We all do things differently
Each of us have preferences for how we approach..
Solving problems
Solving scalability
Structuring solutions
Great but can lead to..
serious problems when working on code to be used by others.
Inconsistent solutions
Inconsistent architecture
Difficult refactoring
Design Patterns
Reusable solutions that can be applied to commonly occurring problems in software design and architecture.
“We search for some kind of harmony between two intangibles: a form we have not yet designed and a context we cannot properly describe’
- Christopher Alexander, the father of design patterns.
They’re proven
Patterns are generally proven to have successfully solved problems in the past.
Solid
Reflect experience
Reliable approaches
Represent insights
They’re reusable
Patterns can be picked up, improved and adapted without great effort.
Incredibly flexible
Out-of-the-box
solutions
Easily adapted
By Krdan [Public domain], via Wikimedia Commons
They’re expressive
Patterns provide us a means to describing approaches or structures.
Easier than describing syntax and semantics
Common vocabulary
for expressing solutions elegantly.
Problem agnostic
They offer value
Patterns genuinely can help avoid some of the common pitfalls of development.
Major problems that can cause later down the line
Prevent minor issues that can cause
JavaScript Patterns
Writing code that’s expressive, encapsulated & structured
Module Pattern
An interchangeable single-part of a larger system that can be easily re-used.
“Anything can be defined as a reusable module”
- Nicholas Zakas, author ‘Professional JavaScript For Web Developers’
History
From a historical perspective, the Module pattern was originally developed by a number of people including Richard Cornford in 2003. It was later popularized by Douglas Crockford in his lectures. Another piece of trivia is that if you've ever played with Yahoo's YUI library, some of its features may appear quite familiar and the reason for this is that the Module pattern was a strong influence for YUI when creating their components.
If you've ever played with Yahoo's YUI library, some of its features may appear quite familiar and the reason for this is that the Module pattern was a strong influence for YUI when creating their components.
Stepping stone: IIFE
Immediately invoked function expressions (or self-executing anonymous functions)
(function() {
// code to be immediately invoked
}()); // Crockford recommend this way
(function() {
// code to be immediately invoked
})(); // This is just as valid
(function( window, document, undefined ){
//code to be immediately invoked
})( this, this.document);
(function( global, undefined ){
//code to be immediately invoked
})( this );
Defined by Ben Alman in his blog.
This is great, but..
there's no privacy.
Privacy In JavaScript
There isn’t a true sense of it in JavaScript.
Variables & Methods can’t be ‘private’
No Access Modifiers
Variables & Methods can’t be ‘public’
Simulate privacy
The typical module pattern is where immediately invoked function expressions (IIFEs) use execution context to create ‘privacy’. Here, objects are returned instead of functions.
// Object literal pattern
var basketModule = (function() {
var basket = []; //private
return { //exposed to public
addItem: function(values) {
basket.push(values);
},
getItemCount: function() {
return basket.length;
},
getTotal: function(){
var q = this.getItemCount(),p=0;
while(q--){
p+= basket[q].price;
}
return p;
}
}
}());
-
In the pattern, variables declared are only available inside the module.
-
Variables defined within the returning object are available to everyone
-
This allows us to simulate privacy
Notice the () around the anonymous function. This is required by the language, since statements that begin with the token function are always considered to be function declarations. Including () creates a function expression instead.
Sample usage
Inside the module, you'll notice we return an object. This gets automatically assigned to basketModule so that you can interact with it as follows:
//basketModule is an object with properties which can also be methods
basketModule.addItem({item:'bread',price:0.5});
basketModule.addItem({item:'butter',price:0.3});
console.log(basketModule.getItemCount());
console.log(basketModule.getTotal());
//however, the following will not work:
// (undefined as not inside the returned object)
console.log(basketModule.basket);
//(only exists within the module scope)
console.log(basket);
Global Import
Whenever a name is used, the interpreter walks the scope chain backwards looking for a var statement for that name. If none is found, that variable is assumed to be global. If it’s used in an assignment, the global is created if it doesn’t already exist. This means that using or creating global variables in an anonymous closure is easy. Unfortunately, this leads to hard-to-manage code, as it’s not obvious (to humans) which variables are global in a given file.
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
By passing globals as parameters to our anonymous function, we import them into our code, which is both clearer and faster than implied globals.
Module Pattern: Dojo
Dojo attempts to provide 'class'-like functionality through dojo.declare, which can be used for amongst other things, creating implementations of the module pattern. Powerful when used with dojo.provide.
// traditional way
var store = window.store || {};
store.basket = store.basket || {};
// another alternative..
// using dojo.setObject (with basket as a module of the store namespace)
dojo.setObject("store.basket.object", (function() {
var basket = [];
function privateMethod() {
console.log(basket);
}
return {
publicMethod: function(){
}
};
}()));
Module Pattern: jQuery
In the following example, a library function is defined which declares a new library and automatically binds up the init function to document.ready when new libraries (ie. modules) are created.
function library(module) {
$(function() {
if (module.init) {
module.init();
}
});
return module;
}
var myLibrary = library(function() {
return {
init: function() {
/*implementation*/
}
};
}());
Module Pattern: YUI
A YUI module pattern implementation that follows the same general concept.
YAHOO.store.basket = function () {
//"private" variables:
var myPrivateVar = "I can be accessed only within YAHOO.store.basket .";
//"private" method:
var myPrivateMethod = function () {
YAHOO.log("I can be accessed only from within YAHOO.store.basket");
}
return {
myPublicProperty: "I'm a public property.",
myPublicMethod: function () {
YAHOO.log("I'm a public method.");
//Within basket, I can access "private" vars and methods:
YAHOO.log(myPrivateVar);
YAHOO.log(myPrivateMethod());
//The native scope of myPublicMethod is store so we can
//access public members using "this":
YAHOO.log(this.myPublicProperty);
}
};
}();
Better: AMD
Take the concept of reusable JavaScript modules further with the Asynchronous Module Definition.
Non-blocking, parallel loading and well defined.
Mechanism for defining asynchronously loadable modules & dependencies
Stepping-stone to the module system proposed for ES Harmony
Why Is AMD A Better Choice For Writing Modular JavaScript?
- Provides a clear proposal for how to approach defining flexible modules.
- Significantly cleaner than the present global namespace and <script> tag solutions many of us rely on. There's a clean way to declare stand-alone modules and dependencies they may have.
- Module definitions are encapsulated, helping us to avoid pollution of the global namespace.
- Works better than some alternative solutions (eg. CommonJS, which we'll be looking at shortly). Doesn't have issues with cross-domain, local or debugging and doesn't have a reliance on server-side tools to be used. Most AMD loaders support loading modules in the browser without a build process.
- Provides a 'transport' approach for including multiple modules in a single file. Other approaches like CommonJS have yet to agree on a transport format.
- It's possible to lazy load scripts if this is needed.
AMD: define()
define allows the definition of modules with a signature of define(id /*optional*/, [dependencies], factory /*module instantiation fn*/);
// A module_id (myModule) is used here for demonstration purposes only
define('myModule',
['foo', 'bar'],
// module definition function
// dependencies (foo and bar) are mapped to function parameters
function ( foo, bar ) {
// return a value that defines the module export
// (i.e the functionality we want to expose for consumption)
// create your module here
var myModule = {
doStuff:function(){
console.log('Yay! Stuff');
}
}
return myModule;
});
AMD: require()
require.js is used to load code for top-level JS files or inside modules for dynamically fetching dependencies
/* top-level: the module exports (one, two) are passed as
function args to the callback.*/
require(['one', 'two'], function (one, two) {
});
/* inside: complete example */
define('three', ['one', 'two'], function (one, two) {
/*require('string') can be used inside the function
to get the module export of a module that has
already been fetched and evaluated.*/
var temp = require('one');
/*This next line would fail*/
var bad = require('four');
/* Return a value to define the module export */
return function () {};
});
Registering jQuery As An Async-compatible Module
AMD loaders need to take into account multiple versions of the library being loaded into the same page as you ideally don't want several different versions loading at the same time.
// Account for the existence of more than one global
// instances of jQuery in the document, cater for testing
// .noConflict()
var jQuery = this.jQuery || "jQuery",
$ = this.$ || "$",
originaljQuery = jQuery,
original$ = $,
amdDefined;
define(['jquery'] , function ($) {
$('.items').css('background','green');
return function () {};
});
// The very easy to implement flag stating support which
// would be used by the AMD loader
define.amd = {
//supports multiple jQuery versions
jQuery: true
};
ECMAScript 6 (ES2015)
A promise is an object which is used for deferred and asynchronous computations. A promise represents an operation that hasn’t completed yet, but is expected in the future. Promises are a way of organizing asynchronous operations in such a way that they appear synchronous.
ECMAScript 7 (ES2016)
Async functions are built on top of ECMAScript 6 features like generators. The introduction of promises and generators in ECMAScript presents an opportunity to dramatically improve the language-level model for writing asynchronous code in ECMAScript. Indeed, generators can be used jointly with promises to produce the same results but with much more user code.
Note: The async function is available in Edge.
Alternative: CommonJS
Another easy to use module system with wide adoption server-side
Format widely accepted
on a number of server-side platforms (Node)
CommonJS
Working group designing, prototyping, standardizing JS APIs
Competing standard. Tries to solve a few things AMD doesn’t.
CommonJS Modules
They basically contain two parts: an exports object that contains the objects a module wishes to expose and a require function that modules can use to import the exports of other modules
/* here we achieve compatibility with AMD and CommonJS
using some boilerplate around the CommonJS module format*/
(function(define){
define(function(require,exports){
/*module contents*/
var dep1 = require("foo");
var dep2 = require("bar");
exports.hello = function(){...};
exports.world = function(){...};
});
})(typeof define=="function"? define:function(factory){factory
(require,exports)});
Universal Module Definition
Defining modules that can work anywhere. Credit: @KitCambridge
(function (root, Library) {
// The square bracket notation is used to avoid property munging by the Closure Compiler.
if (typeof define == "function" && typeof define["amd"] =="object" && define["amd"]) {
// Export for asynchronous module loaders (e.g., RequireJS, `curl.js`).
define(["exports"], Library);
}
else {
// Export for CommonJS environments, web browsers, and JavaScript engines.
Library = Library(typeof exports == "object" && exports|| (root["Library"] = {
"noConflict": (function (original) {
function noConflict() {
root["Library"] = original;
// `noConflict` can't be invoked more than once.
delete Library.noConflict;
return Library;
}
return noConflict;
})(root["Library"])
}));
}
})(this, function (exports) {
// module code here
return exports;
});
Drupal JS
How does this translate into Drupal?
// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it with an "anonymous closure". See:
// - https://drupal.org/node/1446420
// - http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth
(function ($, Drupal, window, document, undefined) {
'use strict';
// To understand behaviors, see https://drupal.org/node/756722#behaviors
Drupal.behaviors.myCustomBehavior = {
attach: function (context, settings) { // jshint ignore:line
// Place your code here.
}
};
})(jQuery, Drupal, this, this.document);
Drupal 7 uses immediately invoked function expressions (IIFE's)
Just say no to Spaghetti Code
(function ($, Drupal, window, document, undefined) {
'use strict';
Drupal.behaviors.equalizeHeights = {
attach: function (context, settings) { // jshint ignore:line
// Build our equalize function.
function equalize() {
$('footer .region').not('.region.region-footer-fourth').matchHeight();
$('.view-id-grid .views-row').matchHeight();
$('.trip-idea-trip_idea_related .field-name-title-field h2').matchHeight();
$('.field-group--mentioned > div').not('.field-group--mentioned .field-name-field-related-listings-title').matchHeight();
$('.group-find-nearby > form, .group-find-nearby > div').matchHeight();
}
// Run equalize on page load.
$(window).load(function() {
equalize();
});
// Rerun equalize function after ajax runs.
$(document).ajaxComplete(function () {
equalize();
});
}
};
})(jQuery, Drupal, this, this.document);
Spaghetti Turned Modular
var equalize = { // object literal pattern (var).
elements: [$('.view-id-grid .views-row'), $('.group-find-nearby > div')],
init: function() {
this.cacheDom();
this.bindEvents();
},
cacheDom: function() {
this.$doc = $(document);
this.$win = $(window);
},
bindEvents: function() {
this.$win.on('load', this.matchEl.bind(this));
this.$doc.ajaxComplete(this.matchEl.bind(this));
},
matchEl: function() {
$.each(this.elements, function() {
$(this).matchHeight();
});
}
};
equalize.init();
Drupal 8
Boom! Modular out of the box
// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it with an "anonymous closure". See:
// - https://drupal.org/node/1446420
// - http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth
(function ($, Drupal) {
'use strict';
// To understand behaviors, see https://drupal.org/node/756722#behaviors
Drupal.behaviors.myCustomBehavior = {
attach: function (context, settings) {
// Place code here.
}
};
})(jQuery, Drupal);
Drupal 8
even lets you define your own dependencies inside the *.libraries.yml file.
drupal.nav-tabs:
version: VERSION
js:
js/nav-tabs.js: {}
dependencies:
- core/matchmedia
- core/jquery
- core/drupal
- core/jquery.once
- core/drupal.debounce
From D8's seven.libraries.yml file
Theme Level Patterns
Create a base that's solid and supportive
Scalable Architecture
Strategies for decoupling and future-proofing the structure of your theme.
Challenge
Define what it means for a theme to be ‘large’.
- It’s a very tricky question to get right
- Even experienced themers have trouble accurately defining this
My Answer
Large-scale themes are non-trivial applications requiring significant development effort to maintain. They usually consist of a large number of files.
Think Long-Term
What future concerns haven’t been factored in to your architecture?
- You may decide to switch from using jQuery to Dojo or YUI for reasons of performance, security or design
- Libraries are not easily interchangeable and have high switching costs if tightly coupled to your app
Ask Yourself
This is important.
If you reviewed your architecture right now, could a decision to switch libraries be made without rewriting your entire application?
“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application”
- Justin Meyer
“The more tied components are to each other, the less reusable they will be, and the more difficult it becomes to make changes to one without accidentally affecting another”
- Rebecca Murphey
“The key is to acknowledge from the start that you have no idea how this will grow. When you accept that you don't know everything, you begin to design the system defensively. You identify the key areas that may change, which often is very easy when you put a little bit of time into it.”
- Nicholas Zakas
Solution: Design Patterns
Fixing our architecture with JavaScript design patterns.
“The only difference between a problem and a solution is that people understand the solution.”
- Charles F. Kettering
Brainstorm.
What do we want?
Loosely coupled
architecture
Functionality broken down into smaller independent modules
Framework or library agnostic. Flexibility to change in future.
Some More Ideas.
How might we achieve this?
Theme interprets requests. Modules don’t access the core or libraries directly.
Single modules speak
to the app when something interesting happens
Prevent apps from falling over due to errors with specific modules.
Current Architecture
If working on a significantly large theme, remember to dedicate sufficient time to planning the underlying architecture that makes the most sense to your team.
Your Current Architecture
will probably contain a mixture of the following:
Basic Files (.info.yml, template.php, etc)
Assets Direcotries (js, scss, css, images, etc)
Template Files
Extras
Template Files
In large themes template files should be separated by the base template type (ie. system, node, block, view).
templates/page/page.tpl.php
templates/page/page-front.tpl.php
templates/node/node.tpl.php
templates/node/node-blog.tpl.php
templates/views/views-view.tpl.php
templates/views/views-view-fields--directory.tpl.php
templates/views/views-view-field--user-list.tpl.php
templates/block/block.tpl.php
templates/block/block-footer.tpl.php
templates/block/block-login.tpl.php
templates/blocks/block--system-branding-block.html.twig
templates/blocks/block--system-menu-block.html.twig
templates/blocks/block.html.twig
templates/content/node.html.twig
templates/content/page-title.html.twig
templates/content/search-result.html.twig
templates/layout/maintenance-page.html.twig
templates/layout/html.html.twig
templates/layout/page.html.twig
templates/layout/region.html.twig
templates/page/page.tpl.php
templates/page/page-front.tpl.php
templates/node/node.tpl.php
templates/node/node-blog.tpl.php
templates/views/views-view.tpl.php
templates/views/views-view-fields--directory.tpl.php
templates/views/views-view-field--user-list.tpl.php
templates/block/block.tpl.php
templates/block/block-footer.tpl.php
templates/block/block-login.tpl.php
Assets
Keep assets in separate folders like so:
theme-name/css/
theme-name/images/
theme-name/js/
theme-name/scss/
theme-name/assets/css/
theme-name/assets/images/
theme-name/assets/js/
theme-name/assets/scss/
or you may want to organize them into an assets folder like this:
Template.php Files (D7)
In large themes it is common to see template.php files split out into multiple files (where all the logic is broken out into correlating files).
theme-name/template.php
theme-name/includes/pager.inc
theme-name/includes/field.inc
//////////////////////////////
// Includes
//////////////////////////////
require_once dirname(__FILE__) . '/includes/field.inc';
require_once dirname(__FILE__) . '/includes/pager.inc';
template.php
Extras
Keep these files in their own folders if possible.
theme-name/app/
theme-name/bower_components/
theme-name/fonts/
theme-name/grunt/
theme-name/images-min/
theme-name/node_modules/
SMAJS
Scalable and Modular
Architecture for JavaScript
(SMACSS for JS)
The Application Core
The Mediator Pattern
JS Directory Structure
Just like with our scss partials our js needs a logical structure.
JS Sub-Directory Structure
Performance
Keep performance in the forefront of your decisions.
Site
Performance
Developer Performance
Site Performance
Concat whenever possible
Optimize
all things
“Nearly half of web users expect a site to load in 2 seconds or less, and they tend to abandon a site that isn’t loaded within 3 seconds.”
- surveys done by Akamai and Gomez.com
Developer Performance
Automize
all things
Test & Lint
Don't make them think
Grunt/Gulp/task runner flavor of the week
'use strict';
module.exports = function(grunt) {
require('load-grunt-config')(grunt);
};
Gruntfile.js
Lives in the theme directory
Modularize All Things
Each task gets its own file then assigned to an alias in a yml/json/js file
Package Management
The idea here is that everything is self dependent; but, there are often times where we need a helpful plugin to speed up dev.
Repo size constraints
Keeps dependencies in one place
Version control
Keep it simple
Our task runner already depends on npm so why not use it to manage our theme dependencies as well.
Easy to implement
The creation of the dependency requires one command.
npm install [package] --ignore-scripts --save-dev
Using --ignore-scripts helps prevent malicious scripts from running during install.
NPM Unavailable?
If we have to commit dependencies maintain your team's directory patern
Anonymous Presentation Closure
// Revealing last slide group using a reveal pattern
var slides = (function (slide) {
// Next Slide
function nextSlide(slide) {
// Some dorky code that really is just saying almost done.
}
}(slide));
Resources
- Addy Osmani
- Ben Cherry
Stephane Bellity, Addy Osmani, Ates GoralJuan, Pablo Buritica, Joel Hooks, Dan Lynch, Robert Djurasaj, Peter Rudolfsen, Sindre Sorhus, Romain Dardour, Tony Narlock, Dustin Boston
A framework-agnostic, extensible architecture for decoupled and reusable components.
- Addy Osmani
- Ben Alman
- Khan Academy
Ryan McVeigh
SCALABLE JAVASCRIPT DESIGN PATTERNS IN DRUPAL
By Ryan McVeigh
SCALABLE JAVASCRIPT DESIGN PATTERNS IN DRUPAL
- 4,018