Last active
February 6, 2016 22:04
-
-
Save btpoe/55470d6b4b8bfca56e8f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// # 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