Can You “Over Organize” JavaScript?

Avatar of Chris Coyier
Chris Coyier on (Updated on )

There’s no question that on sites with lots of JavaScript, you need a strict organizational pattern to keep everything making sense and the code approachable. I’ve mentioned in the past I like grouping things into individual files each containing a functionality-specific object literal. Taking things a wee bit further, we can be strict about this pattern and make sure we group together all sectors in one place, all “init” functions in one place, all event binding in once place, and have the rest be little well-named mini functions that do very specific things.

I wonder though, is this too organized?

I have little doubt that on a large heavy-JavaScript site that strict organization like this is of huge benefit. But on smaller sites perhaps not so much.

I recently needed to fix up some JavaScript here on CSS-Tricks. Since I’ve been writing so much JavaScript in the object literal pattern I thought I’d convert it over to my regular format. CSS-Tricks is not a JavaScript heavy site. The vast majority of the JS is in one global.js file. I saved the JavaScript in its “before” and “after” states so we could take a look and discuss.

The “Before”

The dependencies are up top. They get minified/concatenated into the final global.js file. The rest of the script is wrapped in an IIFE so that variables aren’t global. You can just kinda read it top-to-bottom as it’s just section after section of functionality. All the code that deals with that functionality is all together in those blocks, separated only by space and a brief comment.

// @codekit-prepend "jquery.fitvids.js"
// @codekit-prepend "placeholder.js"
// @codekit-append "prism.js"

// Protect global namespace
(function($) {


  // Make videos fluid width
  $("article, .photo-grid, .single-video-wrapper, .gallery-grid .grid-5-6").fitVids({
    customSelector: "video"
  });



  // IE 9 placeholders
  $('input, textarea').placeholder();



  // Search stuff
  var openSearch = $(".open-search, #x-search, #refine-search");
  var body = $("body");

  function toggleSearch() {
    if (body.hasClass("show-nav")) {
      $("body").toggleClass("show-nav");
    } else {
      $(".search").toggleClass("open");
      $(".open-search").toggle();
      $(".close-search").toggle();
    }
  }
  openSearch.on("click", function(e) {
    e.preventDefault();
    toggleSearch();
    setTimeout(function(){
      $(".search-field").focus();
    }, 100);
  });
  var searchParts = $(".search-parts > a:not(.x-search)");
  var searchForm = $("#search-form");
  searchParts.on("click", function() {
    var el = $(this);
    var newActionURL = el.data("url");
    searchForm.attr("action", newActionURL);
    searchParts.removeClass("active");
    el.addClass("active");
  });



  // Small screen navigation stuff
  $("#show-hide-navigation").on("click", function(e) {
    e.preventDefault();
    $("body").toggleClass("show-nav");
  });



  // Code highlighting stuff
  $("pre.lang-html, pre[rel=HTML]").find("code").addClass("language-markup");
  $("code.html, code.lang-html").removeClass().addClass("language-markup").parent().attr("rel", "HTML");

  $("code.javascript").removeClass().addClass("language-javascript").attr("rel", "JavaScript");
  $("pre[rel=JavaScript], pre.lang-js, pre[rel=jQuery], pre.JavaScript").attr("rel", "JavaScript").find("code").removeClass().addClass("language-javascript");

  $("pre[rel='CSS'], pre[rel='LESS'], pre[rel='Sass'], pre[rel='SASS'], pre[rel='SCSS']").find("code").removeClass().addClass("language-css");
  $("code.css, code.lang-css").removeClass().addClass("language-css").parent().attr("rel", "CSS");

  $("pre[rel=PHP]").attr("rel", "PHP").find("code").removeClass().addClass("language-javascript");
  $("code.php").removeClass().addClass("language-javascript").parent().attr("rel", "PHP");



  // Comments Stuff
  $(".comment.buried").on("click", function() {
    $(this).removeClass("buried");
  });
  $("#view-comments-button").on("click", function(e) {
    e.preventDefault();
    $("#comments").show();
    $(this).hide();
  });



  // Illustrator links
  var timer;
  var illustratorLink = $(".illustrator-link").hide();
  $(".deals-header, .almanac-title, .videos-title, .snippets-title, .demos-title, .gallery-header, .forums-title")
    .mouseenter(function() {
      timer = setTimeout(function() {
        illustratorLink.slideDown(200);
      }, 3000);
    })
    .mouseleave(function() {
      clearTimeout(timer);
    });
    

})(jQuery);

The “After”

Dependencies still up top, only I moved out the whole chunk regarding code highlighting into its open separate file, since it required no binding and created no variables.

All selectors are up top, grouped together. All init functions are together. All event binding is together. All “actions” are mini well-named functions.

// @codekit-prepend "jquery.fitvids.js"
// @codekit-prepend "placeholder.js"
// @codekit-prepend "highlighting-fixes.js"
// @codekit-append "prism.js"

var csstricks = {

  el: {

    body: $("body"),

    allInputs: $('input, textarea'),

    searchForm: $("#search-form"),
    searchOpeners: $(".open-search, #x-search, #refine-search"),
    searchSections: $(".search-parts > a:not(.x-search)"),
    searchField: $(".search-field"),
    search: $(".search"),
    openSearch: $(".open-search"),
    closeSearch: $(".close-search"),

    videoWrappers: $("article, .photo-grid, .single-video-wrapper, .gallery-grid .grid-5-6"),

    navToggle: $("#show-hide-navigation"),

    illustratorLink: $(".illustrator-link"),
    headerAreas: $(".deals-header, .almanac-title, .videos-title, .snippets-title, .demos-title, .gallery-header, .forums-title"),

    buriedComments: $(".comment.buried"),
    viewCommentsButton: $("#view-comments-button"),
    commentsArea: $("#comments")

  },

  timer: 0,

  init: function() {
    csstricks.bindUIActions();

    csstricks.makeVideosFluidWidth();
    csstricks.polyfillPlaceholders();

    csstricks.el.illustratorLink.hide();
  },

  bindUIActions: function() {
    csstricks.el.searchOpeners.on("click", csstricks.handleSearchClick);
    csstricks.el.searchSections.on("click", csstricks.handleSearchPartsClick);

    csstricks.el.navToggle.on("click", csstricks.mobileNavToggle);

    csstricks.el.headerAreas.on("mouseenter", csstricks.openIllustratorLinkArea);
    csstricks.el.headerAreas.on("mouseleave", csstricks.closeIllustratorLinkArea);

    csstricks.el.buriedComments.on("click", csstricks.revealComment);
    csstricks.el.viewCommentsButton.on("click", csstricks.revealCommentsArea);
  },

  makeVideosFluidWidth: function() {
    csstricks.el.videoWrappers.fitVids({
      customSelector: "video"
    });
  },

  polyfillPlaceholders: function() {
    csstricks.el.allInputs.placeholder();
  },

  handleSearchClick: function(event) {
    event.preventDefault();
    csstricks.toggleSearch();
    setTimeout(function() {
      csstricks.focusSearchField();
    }, 100);
  },

  mobileNavToggle: function(event) {
    event.preventDefault();
    csstricks.el.body.toggleClass("show-nav");
  },

  focusSearchField: function() {
    csstricks.el.searchField.focus();
  },

  handleSearchPartsClick: function(event) {
    var el = $(event.target);
    var newActionURL = el.data("url");
    csstricks.el.searchForm.attr("action", newActionURL);
    csstricks.el.searchSections.removeClass("active");
    el.addClass("active");
  },

  toggleSearch: function() {
    if (csstricks.el.body.hasClass("show-nav")) {
      csstricks.el.body.toggleClass("show-nav");
    } else {
      csstricks.el.search.toggleClass("open");
      csstricks.el.openSearch.toggle();
      csstricks.el.closeSearch.toggle();
    }
  },

  openIllustratorLinkArea: function() {
    csstricks.timer = setTimeout(function() {
      csstricks.el.illustratorLink.slideDown(200);
    }, 3000);
  },

  closeIllustratorLinkArea: function() {
    clearTimeout(csstricks.timer);
  },

  revealComment: function(event) {
    $(event.target).removeClass("buried");
  },

  revealCommentsArea: function(event) {
    event.preventDefault();
    csstricks.el.commentsArea.show();
    csstricks.el.viewCommentsButton.hide();
  }

};

csstricks.init();

How do they compare?

Regarding “before”, I like how you can read it top-to-bottom and everything related is grouped together. Since each of those groups only averages about 10 lines, it’s fairly readable. If things got more complicated, I’d worry the file would become harder to manage.

Regarding “after”, it’s a bit weird to see all the selectors grouped together. You have no idea why those elements are being selected – but you can see every element on the page that matters to this file and manage them independently of their functions. If you were totally unfamiliar with this file and trying to familiarize yourself with it, does reading the init and bindUIActions functions make that clear? Is it faster to read that than scan the entire “before” file? Is it confusing how both elements and functions both start with “csstricks.”?

“After” is also 25 lines longer despite moving a big chunk of code away to a separate file.

I don’t have all the answers. I’m still not 100% sure which I like better or if a hybrid approach would be better or worse.