Monday, June 6, 2011

Cloning Dijit widgets

Dojox and Dijit widgets are undoubtedly cool, bringing a richer user experience to your web site.

One of their drawback though is that they are not easily cloneable. A typical example is for instance a table with a set of controls per row and a “Add new row” button, allowing to add a new row to the table.

This article provides a simple solution to clone a table row containing dijit widgets.

The problem

I've recently started using dojo and dijit to provide some cool user interface experience on a project I am working on.

During implementation I've faced with a problem: I have a table with a set of widgets on each row, initially with a single row, but with the user able to add as many rows as he needs. Unfortunately there is no (native) way to clone a dijit widget, so I had to find a different solution.

I am using the declarative approach of creating dijit widgets, by adding some dijit related attributes to input controls and let the dojo parser convert them from normal input controls to dijit widgets. For instance, the following input textbox:
<input type='text' dojoType='dijit.form.ValidationTextBox' />

is parsed by dojo and converted into a ValidationTextBox widget like this:

<div
	class="dijit dijitReset dijitInlineTable dijitLeft dijitTextBox dijitValidationTextBox"
	id="widget_dijit_form_ValidationTextBox_0" role="presentation"
	dir="ltr" widgetid="dijit_form_ValidationTextBox_0">
	<div class="dijitReset dijitValidationContainer">
		<input
			class="dijitReset dijitInputField dijitValidationIcon dijitValidationInner"
			value="Χ" type="text" tabindex="-1" readonly="readonly"
			role="presentation">
	</div>
	<div class="dijitReset dijitInputField dijitInputContainer">
		<input class="dijitReset dijitInputInner"
			dojoattachpoint="textbox,focusNode" autocomplete="off" type="text"
			id="dijit_form_ValidationTextBox_0" tabindex="0" value="">
	</div>
</div>

Conversion from html control to dijit widget can be performed either automatically, by specifying the djconfig='parseOnLoad:true' parameter when loading the dojo script:

<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/dojo/1.6.0/dojo/dojo.xd.js' djconfig='parseOnLoad:true'></script>
or manually, by calling the dojo.parser.parse() function, most likely in addOnLoad().

 

The solution

The idea behind the solution I’ve found out can be summarized as follows:

  1. read the table row before conversion from input control to dijit widget occurs and save its html code in a safe place
  2. reuse the table row as a template to clone new table rows, and apply the dojo parsing on it for input to widget conversions

In order to work, row cloning must be done before the dojo parsing occurs.

This is the raw html form we’ll work on:

<form dojoType='dijit.form.Form' id='form'>
	<input type='hidden' value='1' id='hRowsCounter' />

	<input type='button' id='btnAddNewRow' value='Add new row'  label='Add new row' dojoType='dijit.form.Button' />
	
	<table id='table'>
		<thead>
			<tr>
				<td>Quantity</td>
				<td>Unit Price</td>
		</thead>
		<tbody>
			<tr id="row-1">
				<td><input type='text'  id='editQuantity1' dojoType='dijit.form.NumberSpinner' required='true' /></td>
				<td><input type='text'  id='editUnitPrice1' dojoType='dijit.form.CurrencyTextBox' required='true' /></td>
			</tr>
		</tbody>
	</table>
	<input type='submit' value='Submit' label='Submit' dojoType='dijit.form.Button'  />
</form>

The hidden field is used to keep track of the total number of rows, used both locally to generate the identifiers of the cloned controls and at server side, to let the web application know in advance how many rows to process.

The table row is made up of 2 dijit widgets, NumberSpinner and CurrencyTextBox,  but of course the same principle applies to any other dijit or dojox widget.

This form “as is” is rendered by the browser as a standard html form, not powered by dijit yet, and it should look like the following:

standard form

In order to perform the enhancement, we need some javascript code. First of all, we need to load the dojo library in the <head> section, along with jquery, which I’m going to use as I feel more familiar with:

<head>
	<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/dojo/1.6.0/dojo/dojo.xd.js' djconfig='parseOnLoad:false'></script>
	<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js'></script>
</head>

Notice the parseOnLoad parameter explicitly set to false, to prevent automatic dojo parsing of the body, precondition needed for the solution to work. Following the javascript code to enhance the standard html form into a widget based dijit form.

<script type='text/javascript'>
	dojo.require('dijit.form.Form');
	dojo.require('dijit.form.Button');
	dojo.require('dijit.form.CurrencyTextBox');
	dojo.require('dijit.form.NumberSpinner');

	dojo.addOnLoad(function() {

		// Parse the body to convert input controls into dojox/dijit widgets
		dojo.parser.parse(dojo.body());
		
		dojo.connect(dijit.byId('btnSubmit'), 'onClick', function(e) {
			dijit.byId('form').submit();
		});
		
		dojo.connect(dijit.byId('form'), 'onSubmit', function(e) {
			return this.validate();
		});
	});	
</script>

After adding the above code to our page, the form is now visibly different:

enhanced form

The final step is to implement the row addition. First we need to save the row template in the addOnLoad() event, prior to dojo parsing:

var rowTemplate = null;

dojo.addOnLoad(function() {

	// Save the row template
	rowTemplate = $('#row-1').clone();
	
	// Parse the body to convert input controls into dojox/dijit widgets
	dojo.parser.parse(dojo.body());

	...
});	

Next we have to attach a click handler to the “Add new row” button:

var rowTemplate = null;
	
dojo.addOnLoad(function() {

	// Save the row template
	rowTemplate = $('#row-1').clone();
		
	// Parse the body to convert input controls into dojox/dijit widgets
	dojo.parser.parse(dojo.body());
		
	dojo.connect(dijit.byId('btnSubmit'), 'onClick', function(e) {
		dijit.byId('form').submit();
	});
	
	dojo.connect(dijit.byId('form'), 'onSubmit', function(e) {
		return this.validate();
	});
		
	dojo.connect(dijit.byId('btnAddNewRow'), 'onClick', function(e) {
		addTableRow();
	});
});	

Last, the missing piece is the actual addTableRow() implementation:

function addTableRow() {
	// Retrieve the rows counter hidden control
	var hRowsCounter = $('#hRowsCounter');
	
	// Calculate the next row index
	var index = parseInt(hRowsCounter.val()) + 1;
	
	// Set the new row id
	var newRowId = 'row-' + index;

	// Clone the table row from the previously saved template
	var newRow = rowTemplate.clone().attr('id', newRowId);
	
	// Replace the id of each input control
	newRow.find('input').each(function() {
		var newId = $(this).attr('id').replace(/[\d]+$/g, index);
		$(this).val('');
		$(this).attr('id', newId)
		$(this).attr('name', newId);
	}).end().appendTo('#table');
	
	// Update the number of rows
	hRowsCounter.val(index);
	
	// Force a dojo parsing of hte newly created row 
	dojo.parser.parse(dojo.byId(newRowId));
}

The actual row cloning is done at line 12, with contextual update of the row id.

Lines 15 to 20 are used to locate all <input> controls in the newly created row and for each one:

  • calculate a unique control id (line 16)
  • reset the control value (line 17)
  • set the new control id (line 18)
  • set the control name (line 19)

The new control id is calculated by replacing the ending numbers in the control id with the row number, using a regular expression. If you look at the html form code, you’ll see that each control in the table has an id made up of a descriptive text (editQuantity) and a number, identifying the row. The regular expression locates the ending number, so it can be replaced with the new row number. Needless to say, this process is required to ensure that each control in the table has a unique id.

Here is the final form, with some rows manually added to the table:

enhanced form with multiple rows

0 comments:

Copyright © 2013. All Rights Reserved.