Skip to content

Instantly share code, notes, and snippets.

@btpoe
Last active February 6, 2016 22:04
Show Gist options
  • Save btpoe/55470d6b4b8bfca56e8f to your computer and use it in GitHub Desktop.
Save btpoe/55470d6b4b8bfca56e8f to your computer and use it in GitHub Desktop.
// # Anatomy of an ES2015 Javascript Module
// *for older modules, see:* [ES5 Javascript Module](https://gist.github.com/btpoe/de6fd193b7dc822ef3a6)
// ## Module Defaults
// Important to note: These are shared across all instances and will only be calculated once.
// Export the defaults so that other modules can read them if necessary.
export const DEFAULTS = {
param: 'default_value'
};
export default class ModuleName {
// ## The Constructor
// First, define a constructor function. The goal is to keep this function as lightweight as
// possible. For the first argument, we're going to receive an HTMLElement. This element is
// critical because it is essentially the key to this module instance. The second argument is all
// of the developer defined options for this particular instance. For any option used, we should
// declare a default (seen below) to prevent the module from breaking if certain options are not
// provided.
constructor(node, options) {
// First, we check if an instance of this module has been bound to this HTMLElement previously.
// Note the use of an underscore to avoid conflicts with potential html attributes.
var instance = $.data(node, '_moduleName');
// We're not only going to check if something has been stored here, but ensure that it is an
// instance of this module.
if (instance instanceof ModuleName) {
// If we reach this line, it means that, at an earlier point in time, we already run
// through the process of initializing this module on this element. That means we are going
// to stop the process of creating a new instance, and return the original instance. We are
// also going to pass the options argument along to the original instance in the event that
// the developer wanted to update those.
return instance.setOptions(options);
}
// If we are here, it means this HTMLElement has not yet been given an instance of this module
// and we need to begin the process of creating a new module instance. We're going to reuse
// this variable so we don't have to write "var" another time. Using a variable instead of
// "this" will also let us compact our code by a few characters once we minifiy.
instance = this;
// Inevitably, we are going to need to use certain properties throught the module
// (ie: HTMLElements that are used for user interaction, instance's current state, etc.).
// Within these properties there are 2 main types of properties you'll be using: Ones you want
// to give the developer access to and ones you don't. Properties that are modified by the
// developer are scoped into an object to prevent developer defined properties from bleeding
// into the top level namespace of the module. We'll use the property "opts" as a namespace for
// developer defined options.
// The first thing we pass into $.extend is an empty object so that the developer defined
// options doesn't modify the module defaults. Any non developer defined options will inherit
// the default value of said option.
instance.opts = $.extend({}, DEFAULTS, options);
// After assigning the developer options, we're going to add the modules "protected"
// properties. The advantage of declaring these after the developer defined options is that, if
// necessary, we have those options available to decide how and what we are going assign to
// these properties.
//
// A note about these "protected" properties: Javascript doesn't have truly protected
// properties. We are going to assume that users and developers don't have access to modify
// these values, but the fact is, they can very easily crack open the DevTools and read/write
// to these properties, so don't store sensitive data here and don't assume you can protect the
// server from anything that comes from this module. You can (and should) run checks
// client-side to see if you should send data to the server, but you always must run those same
// checks server-side to be absolutely sure nothing malicious is happening.
instance.node = node;
// ... more property assignment
// We're going to break these out of the constructor function. Mostly for organization. It's
// worth mentioning that this is not a method of the instance (which is why we must pass a
// reference of the instance to the function). This prevents a developer from accidentally
// (or purposely) calling "bindEvents" twice on an instance.
bindEvents(instance);
// This is where the module comes full circle. Now that we are done initializing the module, we
// store a reference of the instance on the original HTMLElement and return it.
$.data(node, '_moduleName', instance);
return instance;
}
// ## Module Instance Methods
// These methods will be available to each instance. The "this" reference is the particular
// instance calling the method. Unless returning a requested value, return the instance to allow
// method chaining.
//
// instance.setOptions();
// instance.render();
//
// becomes:
//
// instance.setOptions().render();
//
/**
* Update instance options.
*
* @param {object|null} options
* @return ModuleName
*/
setOptions: function(options) {
$.extend(this.opts, options);
return this;
}
}
// ## Binding Events
// As you can see in the module constructor, this function will only be invoked the first time this
// particular instance is kicked off. The advantage of doing this is to ensure that events captured
// are only fired once. The advantage of breaking these event bindings out of the constructor is
// partly for organization and also to help keep the constructor function as lean as possible.
function bindEvents(instance) {
// Prefix functions here with "event_". Creating a variable for each function instead of an
// object of functions allows for better minification.
var event_click = function(e) {
e.preventDefault();
console.count(instance.opts.param);
};
// Instead of relying on jQuery event namespaces to avoid event collisions with other modules,
// do not programatically trigger events. Instead, call on methods directly.
// Reserve event listeners for user engagement.
instance.node.addEventListener('click', event_click, false);
}
// ### init
// This public method allows for programatically creating new instances of the module based on a query selector.
export function init(selector, options) {
var nodes = document.querySelectorAll(selector);
var instances = [];
for (var i = 0, l = nodes.length; i < l; i++) {
instances.push(new ModuleName(nodes[i], options));
}
return instances;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment