Skip to content

Instantly share code, notes, and snippets.

@btpoe
Last active February 6, 2016 22:04
Show Gist options
  • Save btpoe/de6fd193b7dc822ef3a6 to your computer and use it in GitHub Desktop.
Save btpoe/de6fd193b7dc822ef3a6 to your computer and use it in GitHub Desktop.
// # 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