Search code examples
javascriptjquerydesign-patternsbackbone.jsrequirejs

Best way to organize jQuery/JavaScript code (2013)


The Problem

This answer has been answered before but are old and not up to date. I have over 2000 lines of code in a single file, and as we all know this is bad practice, especially when i'm looking through code or adding new features. I want to better organize my code, for now and for the future.

I should mention that I'm building a tool (not a simple website) with lots of buttons, UI elements, drag, drops, action listeners/handlers and function in the global scope where several listeners may use the same function.

Example code

$('#button1').on('click', function(e){
    // Determined action.
    update_html();
});

... // Around 75 more of this

function update_html(){ .... }

...

More example code

Conclusion

I really need to organize this code for best use and not to repeat myself and be able to add new features and update old ones. I will be working on this by myself. Some selectors can be 100 lines of code others are 1. I have looked a bit at require.js and found it kinda repetitive, and actually writing more code than needed . I'm open to any possible solution that fit this criteria and link to resource / examples are always a plus.

Thanks.


Solution

  • I'll go over some simple things that may, or may not, help you. Some might be obvious, some might be extremely arcane.

    Step 1: Compartmentalize your code

    Separating your code into multiple, modular units is a very good first step. Round up what works "together" and put them in their own little encased unit. don't worry about the format for now, keep it inline. The structure is a later point.

    So, suppose you have a page like this:

    enter image description here

    It would make sense to compartmentalize so that all the header-related event handlers/binders are in there, for ease of maintenance (and not having to sift through 1000 lines).

    You can then use a tool such as Grunt to re-build your JS back to a single unit.

    Step 1a: Dependency management

    Use a library such as RequireJS or CommonJS to implement something called AMD. Asynchronous Module Loading allows you to explicitely state what your code depends on, which then allows you to offload the library-calling to the code. You can just literally say "This needs jQuery" and the AMD will load it, and execute your code when jQuery is available.

    This also has a hidden gem: the library loading will be done the second the DOM is ready, not before. This no longer halts load-up of your page!

    Step 2: Modularize

    See the wireframe? I have two ad units. They'll most likely have shared event listeners.

    Your task in this step is to identify the points of repetition in your code and to try to synthesise all this into modules. Modules, right now, will encompass everything. We'll split stuff as we go along.

    The whole idea of this step is to go from step 1 and delete all the copy-pastas, to replace them with units that are loosely coupled. So, instead of having:

    ad_unit1.js

     $("#au1").click(function() { ... });
    

    ad_unit2.js

     $("#au2").click(function() { ... });
    

    I will have:

    ad_unit.js:

     var AdUnit = function(elem) {
         this.element = elem || new jQuery();
     }
     AdUnit.prototype.bindEvents = function() {
         ... Events go here
     }
    

    page.js:

     var AUs = new AdUnit($("#au1,#au2"));
     AUs.bindEvents();
    

    Which allows you to compartmentalize between your events and your markup in addition to getting rid of repetition. This is a pretty decent step and we'll extend this further later on.

    Step 3: Pick a framework!

    If you'd like to modularize and reduce repetitions even further, there are a bunch of awesome frameworks around that implement MVC (Model - View - Controller) approaches. My favourite is Backbone/Spine, however, there's also Angular, Yii, ... The list goes on.

    A Model represents your data.

    A View represents your mark-up and all the events associated to it

    A Controller represents your business logic - in other words, the controller tells the page what views to load and what models to use.

    This will be a significant learning step, but the prize is worth it: it favours clean, modular code over spaghetti.

    There are plenty of other things you can do, those are just guidelines and ideas.

    Code-specific changes

    Here are some specific improvements to your code:

     $('.new_layer').click(function(){
    
        dialog("Create new layer","Enter your layer name","_input", {
    
                'OK' : function(){
    
                        var reply = $('.dialog_input').val();
    
                        if( reply != null && reply != "" ){
    
                                var name = "ln_"+reply.split(' ').join('_');
                                var parent = "";
    
                                if(selected_folder != "" ){
                                parent = selected_folder+" .content";
                                }
    
                                $R.find(".layer").clone()
                                .addClass(name).html(reply)
                                .appendTo("#layer_groups "+parent);
    
                                $R.find(".layers_group").clone()
                                .addClass(name).appendTo('#canvas '+selected_folder);
    
                }
    
            }
    
        });
     });
    

    This is better written as:

    $("body").on("click",".new_layer", function() {
        dialog("Create new layer", "Enter your layer name", "_input", {
             OK: function() {
                 // There must be a way to get the input from here using this, if it is a standard library. If you wrote your own, make the value retrievable using something other than a class selector (horrible performance + scoping +multiple instance issues)
    
                 // This is where the view comes into play. Instead of cloning, bind the rendering into a JS prototype, and instantiate it. It means that you only have to modify stuff in one place, you don't risk cloning events with it, and you can test your Layer stand-alone
                 var newLayer = new Layer();
                 newLayer
                   .setName(name)
                   .bindToGroup(parent);
              }
         });
    });
    

    Earlier in your code:

    window.Layer = function() {
        this.instance = $("<div>");
        // Markup generated here
    };
    window.Layer.prototype = {
       setName: function(newName) {
       },
       bindToGroup: function(parentNode) {
       }
    }
    

    Suddenly, you have a way to create a standard layer from anywhere in your code without copy pasting. You're doing this in five different places. I've just saved you five copy-pastes.

    One more:

    // Ruleset wrapper for actions

    var PageElements = function(ruleSet) {
    ruleSet = ruleSet || [];
    this.rules = [];
    for (var i = 0; i < ruleSet.length; i++) {
        if (ruleSet[i].target && ruleSet[i].action) {
            this.rules.push(ruleSet[i]);
        }
    }
    }
    PageElements.prototype.run = function(elem) {
    for (var i = 0; i < this.rules.length; i++) {
        this.rules[i].action.apply(elem.find(this.rules.target));
    }
    }
    
    var GlobalRules = new PageElements([
    {
        "target": ".draggable",
        "action": function() { this.draggable({
            cancel: "div#scrolling, .content",
            containment: "document"
            });
        }
    },
    {
        "target" :".resizable",
        "action": function() {
            this.resizable({
                handles: "all",
                zIndex: 0,
                containment: "document"
            });
        }
    }
    
    ]);
    
    GlobalRules.run($("body"));
    
    // If you need to add elements later on, you can just call GlobalRules.run(yourNewElement);
    

    This is a very potent way to register rules if you have events that are not standard, or creation events. This is also seriously kick-ass when combined with a pub/sub notification system and when bound to an event you fire whenever you create elements. Fire'n'forget modular event binding!