Scalable JavaScript Design Patterns in Drupal

Ryan McVeigh

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. 

Recycling symbol

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

  • 3,884