A few days ago I wrote a blog entry demonstrating how one could do a multi-step form in a jQuery Mobile application. This worked by simply breaking up the forms into multiple files and having a 'controller' file load in the right one via an include as you progressed through the process. I got some feedback that it would be interesting (and perhaps better) if the process was done completely client-side. Today I worked up a small demo of this as an alternative.

While working on this demo I discovered two things about jQuery Mobile:

  1. I knew that jQuery Mobile "took over" forms and when you submitted them it converted the action into an Ajax-based post. I thought, however, if you wrote your own code to handle the form submit, and used e.preventDefault(), it would, well, prevent that. Nope. And this is what weirded me out. My form handler was called when I submitted the form. But I couldn't prevent jQuery Mobile from submitting the data anyway. This to me feels like a bug. You have to completely disable jQuery Mobile's form handling by adding data-ajax="false" to the form tag. Not a big deal - just not expected.
  2. You can include more than one "page" inside an HTML page. This is useful for times when you may have a few simple static pages you want immediately available. I thought it would be a good way to handle my multistep form. But - here's the rub. You cannot have a "multipage" page loaded via jQuery Mobile's Ajax page loads. I don't think that's clear, so let me back up. Normally when you link to a page, like foo.html, jQuery Mobile hijacks the link and will load the contents of foo.html via Ajax. jQuery Mobile will look for a "page" div and render just that. If you do this, and link to a file with N pages inside it, jQuery Mobile will destroy the other pages. You can't use them. So if you want to use a multipage html, you have to either ensure it is the first page loaded, or ensure the link to to the page does not use ajax - again using data-ajax="false". This is documented here, and I want to thank Robert Bak for helping me find that detail.

So given the above, let's take a look. First I have my index page. This is merely meant to reflect the fact that our demo is part of a "real" site. So the first link is just some random other page and the second link is the form we want to actually demo.

<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Page Title</title> <link rel="stylesheet" href="http://code.jquery.com/mobile/latest/jquery.mobile.min.css" /> <script src="http://code.jquery.com/jquery-1.6.4.min.js"></script> <script src="http://code.jquery.com/mobile/latest/jquery.mobile.min.js"></script> <script src="main.js"></script> </head> <body>

<div data-role="page">

<div data-role="header"> <h1>Page Title</h1> </div>

<div data-role="content">

<ul data-role="listview" data-inset="true"> <li><a href="foo.html" data-ajax="false">Something</a></li> <li><a href="someform.html" data-ajax="false">The Form</a></li> </ul>

</div>

<div data-role="footer"> <h4>Page Footer</h4> </div>

</div>

</body> </html>

Note the use of data-ajax="false" in the link to my form. This is critical since someform.html is a multipage file. Ok, now let's look at someform.html. This one is a bit big.

<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Some Form</title> <link rel="stylesheet" href="http://code.jquery.com/mobile/latest/jquery.mobile.min.css" /> <script src="http://code.jquery.com/jquery-1.6.4.min.js"></script> <script src="http://code.jquery.com/mobile/latest/jquery.mobile.min.js"></script> <script src="main.js"></script> </head> <body>

<!-- Step One --> <div data-role="page" id="step1">

<div data-role="header"> <a href="index.html" data-icon="home">Home</a> <h1>Reg Form</h1> </div>

<div data-role="content">

<form method="post" class="msform" data-ajax="false"> <input type="hidden" name="nextStep" value="step2"> <div data-role="fieldcontain">
<label for="name">Name:</label>
<input type="text" name="name" id="name" value="" />
</div> <div data-role="fieldcontain">
<label for="email">Email:</label>
<input type="email" name="email" id="email" value="" />
</div>

<div data-role="fieldcontain">
<input type="submit" name="submit1" value="Send" />
</div> </form>

</div>

<div data-role="footer"> <h4>Page Footer</h4> </div>

</div>

<!-- Step Two --> <div data-role="page" id="step2">

<div data-role="header"> <a href="index.html" data-icon="home">Home</a> <h1>Reg Form</h1> </div>

<div data-role="content">

<form method="post" class="msform" data-ajax="false"> <input type="hidden" name="nextStep" value="step3">

<div data-role="fieldcontain"> <fieldset data-role="controlgroup"> <legend>Gender:</legend> <input type="radio" name="gender" id="male" value="male" checked="checked" /> <label for="male">Male</label>

<input type="radio" name="gender" id="female" value="female" /> <label for="female">Female</label> </fieldset> </div>

<div data-role="fieldcontain"> <label for="coolness">Coolness:</label> <input type="range" name="coolness" id="coolness" value="25" min="0" max="100" /> </div>

<div data-role="fieldcontain">
<input type="submit" name="submit2" value="Send" />
</div> </form>

</div>

<div data-role="footer"> <h4>Page Footer</h4> </div>

</div>

<!-- Step Three --> <div data-role="page" id="step3">

<div data-role="header"> <a href="index.html" data-icon="home">Home</a> <h1>Reg Form</h1> </div>

<div data-role="content">

<form method="post" class="msform" data-ajax="false"> <input type="hidden" name="nextStep" value="echo.cfm">

<div data-role="fieldcontain"> <fieldset data-role="controlgroup">

<legend>Stuff I like:</legend>

<input type="checkbox" name="stuffilike" id="checkbox-1" value="Star Wars" /> <label for="checkbox-1">Star Wars</label>

<input type="checkbox" name="stuffilike" id="checkbox-2" value="BSG" /> <label for="checkbox-2">BSG</label>

<input type="checkbox" name="stuffilike" id="checkbox-3" value="Beer" /> <label for="checkbox-3">Beer</label>

</fieldset> </div>

<div data-role="fieldcontain">
<input type="submit" name="submit3" value="Send" />
</div>

</form>

</div>

<div data-role="footer"> <h4>Page Footer</h4> </div>

</div>

<script> $("#step1").live("pageinit", function() { $("form.msform").live("submit", handleMSForm); }); </script>

</body> </html>

Holy smokes - that's a big template. Basically what I've done here is put every form into it's own "Page" object. The actual form fields aren't really that important. Do note though the use of hidden form fields. That's going to come into play real soon now. Finally, note at the end I've registered a pageinit handler. I'm asking it to notice the submit of any form using the class "msform". handleMSForm is defined in main.js:

var formData = {};

function handleMSForm(e) { var next = "";

//gather the fields var data = $(this).serializeArray();

//store them - assumes unique names for(var i=0; i<data.length; i++) { //If nextStep, it's our metadata, don't store it in formdata if(data[i].name=="nextStep") { next=data[i].value; continue; } //if we have it, add it to a list. This is not "comma" safe. if(formData.hasOwnProperty(data[i].name)) formData[data[i].name] += ","+data[i].value; else formData[data[i].name] = data[i].value; }

//now - we need to go the next page... //if next step isn't a full url, we assume internal link //logic will be, if something.something, do a post if(next.indexOf(".") == -1) { var nextPage = "#" + next; $.mobile.changePage(nextPage); } else { $.mobile.changePage(next, {type:"post",data:formData}); } e.preventDefault();

};

So - what's going on here? My basic idea here is that on every form submit, I want to gather, and store, the form data. When done I can send the entire thing at once to the server. I begin with an object, formData. That's going to store name/value pairs of form information. Next is handleMSForm. It begins by calling serializeArray() on the form. This is a jQuery utility that will gather up all the form fields and return them as an array. I then just have to loop over them. Remember I said those hidden form fields would come into play? I'm using them for a bit of logic, so if I encounter them, I store the value separately and then continue looping over the data. I do a bit of logic to see if a value already exists, and if so, I append it. (This is important for checkboxes.)

After storing the data, I then look at the "next" variable. Remember this stored the value from the hidden form field. I decided that any simple value, like "foo", implied the ID of a page to load. Therefore, if the value does not have a dot in it, I simply load the next page. If it does have a dot, I'm assuming something.cfm, or .php, you get the idea, and I switch to a post operation.

All in all - it works ok. You can see my note about commas and values, but for the most part, you can probably not worry about that. (And if it does concern you, just store it as an array and JSON-encode the value.) You can try this code yourself via the link below.