On many Rails projects, the application.js file can quickly go from a simple manifest (if you’re using the asset pipeline) to a gigantic unmaintainable mess of objects, functions, and on DOM ready initializations. There are many components of your site that need more interactive front-end functionality, but adding and maintaining them soon amounts to hitting cmd + f
and praying.
On my first software development project at MojoTech we needed some front-end functionality that was unique to certain page components. Most of the components were present on a single page, though some could be on multiple pages. Since the bulk of each component’s functionality was not reusable, it seemed that there wouldn’t be much benefit in making a jQuery plugin. In order to keep the code clean and not pollute the public namespace, we created an encapsulated module in its own js file that contained all the setup, event listeners, and function handlers for each component. This module was then initiated with the parent element the code was designed for on DOM ready. All these files would get included by the asset pipeline so there was no need to add the js include tags for each one.
All our modules had the same basic structure that looked like:
var OurApp = OurApp || {};
// Unless specified by name, the Rails asset pipeline doesn't guarantee the order of files.
// All our modules would be defined within the OurApp object.
// The only public method our module has is its constructor.
OurApp.ModuleName = function (moduleParentDOMElement) {
// Module specific code here
}
$(function () {
new OurApp.ModuleName($('.our-dom-element'));
});
Because we didn’t retain a reference to the object created, none of these modules had any public methods beyond their constructor. This meant all interaction with other objects and events had to be done through custom events. We eventually encountered a problem with two modules listening and responding to the same custom event. There is a common filtering element that appears at the top of two pages. When the user makes a filter choice, an event is sent to the window and the element containing the items being filtered listens and responds. The Rails asset pipeline concatenates all the project’s JavaScript together which caused two modules to be created which listen and respond to the same custom event. However, only one of their corresponding DOM elements was on the page. To fix this we added the following guard statement at the top of our modules.
OurApp.ModuleName = function (jqueryWrappedDOMElement) {
if (jqueryWrappedDOMElement.length == 0) return;
// Module specific code here
}
It seems that in the future, there will be more need for custom modules for specific groupings of HTML components on a page. These components may need to function in such a way that their JavaScript code is not completely reusable. As I was working on this project, I began to wonder what is the best way of encapsulating this code. Besides the method we went with, I’ve thought of a couple of other options. As with everything, there are strengths, weaknesses, and trade-offs to each.
One way would be to create these self-contained modules, but instead of initializing it on DOM ready, put a self-executing function inline after the group of HTML components. This method avoids the return if
guard clause at the beginning of each module but does mix HTML and JS and many people would like to avoid that.
Another option is to finely tune the asset pipeline and only include certain JavaScript files, as each module should be in its own file regardless of the pages on which they will be used. This method doesn’t mix HTML and JS and eliminates the need for the guard clause. This makes using the asset pipeline a little more difficult and can reduce the benefit your app gets from having a single minified JavaScript file.
While I’ve mentioned that a jQuery plugin would lose some value since these are separate elements with specific behavior, plugins also have the additional benefit of being encapsulated. The last option I investigated was going ahead with creating a typical jQuery plugin. Since it is incredibly useful and common to chain methods in jQuery, most plugins have a line in their initializer similar to the following:
return this.each(function() {};);
This is beneficial to our usage because now, when all the JavaScript files are included in the asset pipeline, all the modules can still be created on DOM ready. As the selector the plugin is being called on is an empty array, the code that can conflict will never be run on pages where the element is not present.