Decoupling JS from the DOM

February 20, 2014 Alex Welch

There has been a big shift in the last few years toward javascript frameworks that dictate how we deal with the DOM. In this series I want to highlight the value in “progressive enhancement” style patterns when it comes to interacting with the DOM. Don’t get me wrong, I’m not saying the new ways are bad. There are still some takeaways from the, older, progressive enhancement style approach.

I have always appreciated the separation of concerns afforded by the “progressive enhancement” approach. Separating structure, behavior, and presentation makes sense from an organizational standpoint. The “progressive enhancement” approach of DOM scripting lends itself well to this. Ideally, the CSS and JS tend to have knowledge of the markup, but the markup does not have knowledge of the JS or CSS.

In the spirit of maintaining the separation I try to make my Javascript widgets HTML agnostic. For some this is overkill, but I want to underscore this approach as I have found it useful for many years. To achieve this just inject all the selectors needed by the component. jQuery plugins, Backbone views, etc. do this already for the base element. Taking that approach a step further is simple, just pass in the additional selectors needed for the widget to do its job.

Example:


  var $form = $('form');
  var selectableList = new $.SelectableList($form, {
    $submitElement: $form.find('input[type="submit"]'),
    $titleElement: $form.find('h2'),
    checkboxSelector: 'input[type="checkbox"]',
    $checkboxElements: $form.find('input[type="checkbox"]')
  });

This buys the developer a couple things:

1. It forces diligence around how many selectors, DOM elements, etc. are used by the widget.
2. It allows a component to be shared across interfaces with different markup.
3. It makes testing easier as it allows the simplest possible HTML fixture data.

Below is an example implementation and test file (there is a dependency on jQuery):


(function($){
  $.SelectableList = function(el, options){
    var base = this;

    base.$el = $(el);
    base.el = el;
    base.options = options;

    base.init = function(){
      base.toggleSubmit(false);
      base.bindEvents();
    };

    base.bindEvents = function() {
      base.$el.on('change', base.options.checkboxSelector, base.handleCheck);
    };

    base.handleCheck = function(e) {
      var count = base.getSelectedItemsCount()
      base.updateTitle(count);
      base.toggleSubmit(count);
    };

    base.toggleSubmit = function(enabled) {
      base.options.$submitElement.prop('disabled', !enabled);
    };

    base.updateTitle = function(count) {
      base.options.$titleElement.html(count + ' members selected')
    };

    base.getSelectedItemsCount = function() {
      return base.options.$checkboxElements.filter(':checked').length;
    };
  };
})(jQuery);

describe("$.SelectableList", function() {
  var selectableList,
      $titleElement,
      $submitInput

  beforeEach(function() {
    var fixtureHTML = "<form id='test_form'>" +
      "<h2>Select Tribe members</h2>" +
      "<ul>" +
        "<li><input type='checkbox' />Phife</li>" +
        "<li><input type='checkbox' />Q-tip</li>" +
        "<li><input type='checkbox' />Ali</li>" +
        "<li><input type='checkbox' />Jarobi</li>" +
      "</ul>" +
      "<input type='submit' value='Delete selected tribe members' />"
    "</form>";

    $('body').append(fixtureHTML);

    $titleElement = $('#test_form h2');
    $submitInput = $('input[type="submit"]')

    var $form = $('form#test_form');
    selectableList = new $.SelectableList($form, {
      $titleElement: $form.find('h2'),
      $submitElement: $form.find('input[type="submit"]'),
      $checkboxElements: $form.find('input[type="checkbox"]'),
      checkboxSelector: 'input[type="checkbox"]'
    });
  });

  afterEach(function() {
    $('#test_form').remove();
  });

  describe('#init', function() {
    beforeEach(function() {
      selectableList.init();
    });

    it('disables the submit element', function() {
      expect($submitInput.is(':disabled')).toBe(true)
    });
  });

  describe('when more than one item has been selected', function() {
    beforeEach(function() {
      selectableList.init();
      $('ul li:first-child input').prop('checked', true).change();
      $('ul li:last-child input').prop('checked', true).change();
    });

    it('updates the title', function() {
      expect($titleElement.text()).toEqual('2 members selected');
    });

    it('enables the submit element', function() {
      expect($submitInput.is(':disabled')).toEqual(false);
    });
  });
});

View on github.

About the Author

Biography

More Content by Alex Welch
Previous
Using the Boogie Board Sync 9.7
Using the Boogie Board Sync 9.7

I just got my hands on a Boogie Board Sync 9.7. I asked our IT department if we could evaluate one because ...

Next
Integration Testing in a Spring Project
Integration Testing in a Spring Project

We love testing here at Pivotal Labs. Every pair on every project at some point asks the question, “So how ...

Enter curious. Exit smarter.

Learn More