views:

139

answers:

6

I'm starting to do a lot of jQuery programming. I'm finding that my code is becoming a little hard to maintain and is not all that readable. My javascript is being included on every page of our web application, even though it is only used on one page. This has the benefits of having the javascript cached before loading the page, but I'm worried about accidentally creating functions with identical names (created by other programmers on my team).

I'm new to javascript, so there may be some basics that I'm missing. What are some techniques that I can apply to the following code, as well as other code in the future to make my jQuery code more maintainable and easy to read?

<script type="text/javascript">

$(document).ready(function(){
    $('#registration-information .copy-button').click(copyField);
});

function copyField(e) {

    e.preventDefault();

    var $tr = $(this).closest('tr');
    var originalText = jQuery.trim($tr.find('.contact-data').text());
    var newText = jQuery.trim($tr.find('.registration-data').text());
    var $button = $tr.find('.copy-button');
    var $loadingIcon = $('<span class="loading-icon"></span>').hide().insertAfter($button);
    var $undoLink = $('<a class="undo-link" href="#">Undo</a>').hide().insertAfter($button);
    var field = $button.attr('id');

    $undoLink.click(function(e){
        e.preventDefault();
        undoCopyField($tr, originalText);
    });

    $button.hide();
    $loadingIcon.show();

    $.ajax({
        url: '/registrations/copy-field-to-contact',
        data: {
            id: '<?php echo $registration->ID ?>',
            field: field,
            data: newText
        },
        success: function(data){
            if (data.success) {
                $loadingIcon.hide();
                $tr.find('.contact-data').html(newText);
                $tr.find('td.form_field_label').removeClass('mismatched');
                $tr.effect('highlight', 1000, function(){
                    $undoLink.fadeIn();
                });
            } else {
                displayErrorMessage(data.error);
                $loadingIcon.hide();
                $button.show();
            }
        },
        error: function(){
            displayErrorMessage('Unknown reason');
            $loadingIcon.hide();
            $button.show();
        }
    });
}

function undoCopyField($tr, originalText) {

    var $button = $tr.find('.copy-button');
    var $loadingIcon = $tr.find('.loading-icon');
    var $undoLink = $tr.find('.undo-link');
    var field = $button.attr('id');

    $undoLink.hide();
    $loadingIcon.show();

    $.ajax({
        url: '/registrations/copy-field-to-contact',
        data: {
            id: '<?php echo $registration->ID ?>',
            field: field,
            data: originalText
        },
        success: function(data){
            if (data.success) {
                $undoLink.remove();
                $loadingIcon.hide();
                $tr.find('.contact-data').html(originalText);
                $tr.find('td.form_field_label').addClass('mismatched');
                $tr.effect('highlight', 1000, function(){
                    $tr.find('.copy-button').fadeIn();
                });
            } else {
                displayErrorMessage(data.error);
                $loadingIcon.hide();
                $undoLink.show();
            }
        },
        error: function(){
            displayErrorMessage('Unknown reason');
            $loadingIcon.hide();
            $undoLink.show();
        }
    });
}

function displayErrorMessage(message) {
    alert('Sorry, there was an error while trying to save your changes: ' + message);
}
</script>

Update: There are numerous sections of this code sample with chunks of code that are almost identical. Specifically the AJAX calls. Those two blocks are essentially the same, except for the actions that take place after the call has completed. I'd really like to figure out a way to DRY up those sections.

+3  A: 

Two tips:

  • Use namespaces for your code to avoid name conflicts. There are of course no real namespaces in Javascript, but you can fake them using objects.

`

var MyCode=MyCode||{
        copyField:function (e){
    },
        someOtherFunction:function(){
    }
};

    $(document).ready(function(){
        MyCode.copyField(...);
    });
  • Put your javascript code one or more separate files (one per namespace), and use third party libraries like combres to combine them into one file, minify them and take care of proper caching. It saves a lot of bandwidth and is cleaner than distributing all kinds of javascript functions across different pages.
Adrian Grigore
I don't know if the use of a ternary here really solves anything. It makes it so that a previously defined MyCode won't be overwritten, but that's far from a solution. If global namespaces like that are being overwritten, the problem is the programmer not the program.
BBonifield
@BBonifield: You are right, I put the ternary operator there to make sure that variables defined in the object are not overwritten. But what do you mean to suggest? It works fine for me, and there are no true namespaces in Javascript, so one way or another you will have to use objects if you want to fake namespaces.
Adrian Grigore
Adrian, the common way is to use the OR operator. As invar MyCode = MyCode || {}; This is much more readable and elegant, imo.
lark
@lark: Oh, ok. I've updated my post accordingly. Thanks!
Adrian Grigore
A: 

The only real readability issue I see here is that you could declare your variables without using the var each time by using a comma after each variable:

var $tr = $(this).closest('tr'),
    originalText = jQuery.trim($tr.find('.contact-data').text()),
    newText = jQuery.trim($tr.find('.registration-data').text());

I've always found this a bit easier to read then just a series of vars. To the eye, it looks like a code block that is started with var and ends when the indent returns.

Aside from that, it all looks good.

treeface
+1  A: 

One way to make your jQuery code cleaner and more maintainable is to break it into reusable jQuery plugins. This allows you to encapsulate related functionality in a single file. Each plugin is effectively a namespace so you will avoid function name collisions. You can also pass arguments to the plugin to customize the behaviour on a page by page or case by case basis.

There is a pretty good guide and template for writing plugins here

Simon Dyson
A: 

As a start...

Test drive the code using something like jsunit.

Create small well named classes, with small well named methods. The name of the class will describe it's responsibilities. The name of the method will describe it's responsibilities as well.

Refactor to remove duplication.

Limit the use of global scope.

Read Clean Code: A Handbook of Agile Software Craftsmanship [Paperback] Robert C. Martin (Editor)

jeffo
I've read it! (well...at least half of it). It's a great book!
Andrew
A: 

You can use YUI namespace model, create a top level object (namespace) lets var STW = {}; use this namespace function to create other classes or namespace

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

lets takes some example in your first file use STW.namespace("STW.namespace1"); STW.Namespace1.class1=function(){

//your code } in other files STW.namespace("STW.namespace1.namespace2"); STW.namespace1.namespace2.class2=function(){

//your code }

Shusl
A: 

The stuff in the $.ajax parens is just a single argument like in any c-based function.

{} is JS's literal representation of an object. So if both cases have the same stuff between {} you could do this:

    options = {
        url: '/registrations/copy-field-to-contact',
        data: {
            id: '<?php echo $registration->ID ?>',
            field: field,
            data: newText
        },
        success: function(data){
            if (data.success) {
                $loadingIcon.hide();
                $tr.find('.contact-data').html(newText);
                $tr.find('td.form_field_label').removeClass('mismatched');
                $tr.effect('highlight', 1000, function(){
                    $undoLink.fadeIn();
                });
            } else {
                displayErrorMessage(data.error);
                $loadingIcon.hide();
                $button.show();
            }
        },
        error: function(){
            displayErrorMessage('Unknown reason');
            $loadingIcon.hide();
            $button.show();
        }
    }

And then for both:

$.ajax(options);

Now if there's a variation in the options for your next ajax call:

Example #1 - just change the URL to google

options.url = "http://www.google.com";
$.ajax(options);

Example #2 - change id property of the data property (which is another object)

options.data.id = "newID";

A word of caution on JQuery's selector syntax. Grabbing directly by ID is ideal. Grabbing just by class can be really slow in older versions of IE (which have no native getByClassName methods so there's more interpreter-level looping going on). The ideal way to write a selector narrows down to the closest parent ID available first and if you can be explicit add the tag name to the follow-up class selector to narrow down further (getElementsByTagName is native and speedy).

So if we could rely on .copy-button being an input tag:

$('#registration-information input.copy-button')

or we could rely on it being an input or an anchor tag

$('#registration-information input.copy-button, #registration-information a.copy-button')

those might be considerably faster options in IEs 6 and maybe 7 if there's a ton of HTML elements inside #registration-information. I try to give IDs to all unique containers likely to stay that way such that I've always got IDs handy for more efficient JQuery selectors.

But whatever you do, avoid these:

$('.copy-button')

That can choke IE 6 even on a largish html doc if there's enough of it going around. If you plan on using the results of a selector more than once, save it to a var. It's so concise it's easy to forget that these can be fairly resource intensive operations in some cases.

Erik Reppen