3

I frequently create (and inherit) small to medium websites where I have the following sort of code in a single file (normally named global.js or application.js or projectname.js).

If functions get big, I normally put them in a seperate file, and call them at the bottom of the file below in the $(document).ready() section.

If I have a few functions that are unique to certain pages, I normally have another switch statement for the body class inside the $(document).ready() section.

How could I restructure this code to make it more maintainable?

Note: I am less interested in the functions innards, more so the structure, and how different types of functions should be dealt with.

I've also posted the code here - http://pastie.org/999932 in case it makes it any easier

var ProjectNameEnvironment = {};


function someFunctionUniqueToTheHomepageNotWorthMakingConfigurable () {   
    $('.foo').hide();
    $('.bar').click(function(){
        $('.foo').show();
    });

}

function functionThatIsWorthMakingConfigurable(config) {
    var foo = config.foo || 700;
    var bar = 200;
    return foo * bar;
}


function globallyRequiredJqueryPluginTrigger (tooltip_string) {

    var tooltipTrigger = $(tooltip_string); 

    tooltipTrigger.tooltip({ 
        showURL: false
            ... 
    });
}

function minorUtilityOneLiner (selector) {
    $(selector).find('li:even').not('li ul li').addClass('even');    
}

var Lightbox = {};

Lightbox.setup = function(){
    $('li#foo a').attr('href','#alpha');
    $('li#bar a').attr('href','#beta');
}


Lightbox.init = function (config){

    if (typeof $.fn.fancybox =='function') {

        Lightbox.setup();

        var fade_in_speed = config.fade_in_speed || 1000;
        var frame_height = config.frame_height || 1700;

        $(config.selector).fancybox({ 
            frameHeight : frame_height,
            callbackOnShow: function() { 
                var content_to_load = config.content_to_load;
                    ...
            },
            callbackOnClose : function(){
                $('body').height($('body').height());
            }
        });

    } 

    else { 
        if (ProjectNameEnvironment.debug) {
            alert('the fancybox plugin has not been loaded'); 
        }
    }

}

// ---------- order of execution -----------

$(document).ready(function () {

    urls = urlConfig();

    (function globalFunctions() {

        $('.tooltip-trigger').each(function(){
            globallyRequiredJqueryPluginTrigger(this);
        });

        minorUtilityOneLiner('ul.foo')

        Lightbox.init({
            selector : 'a#a-lightbox-trigger-js',
                ...
        });

        Lightbox.init({
            selector : 'a#another-lightbox-trigger-js',
                ...
        });

    })();

    if ( $('body').attr('id') == 'home-page' ) {

        (function homeFunctions() {
             someFunctionUniqueToTheHomepageNotWorthMakingConfigurable ();
         })();

     }

});
Dr. Frankenstein
  • 4,634
  • 7
  • 33
  • 48

3 Answers3

2

The 'secure' and 'high performance' parts of your questions are so big that I'm not going to try and address them here :), but I will address maintainability.

This is the approach I use. I'm sure it could be improved - as can anything - but it's worked well for me so far:

1) use namespaces. This is what I use: Is it possible to create a namespace in jQuery?

2) group your functions in widgets by area of use (home page vs admin page, etc.) or by their purpose (string functions, animation, etc.) - whichever works better for you. In each widget, have an init() function to initialise all the required functionality.

3) have a little helper function initialise the widgets for you to be used on the page.

Any logic that you have that limits running a particular function to the home page, or for IE6 users only, etc., as long as it's a one-off thing, can be hidden away inside the relevant functions and leave the initialisation of the whole thing nice and clean.

Example (including the namespacing function pinched from the question linked to above):

jQuery.namespace = function() {
var a=arguments, o=null, i, j, d;
for (i=0; i<a.length; i=i+1) {
    d=a[i].split(".");
    o=window;
    for (j=0; j<d.length; j=j+1) {
        o[d[j]]=o[d[j]] || {};
        o=o[d[j]];
    }
}
return o;
};

// set up your namespace
$.namespace( 'jQuery.myProject' );

// init helper
$.myProject.init = function(widget) {
    $(document).ready(widget.init);
}

/**
* Sitewide scripts
*/
$.myProject.Sitewide = {
    init: function()
    {
        $.myProject.Sitewide.doSomething();
        $.myProject.Sitewide.doSomethingElse();    
    },

    doSomething: function()
    {
        // here goes your code  
    },

    doSomethingElse: function()
    {   
        // you get the idea
    }
};

// more widgets here
// ...

// final step - call the init() helper
$.myProject.init($.myProject.Sitewide);
// init more widgets:
// $.myProject.init($.myProject.AnotherWidget);

Hope this helps.

Community
  • 1
  • 1
Rowlf
  • 1,752
  • 1
  • 13
  • 15
1

There are so many ways of doing this, here is my take:

Maintain your reusable methods and helpers in a repository. We tend to use a global singleton with our company name. Here is a simple example, called company.js:

(function($) {

window.Company = {
    makeRed: function(elem) {
        return $(elem).css('color','red');
    },
    parseAnchors: function() {
        $('a[href="#"]').click(function(e) {
            e.preventDefault();
        });
    }
}

})(jQuery);

You can add larger classes in a separate file, like a modal called company.modal.js:

(function($) {

if (typeof Company == 'undefined') {
    return; // needs to be included after company.js
}

window.Company.modal = {
    open: function() {
        // some opening methods
    },
    close: function() {
        // some closing functions
    }
};

})(jQuery);

Now, for each project create a project.js file in a similar matter. We usually add a views object in here that contains different methods for different views. These are called by parsing the body class in the init method:

(function($) {

window.Project = {
    init: function() {
        this.views._global.call(this);
        var classNames = document.body.className.split(" ");
        for (var i=0; classNames[i]; i++) {
            if (this.views[classNames[i]]) {
                this.views[classNames[i]].call(this);
            }
        }
     },
     views: {
         _global: function() {
             // all views
             Company.parseAnchors();
             $('a.modal').click(function() {
                 Company.modal.open();
             });
         },
         home: function() {
             // called if body classname is 'home'
             Project.addHomeStuff();
             Company.makeRed('#sale');
         }
    },
    addHomeStuff: function() {
        // some stuff to do at home
    }
};

})(jQuery);

Now, we usually call Project.init(); on each template before closing the body tag for a quick init. No need for the domReady handler in 99.9% of the projects you will be working on. If you need it for some dynamic script injection, add it later in the Project code.

This way we have two globals, Company and Project. Each can be used later on when building the code base for each project. If you find yourself creating similar methods for many projects, move them over to the Company repo.

Hope you find this useful!

David Hellsing
  • 106,495
  • 44
  • 176
  • 212
  • Splitting these into two separate namespaces like that seems like a really good thing to do. I like how you've implemented this too. I'm personally not so keen on the "call `Project.init();` on each template before closing the body tag", I prefer @Rowlf's instantiation inside the scripts, but I can see how it could be easy to manage that way. Cheers! – Dr. Frankenstein Jul 26 '10 at 23:06
0

This may not be applicable in your situation, but when I work on sites where I have templates and a template language, I use a helper I've created like so (Python pseudo-example)

def include_js(scripts):
  hash = md5(''.join(scripts))
  if not file_exists("%s/%s.js" % (path_to_js, hash))
    new_script = ""
    for file in scripts:
      new_script += file.contents
    write_file(new_script, "%s/%s.js" % (path_to_js, hash))
  return '<script type="text/js" src="%s/%s.js"></script>' % (path_to_js, hash)

The way I use it is:

<head>
  {% include_js(['lightbox', 'global', 'selectors', 'tooltips']) %}
</head>

etc.

So what's going on here is,

  1. Each of these sections is in its own js file.
  2. I specify to the helper which of those sets of Javascript utilities I need for a given template.
  3. The helper hashes the names of all the required scripts into a filename, then checks to see if that file exists. If it doesn't it opens each of those files individually and concatenates them into one file.
  4. The helper emits a <script> tag that includes the new script made from the concatenation of all those files.

This way I have the benefit of separating my Javascript (and also CSS, I do the same thing) into separate files, but I still only require the browser to make one request to get all the required scripts.

Jesse Dhillon
  • 7,841
  • 1
  • 34
  • 34