Last active
February 6, 2016 22:04
-
-
Save btpoe/de6fd193b7dc822ef3a6 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 a Javascript Module | |
// *for newer modules, see:* [ES2015 Javascript Module](https://gist.github.com/btpoe/55470d6b4b8bfca56e8f) | |
// ## 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. | |
function ModuleName(dom, 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(dom, '_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({}, ModuleName.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.element = $(dom); | |
// ... more elements | |
// 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(dom, '_moduleName', instance); | |
return instance; | |
} | |
// ## Module Defaults | |
// Important to note: These are shared across all instances and will only be calculated once. | |
ModuleName.defaults = { | |
param: 'default_value' | |
}; | |
// ## 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(); | |
// | |
ModuleName.prototype = { | |
/** | |
* Update instance options. | |
* | |
* @param {object|null} options | |
* @return ModuleName | |
*/ | |
setOptions: function(options) { | |
var self = this; | |
$.extend(self.opts, options); | |
return self; | |
} | |
}; | |
// ## 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); | |
}; | |
// Add a namespace to each event capture. This will allow you to trigger events only for this | |
// module and avoid the side effects if this element has another module instance bound to it. | |
// It also will allow unbinding events without losing events for other modules. | |
instance.wrap.on('click.moduleName', event_click); | |
} | |
// ## Exporting | |
// The following two sections are optional, but at least one should be applied, otherwise, the | |
// module would be unreachable. | |
// ### jQuery | |
// The first option to make the module publicaly accessible is as a jQuery plugin. | |
// Any function added to $.fn will become available to all jQuery elements. | |
$.fn.module = function(options) { | |
// inside a jQuery plugin, "this" is a reference to the collection of jQuery elements | |
return this.each(function() { | |
// inside jQuery's "each" method, "this" is the value the iterator is currently selecting. | |
// In this case, an HTMLElement. | |
return new ModuleName(this, options) | |
}) | |
}; | |
// ## Browserify (and NodeJS) | |
// module.exports will allow you to require this module in another using either NodeJS | |
// (server-side) or Browserify (client-side). This is great for dependency management, extending | |
// modules, creating complex adapters, and just for general organization. | |
module.exports = ModuleName; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment