Interactive Drupal Web Forms with jQuery JavaScript library

In this tutorial I provide a sample travel booking from with JavaScript code to turn it interactive. It means that the response to your actions is immediate - there is no need to submit the form. This script could be further extended to fetch up-to-date prices from the server using Ajax queries. Additionally, I created a general purpose jQuery plug-in that helps creating this kind of forms with rule-based pricing.

Have a look at the this familiar looking form below. While booking a flight looks simple at the first sight, it's actually driven by interactive JavaScript scripts that update prices in real time according to complex rules. Go ahead and try it!

Book a flight sample form

  Adults Children
Ticket
 
 
Insurance
 
 
Total
 

Getting started with Drupal and JavaScript

There are two key tools to get you started with writing JavaScript for Drupal: Firebug and Drupal developer module. The first one is a general purpose tool that enables you to inspect page source and to try out your scripts with refreshing the page. The second one helps by displaying the form structure, so that you can easily find form elements.

Drupal comes with a powerful JavaScript library called jQuery. Its usage with Drupal is fairly well documented - see for example the following Drupal pages:

Hey, there's even a book about it: Drupal 6 JavaScript and jQuery (in Amazon / Safari books online). Check out a sample over here: Working with JavaScript in Drupal 6: Part 1.

Drupal forms and jQuery

A quick start to creating your scripts is to update some form field values by executing something in the lines of this code snippet on you Firebug console:

var a = $('#edit-submitted-persons').val();
$('#edit-submitted-sum').val( a * 100 );

Note that input fields generated by the web form module have "edit- submitted-" prefixed to the id, but anyway - as seen below - you can easily find out the actual id:s by using Firebug's Inspect-function.

Use FireBug (or Google Chrome Developer Tools) to check element's ID:

  1.  Select "Click an element in the page to inspect"
  2. Point to an element whose ID you want to know
  3. Check the ID attribute value

Note that in order to use the ID selector in jQuery, you need to put the hash character (#) in front of it: "#edit-submitted-travelers-adults";

 

To run the script every time page loads, you need to add it to your form's theme file. Drupal tutorials have a guide how to do it using top-level template.php, but I recommend you to add it to your own form specific theme files. Here's a sample how to load travelers.js script from a template file.

drupal_add_js(drupal_get_path('theme', 'blueprint') .'/scripts/travelers.js', 'theme');

Before you save the script, I should emphasize two things: Firstly, it introduces a dependency to jQuery library. This library is a part of Drupal, but you need to make sure that it loads before you run your own script. This can be achieved with drupal_add_js as it handles loading correctly. Secondly, the script can be only run after the whole page has loaded and the form fields have been rendered. There is a document ready event which you can connect to using jQuery ready handler or alternatively the Drupal-way: Drupal.behaviors. Basically wrapping all of your code into a jQuery ready handler or a Drupal.behavior function has the same impact: it will get executed at the right time, after the browser has finished rendering the HTML elements on the page:

Drupal.behaviors.bookingForm = function (context) {
// some debugging here, remove when you're finished
console.log('[bookingForm] started.');

// get number of travelers and multiply it by 100
function recalculateTotal() {
    var count = $('#edit-submitted-travelers').val();
    count = parseFloat( count );
    var cost = 100;
    $('#edit-submitted-total').val( count * cost );
}

// run recalculateTotal every time user enters a new value
var fieldCount = $('#edit-submitted-travelers');
fieldCount.change( recalculateTotal );

// etc ...
};

The script above already reveals all the important building blocks of interactive jQuery-driven forms:

  • jQuery change - reacts to user actions and fires the given function
  • jQuery val - reads and sets from field values
  • JavaScript parseFloat - converts strings to numbers

Adding Javascript to Drupal Webform

As I said, the above form is actually driven by complex rules: price is an array look up using destination as the key; insurance is a simple multiplication; children's prices are divided by two and finally everything is summed up to a total value. To make it easier to work with this kind of logic I created a jQuery module that 1) takes a set of form fields, 2) converts their contents to numbers, 3) performs the given function on them and 4) alternatively returns the outcome or stores it to the given node.

// Executes the given function on the numeric values of the selected form fields.
// Selected elements are all passed for function in the internal order.
// The user has to make sure the form fields match function params.
// Target is optional. if set, this function can be used in jQuery chain.
jQuery.fn.calc = function( func, target ) {
	var nodeNumVals = this.map( function( index, node ) {
		return parseFloat( $( node ).val() );
	} );
	nodeNumVals = $.makeArray( nodeNumVals );
	var total = func.apply( this, nodeNumVals );
	if( !target ) {
		return total;
	} else {
		if( !isNaN( total ) ) {
			var mthd = target.is(":input") ? "val" : "text";
			target[mthd]( total.toFixed(2) );
		}
		return this;	
	}
}

This module was inspired by jQuery Calculation Plug-in. The main difference is the simpler interface of this plug-in - instead of string parsing it accepts a standard function.

The following JavaScript snippet for the booking form on this page demonstrates how to use this calc function in various ways. You can take advantage of several console.log messages and find the elements from this page.

$('#edit-submitted-trip-destination')
    .add('#edit-submitted-travelers-adults')
    .calc(
        function( destination, adults ){
            return destination * adults
        },
        $('#total')
    )

To explain the lines above in detail:

  1. Select the first source element (input field) for the calculation
  2. Select another source element
  3. Define calc
  4. Each source element is passed in as an argument
  5. Your own logic
  6. --
  7. Target/output element

You can easily test it with Firefox Firebug (or Google Chrome Developer Tools):

  1. Copy-paste the calc-function from my website in to Firebug console and run it
  2. Then the same for the snippet above
  3. See the value changing in the Total-field

Building complex logic to webforms

The following, more complicated JavaScript snippet actually drives the booking form on this page.

$(function() {
	
	// prices for different destinations organized by destination keys
	var ptckt = {
		1 : 100,
		2 : 200,
		3 : 300,
		4 : 400,
		5 : 600,
		6 : 600,
		7 : 100,
		8 : 700
	};
	
	// price of a single insurance
	var pins = 15;
	
	// form fields
	var adults = $('#edit-submitted-travelers-adults');
	var kids = $('#edit-submitted-travelers-children');
	var dest = $('#edit-submitted-trip-destination');
	var ins = $("input[@name='submitted[insurance]']");
	console.log( adults, kids, dest, ins );
	
	// totals
	var atckt = $('#at');
	var ctckt = $('#ct');
	var ains = $('#ai');
	var cins = $('#ci');
	var total = $('#total');
	console.log( atckt, ctckt, ains, cins, total );
	
	// form field sets
	var travelers = adults.add( kids );
	var destTrav = dest.add( travelers );
	var all = destTrav.add( ins );
	console.log( travelers, destTrav, all );
	
	// calculate ticket prices by looking up prices from array defined earlier
	var calcTicket = function() {
		destTrav.calc( function( d, a, c ) { return ptckt[ d ] * a }, atckt );
		destTrav.calc( function( d, a, c ) { return ptckt[ d ] * c / 2 }, ctckt );
	};
	destTrav.change( calcTicket );
	
	// calculate insurance prices by multiplying with a constant defined earlier
	var calcIns = function() {
		var getIns = $("input[@name='submitted[insurance]']:checked").val() == "yes";
		all.calc( function( d, a, c, y, n ) { return getIns * pins * a }, ains ); // false == 0
		all.calc( function( d, a, c, y, n ) { return getIns * pins * c / 2 }, cins );
	};
	all.change( calcIns );
	
	// calculate total price by summing all terms together
	var calcTotal = function() {
		var at = destTrav.calc( function( d, a, c ) { return ptckt[ d ] * a } );
		var ct = destTrav.calc( function( d, a, c ) { return ptckt[ d ] * c / 2 } );
		
		var getIns = $("input[@name='submitted[insurance]']:checked").val() == "yes";
		var ai = all.calc( function( d, a, c, y, n ) { return getIns * pins * a } );
		var ci = all.calc( function( d, a, c, y, n ) { return getIns * pins * c / 2 });
		
		all.calc( function() { return at + ct + ai + ci }, total );
	};
	all.change( calcTotal );
	
	// manually trigger initial calculation
	adults.change();
});

Saving the Javascript files on the Server

You can save the final jQuery Javascript file onto your server basically into any folder the public can access. As mentioned above, I placed it under the theme directory and loaded it from the form specific theme page using drupal_add_js. If you have a look at the source of this page, you can see from where the scripts are loaded. Probably the best place for all the .js files would be under the module where you handle all the server side calculations.

Internationalization and number formatting

While jQuery is a powerful library, they have decided to keep it simple. Unfortunately this means that there are no internationalization (i18n) and localization (l10n) tools included on jQuery. Every programmer outside the UK and the USA knows the importance of supporting various different languages and locales, meaning e.g. number formatting.

Instead of trying to reinvent the wheel I recommend that you utilize Dojo toolkit, another JavaScript library. It comes with full-blown i18n support that uses localization data from the Common Locale Data Repository (CLDR) at unicode.org, which means that you can easily parse and format numbers as they're used around the world.

Webform server-side additional processing

You noticed how I stored calculation results to div elements, not input fields. This is to remind you not to trust into any values calculated on the client-side. Everyone has heard those horror stories about smart customers seting their own prices when the server-side validation hasn't been done properly. Instead, you need to build duplicate logic on the client side. To avoid this duplication, you might consider fetching the value always from the server using an Ajax query.

Submit Drupal form using Ajax

By utilizing Ajax you will always have the logic in one place, but on the other hand, your form might become slightly slower and less responsive. Here's a sample how to submit your form using jQuery.post for server-side calculation. Just connect this to form fields onChange event as instructed above and replace console.log with a function that saves the value from the server to a form field, and you're done.

$.post("test.php", $("#webform-client-form-16").serialize(), function( priceFromServer, textStatus, XMLHttpRequest ) {
    console.log( priceFromServer, textStatus, XMLHttpRequest )
});

 

4 comments

 
Christophe #1 1 Dec 2010 21:01

Hi,

This is a very nice description ! thanks for this post.
I'm wondering how I can retrieve some lists (ex. in your code : ptckt,the price list) from the database or from values stored in specific node types. Do you have an idea if it is possible for doing that with Drupal ?

thanks,
Christophe

 
Danilo #2 9 Feb 2011 16:50

Thanks this was extremely helpful! It works like a charm. Just one thing tho... I don't get the last bit.. How do I use jQuery.post to save the calculation values? Could you give me an example?

 
Juho #3 14 Feb 2011 23:34

Danilo, basically in order to save the custom pricing results, you'd need to create your own module. This is because you can't trust the user input and consequently you want to do the maths again on the server side. I'll look into writing an article about the details, but meanwhile I hope these links will get you started:

Maybe it's better to skip the the jQuery part for now. Once you have your server-side processing ready, you can add some bling on your form with jQuery.

 
Danilo #4 16 Feb 2011 00:15

Thanks a bunch! You saved me a lot of time. I couldn't wrap my head around it. I'm looking forward to reading your article. Ciao