Mar 122011
 

The moment you start doing real work with Ajax you will find that multiple page elements have to be updated from a single action.

The options that I found are:

  1. Write a massive heap of javascript with triggers and parameterised calls every where.
  2. Create a porcelain ajax framework using Grails includes.
  3. Write a javascript element update function with a nice interface and send JSON to it.

#1 Quickly does not scale.
#2 Seems very nice but is not a complete solution yet and has the disadvantage of multiple calls, one per element.
#3 Is the option further developed here.

The good news is that as of Grails-1.2 the JSON can now be built directly in the controller using the newer JSON builder:

render(contentType:"text/json") {
    updates = array {
        element([ mode: 'replace', target:"$params.target", content: g.render(template: 'create', model:model) ])
        element([ mode: 'execute', script: "alert('Hi');" ])
    }
}

The jQuery javascript can be simplified to:

// Apply updates to multiple page elements.
// @json JSON response object from an ajax request.
// @json.updates Array of element updates to apply.
// @element.mode The update mode: execute or replace, prepend, append.
// @element.script Script to execute, if execute mode.
// @element.target jQuery target selector, if update mode.
// @element.content Content to update target with, if update mode.
function applyElementUpdates(json) {
    var updates;
    var script;
 
    if(json.updates) {
        updates = json.updates;
        var element;
        var scripts = new Array();
 
        for(element in updates) {
            element = updates[element];
 
            switch(element.mode) {
                case 'execute':
                    scripts.push(element.script);
                    break;
                case 'replace':
                    jQuery(element.target).html(element.content);
                    break;
                case 'prepend':
                    jQuery(element.target).prepend(element.content);
                    break;
                case 'append':
                    jQuery(element.target).append(element.content);
                    break;
            }
        }
 
        // Run scripts.
        for(script in scripts) {
            script = scripts[script];
            eval(script);
        }
 
    } // if(json.updates)
} // applyElementUpdates

Usage Example

Controller:

class ExampleController {
 
    static allowedMethods = [ajaxCreate:'POST']
 
    // Cookie based authentication should apparently be protected by only accepting post.
    def ajaxCreate = {
        def book = new Book()
        book.properties = params
 
        if(someError) {
            params.errorMessage = g.message(code:"default.not.found", args:['Book'])
            render(contentType:"text/json", status: 403, template: "/shared/messages")
            return
        }
 
        // Success.
        def model = ['book': book]
        render(contentType:"text/json") {
            updates = array {
                element([ mode: 'replace', target:"#myDiv", content: g.render(template: 'create', model:model) ])
                element([ mode: 'append', target:"#anotherDiv", content: "Done" ])
                element([ mode: 'execute', script: "initFunction();" ])
            }
        }
    }
}

Javascript:

    // On success load target.
    function success(data, textStatus, jqXHR) {
        applyElementUpdates(data);
        // init()...
    }
 
    // On error show controller responseText or show default error.
    function error(jqXHR, textStatus, errorThrown) {
        if(jqXHR.status == 403 && jqXHR.responseText) {
            target.html(jqXHR.responseText);
        }
        else {
            target.html(errorIndication(jqXHR, textStatus, errorThrown).show());
        }
    }
 
    jQuery.ajax({
        url: actionUrl,
        data: params,
        type: 'POST',
        dataType: 'json', // Strict JSON parsing to javascript object.
        success: success,
        error: error
    });

Error Handling
So far as I can tell best practices are still developing in this area.
For example I have seen some recommendations that action errors and http errors should be separated. I could see the javascript updater interface extended to handle action errors (json.updates and json.errors) but for now the http error codes 403 and 503 allow clean branching in the success and error javascript.

Security
Firstly I’m still in the early stages of Ajax usage so this ain’t gospel just a pattern under development.
I would be especially interested in constructive comments about ensuring and improving security. But please no eval is evil comments (see link) all technology can be used for both good and bad.

More on eval usage:
http://javascriptweblog.wordpress.com/2010/04/19/how-evil-is-eval/
More ajax patterns:
http://ajaxpatterns.org/

[suffusion-the-author display='first-name'] [suffusion-the-author display='last-name']:


Thank you for visiting.
Do look around.
Do leave a comment.

  2 Responses to “Updating multiple page elements with Grails, Ajax and jQuery”

  1. Just a quick note of thanks for posting this. I was able to use this pretty much as is and it “just worked”. Thank you so much, you saved me a ton of time.

    Guy

  2. Thank you for the comment sir, glad the javascript was helpful and worked.

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)