Saving Form Drafts to the Server with Yahoo! UI

A client browsing our site may fill in a form with valuable data. His computer may crash or internet connection may go down before he can press the "submit" button. The client may decide not to submit the data. We want this data saved as a draft to our server as he fills in the form. Gmail does this when we are composing an email. If we are a little sneaky in our use of the Yahoo! Connection Manager then saving drafts easy.

When we make a call to YAHOO.util.Connect.setForm() the form data is serialized and stored as a string in the "private" variable YAHOO.util.Connect._sFormData. This string is used as part of the Ajax request when we call YAHOO.util.Connect.asyncRequest() so that the form data is sent to the server. That's what makes this sneaky: we are accessing a private variable. That also means that if the internal code of the Yahoo! Connection Manager changes then maybe this solution won't work. It does work with version 0.11 and I can't see why the Yahoo! folks would change this particular part of the implementation. If they did change it then we could easily write our own form data serializer to collect the form data as a string. Regardless, once we have a page working with a particular version of YUI we should freeze that page to that particular version so that our site is not breaking unexpectedly when YUI us updated.

We only want to save drafts when the form data changes to avoid overloading our server with unnecessary requests. All we have to do is compare the serialized form data from before with the serialized data now. If it has changed then we submit the updated form data with and Ajax request.


The html file

The JavaScript that makes the magic draft saving happen is non-obtrusive. It knows to look for a form element with id "my_form". This keeps the HTML file very clean. It is just any standard HTML form.

<html lang="en">

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Draft Manager Demo</title>
  <script src="/javascripts/yahoo.js" type="text/javascript"></script>
  <script src="/javascripts/event.js" type="text/javascript"></script>
  <script src="/javascripts/connection.js" type="text/javascript"></script>
  <script src="/javascripts/connection-override.js" type="text/javascript"></script>
  <script src="/javascripts/blog_draft_manager.js" type="text/javascript"></script>


<form action="/demo/save" id="my_form" method="post">
  <select id="yyy" name="yyy">
    <option value="yes">yes</option>
    <option value="no">no</option>
  <input id="asdf" name="asdf" type="text" value="asdf" />
  <textarea name="user_input_1">Some stuff</textarea>
  <p><input name="commit" type="submit" value="Save changes" /></p>


The draft_manager.js file

var draft_manager = {};
function DraftMgr() {
  /* Find the form element with id "my_form".
   * This makes the javascript non-intrusive */
  this.form = document.getElementById('my_form');
  /* make the initial cache of the form data */
  this.cache = YAHOO.util.Connect._sFormData;
  /* Set the JavaScript to save a draft later if required */
/* Wait until the window is loaded to initalize the draft manager */
YAHOO.util.Event.addListener(window, "load",
                      function(){draft_manager = new DraftMgr();});

/* Save the data in the form as a draft if the data has changed */
DraftMgr.prototype.saveDraft = function() {
  /* Update the data stored in YAHOO.util.Connect._sFormData
     to be the current from data*/
  /* Check if the form data has changed */
  if (this.cache != YAHOO.util.Connect._sFormData) {
    this.sentData = YAHOO.util.Connect._sFormData;
    YAHOO.util.Connect.asyncRequest('POST', '/demo/save_draft',
                      {success: function(){this.cache = this.sentData;},
                       after: function(){this.setTimeOut();},
                       scope: this});
  } else {

DraftMgr.prototype.setTimeOut = function() {
  /* use a closure so saveDraft() executes in the correct scope */
  var thisC = this;
  /* Look for changes in the form data every one second. */
  this.timeout = setTimeout(function() {thisC.saveDraft();}, 1000);

The draft save Ajax requests go to a different URL on the server than when the user clicks "submit". It doesn't have to be done this way but it is good for my application and makes it easy to know what triggered the POST request.

The cache data is only updated if the Ajax request is successful. This means that if the request was unsuccessful the browser will try again to submit the current form data.

The Ajax callback object does not do anything with the response. The server just accepts the data and stores it as a draft. I don't do any data validation. Validation happens when the user clicks "submit". So for the Ajax request the server just sends back a status of "200 OK" in the header and and empty body. This makes things very fast as the browser doesn't have to download a big file with each draft save.

This solution uses my connection-override.js file to tidy up the callback with the "after" hook. You could easily avoid using my extra file by changing the callback to the following.

{success: function(){this.cache = this.sentData;this.setTimeOut();},
 failure: function(){this.setTimeOut();},
 scope: this});

p.s. Please use these powers for good.


Have something to write? Comment on this article.

Peter Michaux July 16, 2006

In a thread on the YUI group Matt Warden suggested some good optional extras that could be added to this type of draft saving.

In a similar application, Matt would only send altered form elements (excluding the currently focused element) to the server and validate them. The server would send any errors back to the client. This would give feedback to the user as the form is being completed.

Matt also wisely noted that the server must still watch for SQL injection when saving drafts.

Have something to write? Comment on this article.