• Published on | Jun 04, 2012 | by FozzTexx

When jQuery Gives You Roadblocks...

As a web application developer I build a lot of web sites. Generally when getting input from a user a form is used. When I build the form I use standard "old school" techniques and after every form submit a new page is sent back to the browser. It works but it's not considered user friendly because there are too many pages involved and it steers the user away from whatever it they were on. This is especially true of posting comments.

On many sites adding AJAX functionality has meant that I had to code up special functions on the server to deal with processing the form differently than it would if the user was not using AJAX. I end up with what are essentially redundant functions. And due to treating the interaction differently than a regular form, the site often ends up rather fragile. If the Javascript doesn't work as expected the form doesn't get processed correctly. If different errors come back than the Javascript was expecting, the user may be dropped out of the modal panel and end up on a completely unstyled page. When fixing things on the server, I often have two or more functions that have to be updated in parallel, with many of them getting forgotten about, increasing the odds of something going wrong.

In building the comment system for Insentricity I decided I didn't want to do that. I wanted a generic reusable system that ran on the client side in Javascript that would take control of a standard form and submit it to the server. If Javascript was not enabled in the browser or somehow the user ended up getting onto the result page, the page would still appear as a normal page from the site, with the full styling. I wanted the Javascript to handle the form submit, look at the resulting page, and find the ID of the form and replace it on the current page. When form processing was successful, the Javascript would reload or replace the entire page. On the server side there would be no special code for handling an AJAX form.

While it seemed like a simple idea, there were many hurdles to overcome. Unfortunately I was not able to entirely achieve that goal.

jQuery callbacks provide no way to pass additional data. This presented a problem because after I call $.ajax() I need to know what ID I'm looking for. Thanks to Azhral I discovered that the workaround is to use closures and free variables. By using closures the scope of the form ID stays within my submit function and each time the submit function is called I get new instance of the variable to track the form IDs independently.

The next problem was that there is no way to see that the server returned a 303 or 302, so that I could detect a request to reload the page. This seems to be a problem built into the XMLHttpRequest system, and not a problem with jQuery itself. Working around the problem though required yet another compromise: altering the code on the server to deal with AJAX. I ended up making my routine add an extra field to the form which it would send and the server could see that it's doing AJAX and alter the status codes being sent back on page load. It's a minor change, but it limits the ability for me to apply the AJAX form reloading to any random form. I will have to modify the routines on the server for every form. I may see if I can incorporate this directly into ClearLake for the server side so that I don't have to deal with it constantly, but it's still ugly.

After deciding I would work around the 303/302 problem by changing the status code I thought I would make up my own codes to indicate something special to my AJAX routine. No go. Something decides that since the codes aren't in the table of standard codes they knew about, it will convert the code to 500 and tell my jQuery callback that it's a 500. Sigh.

Instead of opting to use my own codes I decided to repurpose 201 and 205. Status code 201 is being used to indicate success and that the comment can be injected into the existing page. Status code 205 means that the page needs to be completely reloaded because now the user is logged in and the old forms are invalid.

I thought I would be able to use the statusCode: parameter of $.ajax() to call my inject and reload routines, but again things don't work as documented. Apparently jQuery decides since the result is within "success" code range, it will only call my success: callback. I ended up making the success callback check the status code in the jqXHR object.

Because an injection requires me to look for a specific comment ID in addition to the form ID, I pass the comment ID as the anchor tag on the returned URL. I then need to fetch that page and look for that comment. Again jQuery breaks down because of the lack of passing additional data to a callback and I had to resort to using another global variable.

For the most part I achieved my goal, I didn't have to do anything more special on the server than send back custom status codes. I don't send need to parse special input data or send back special output data or a special sub page or JSON. The downside is that I do have to do some AJAX detection on the server. If it was possible for Javascript to detect the 303/302 then I wouldn't need to do anything special on the server.

function commentAjaxSubmit(eventObject) {
  var commentCallbackID, commentCommentID;
  var data = $(this.form).serialize();
  var button;
  

  function commentReplaceForm(data, formID)
  {
    var newObject = $(data).find(formID);


    $(formID).replaceWith(newObject);
  }
  
  function commentAjaxInject(data, textStatus, jqXHR)
  {
    var newComment;


    commentReplaceForm(data, commentCallbackID);
    newComment = $(data).find(commentCommentID);

    $(commentCallbackID).next('.comments-wrap').prepend(newComment);
  }
  
  function commentAjaxSuccess(data, textStatus, jqXHR)
  {
    if (jqXHR.status == 201) { /* Inject comment */
      var url = jqXHR.getResponseHeader('Location');
      var cPos;

  
      cPos = url.indexOf('#');
      commentCommentID = url.substring(cPos);

      $.ajax({
        type: 'GET',
            url: url,
            success: commentAjaxInject
            });
    }

    else if (jqXHR.status == 205) { /* Reload page */
      window.location.reload();
      window.location.replace(jqXHR.getResponseHeader('Location'));
    }
    else if (jqXHR.status == 200)
      commentReplaceForm(data, commentCallbackID);
  }

  if (this.name) {
    button = new Object;
    button[this.name] = this.value;
    data += '&' + $.param(button);
  }

  data += '&CLajax=1';

  {
    var submitting, commentInfo;


    submitting =
      '<div id="cl_messageBlock"><ul><li class=cl_infoText>Submitting...</ul></div>';
    commentInfo = $('.commentInfo', this.form);
    if ($('#cl_messageBlock', commentInfo).length)
      $('#cl_messageBlock', commentInfo).replaceWith(submitting);
    else
      commentInfo.prepend(submitting);
  }
  
  commentCallbackID = '#' + this.form.id;
  $.ajax({
    type: 'POST',
        url: this.form.action,
        data: data,
        success: commentAjaxSuccess
        });

  return false;
}

$(document).ready(function() {
    $(".commentSubmit").click(commentAjaxSubmit);
  });

Commenting disabled for spambots

+2  Posted by como • Jun.04.2012 at 21.52 • Reply

This is great!

+1  Posted by Power Thief • Jun.04.2012 at 21.46 • Reply

So this whole comment thing should actually work? Posted and displayed using AJAX?

+1  Posted by Azhral • Jun.04.2012 at 22.08 • Reply

What you need to do is stop treating Javascript as a static language and start using its true power as a functional language with lexical scoping. Lexical scoping allows functions defined inside other functions to use its parents variables and data, even if the parent function has already returned.

function commentAjaxSubmit(eventObject) { var data = $(this.form).serialize(); var commentCallbackID = '#' + this.form.id; var button;

if (this.name) {
    button = {this.name: this.value};
    data += '&' + $.param(button);
}

data += '&CLajax=1';

$.ajax({
    type: 'POST',
    url: this.form.action,
    data: data,
    success: function (data, textStatus, jqXHR) {
        if (jqXHR.status == 201) { /* Inject comment */
            var url = jqXHR.getResponseHeader('Location');
            var cPos = url.indexOf('#');
            var commentCommentID = url.substring(cPos);

            $.ajax({
                type: 'GET',
                url: url,
                success: function (result) {
                    $(commentCallbackID).replaceWith($(result).find(commentCallbackID));

                    $(commentCallbackID).next('.comments-wrap').prepend($(result).find(commentCommentID));
                }
            });
        }
        else if (jqXHR.status == 205) /* Reload page */
            window.location.replace(jqXHR.getResponseHeader('Location'));
        else if (jqXHR.status == 200)
            commentReplaceForm(data, commentCallbackID);
    }
});

return false;

}

$(document).ready(function() { $(".commentSubmit").click(commentAjaxSubmit); });

+1  Posted by Azhral • Jun.04.2012 at 22.09 • Reply