« Back to home

Modularizing And Organizing Javascript Using Data Attributes

The most common way that people use to add javascript behavior to a page is using the $(document).ready() event. In this small post I'll describe a simple way to make these small behavior scripts more modular and reusable.

Multiple Ready Bindings

Binding all small javascript behavior to the ready event has the advantage of simplicity. It is an easy approach to master and put in practice. However, this solution has a few problems. Let's take a look at the following code to discuss the disadvantages of this solution:

$(document).ready(function() {
  var $cpfInput = $(".js-input-cpf");
  var $phoneInput = $(".js-input-phone");

  applyFormMasks();

  function applyFormMasks() {
    $cpfInput.mask("999.999.999-99");
    $phoneInput.mask("(99) 9999-9999");
  }
});

The code above is perfectly fine but it is not very reusable nor configurable. Let's discuss a way to overcome these problems.

Using Data Attributes

One solution that I learned and find very interesting is modularizing this kind of script using html data attributes. Let's take a look at the refactored code using this approach:

(function() {
  var FormMasks = function(element) {
    var $el = $(element);
    var $cpfInput = $el.find(".js-input-cpf");
    var $phoneInput = $el.find(".js-input-phone");

    $cpfInput.mask("999.999.999-99");
    $phoneInput.mask("(99) 9999-9999");
  }

  $(document).ready(function() {
    $('[data-form-masks]').each(function(_, element) {
      new FormMasks(element);
    });
  });
})();

In this code, the ready event will trigger a function that will search for all elements with a data attribute called form-masks and instantiate a new FormMask object to install the input masks. Our html would look something like this:

<div data-form-masks="true">  
  <input class="js-input-cpf" type="text" />
  <input class="js-input-phone" type="text" />
</div>  

As you can see, this code is a little more reusable but it requires the entry element (the div with the data attribute that triggers the FormMasks function) to have two inputs as child with the specific classes "js-input-cpf" and "js-input-phone". This html structure constraint limits our code modularization and reuse.

A nice way to make our script more independent of the html structure is attaching the data attribute directly to the input elements. This way, the data element value can work as a parameter for the javascript function:

<input class="input-cpf" data-form-mask="999.999.999-99" type="text" />  
<input class="input-phone" data-form-mask="(99) 9999-9999" type="text" />  
(function() {
  var FormMask = function(element) {
    var $el = $(element);
    var mask = $el.data('form-mask');
    $el.mask(mask);
  }

  $(document).ready(function() {
    $('[data-form-mask]').each(function(_, element) {
      new FormMask(element);
    });
  });
})();

This code is a lot more flexible because it doesn't expect a specific html structure and is more modular because we can reuse it for any type of mask that we want.

As our project and number of javascript modules grows this approach tends to hit a performance problem. Imagine that we have 100 scripts like FormMask. Each page load would trigger 100 jQuery searches, each one searching for his own data- attribute, even if this module is not being used on a specific page.

To make this more performatic we can separate the part of the script that instantiates the module from the module class itself.

The following code shows how a "component loader" works. Instead of searching for a specific module's data attribute, it searches for a generic data-component attribute that informs which component we want to load on a specific element.

var Components = {}

(function() {
  $(document).on('ready page:load', function() {
    var componentName = null;
    $('[data-component]').each(function(index, element) {
      componentName = $(element).data('component');
      new Components[componentName](element);
    });
  });
})();

Finally, our component function will be the same, but with a extra line to "register" it into our components object declared in the beginning of the component loader script.

(function() {
  var FormMask = function(element) {
    var $el = $(element);
    var mask = $el.data('form-mask');
    $el.mask(mask);
  }

  Components.FormMask = FormMask;
})();

And our html will change a little bit:

<input class="input-cpf" data-component="FormMask" data-form-mask="999.999.999-99" type="text" />  
<input class="input-phone" data-component="FormMask" data-form-mask="(99) 9999-9999" type="text" />  

Outro

Thats it! A simple solution for improving and organizing javascript. If you are reading this post and have a different opinion or a better solution I would love to hear about.
Finally, I want to thank Klismann for the example code and for reviewing this post and Mateus for teaching me this solution.