Master-detail implementation for RESTful services with JQuery

Maarten Winkels

In two previous posts, we have seen how to develop RESTful application with JBoss AS 7. At the end of the second blog we used a generic REST client tool to execute some RESTful web services. Of course we would rather build a custom UI application as client for our services, so that a user can easily access and manipulate the data. In this blog we build a REST client that is based on the master-detail principle.

REST and Master-Detail

A RESTful web service, as developed in the previous posts is essentially an editable collection. This makes a Master-Detail design a prime candidate for the User Interface on top of it: The UI will display the entire collection in the master view and a single entry in the collection will be made editable in the detail view.

How do we implement this? Well, the web services produce data in the JSON format. A client will have to consume this data and display it in a User Interface. A JavaScript client is a very good choice for this, since JSON is a sort-of "native" format for JavaScript. Of course other options are possible, you could develop a client with Adobe Flex or JavaFX, for example. An advantage of using simple HTML and JavaScript would be that there are a lot of applications (web browsers) that understand these languages and can be used to run the REST client.
There are a number of frameworks that can be used to develop a REST client in JavaScript. A popular choice is BackBone.js with Mustache.
For this blog I have chosen to develop the client as a jQuery plugin. jQuery is a general purpose JavaScript library with a lot of features. We are going to use its templating feature to display the data.

Using jQuery template for the Master view

Let's first look at the master view. This will be a table with a row for each book, displaying the id and the title.

What we need for this is a simple jQuery template that displays a single row for each book and an appropriate container in which the rows can be placed.

<script id="bookRowTemplate" type="text/x-jquery-tmpl">
  <tr>
	<td class="idCol"><a href="#" data-id="${id}" class="bookSelect">${id}</a></td>
	<td class="titleCol">${title}</td>
	<td><a href="#" data-id="${id}" class="bookDelete">delete</a></td>
  </tr>
</script>

The jQuery template can be a script tag in the header of the page. It should have the type text/x-jquery-tmpl and an appropriate id for referencing. Properties of the objects we want to display (in this case the id and title of the book) can be inserted using ${...} notation.

<table>
	<caption>Books</caption>
	<thead>
		<tr>
			<th class="idCol">Id</th>
			<th class="titleCol">Title</th>
		</tr>
	</thead>
	<tbody id="booksContainer"></tbody>
</table>

The container element is in this case a tbody with id="booksContainer". The pattern in the ids used here will become more clear later.

Loading the data and filling out the template

Now how do we download the data and use the template to show it?

function loadList (options) {
	doAjaxCall('GET', options.url, null, function(data) {
		useTemplate(options.rowTemplate, data, options.rowsContainer);
	});
}

function doAjaxCall(type, url, data, callback) {
	$.ajax({
		type: type,
		url: url,
		dataType: "json",
		data: data,
		success: callback
	});
}

function useTemplate(template, data, container) {
	if (template) {
		container.empty();
		template.tmpl(data).appendTo(container);
	}
}

The loadList function loads the data and displays it in the master view. It uses the doAjaxCall function to start the asynchronous loading. This is merely a wrapper around the standard jQuery ajax function, but it will be used in many more places. When the ajax call returns successfully, the useTemplate function is used to display the returned data in the template.
The options object that is referenced in some places, is a container for configuration options for the client, that will be discussed later. The relevant configuration options here are:

var options = {
	url: 'books/',
	rowTemplate: $('#bookRowTemplate'),
	rowsContainer: $('#booksContainer')
};

The JSON that is loaded for display is shown below. In this case it is a list of objects, where each object is a book with an id and a title. Since it is a list, the template will be applied to each element and the resulting HTML is concatenated.

Selecting and deleting items from the list

The template for each row contains two links, one for selecting an item, the other for deleting it. How do these work?

// opts.selectLinks = $('.bookSelect');
opts.selectLinks.live('click', function (event) {
	var id = $(this).data('id');
	doAjaxCall('GET', url(opts.url, id), null, function(data) {
		useTemplate(opts.entryTemplate, data, opts.entryContainer);
	});
	event.preventDefault();
});

//opts.deleteLinks = $('.bookDelete');
opts.deleteLinks.live('click', function (event) {
	var id = $(this).data('id');
	doAjaxCall('DELETE', url(opts.url, id), null, function(data) {
		doResetAndReload(opts);
	});
	event.preventDefault();
});

function url(url, id) {
	return url + id + '/';
}

Handlers for the click events are attached to these links using jQueries live function. From the documentation

Attach a handler to the event for all elements which match the current selector, now and in the future.

In our case, the row template generates class attributes for the links that can be used for attaching the handler. For both operations, we need the id of the book we want to select or delete. For easy access, the template generates a data attribute (data-id) in the links. In the event handlers, the id is retrieved from the node and used to make another ajax call. For the delete links, a DELETE method is called and on success the UI is reset and the list reloaded. For the select links we need to display the detail data of the item in an entry form.

The detail view as entry form

The entry form should display the data for one item in the list (in our case a book) and it should allow the user to POST or PUT the data in the form to a Web Service. The static HTML is fairly simple:

<div id="bookContainer">
Book:
<form id="bookForm">
	<fieldset id="bookEntryContainer">
	</fieldset>
	<input type="reset" value="Cancel" />
	<input type="submit" value="Save" />
</form>
</div>

The form has only an id attribute and no action or method. This will be handled by the javascript handler for submitting the form. The submit and cancel buttons are static and standard. The interesting part is the fieldset, that will contain the elements rendered by the template.

<script id="bookEntryTemplate" type="text/x-jquery-tmpl">
	<p>Id: ${id}</p>
	{{if id}}
	<input id="idField" name="id" type="hidden" value="${id}"/>
	{{/if}}
	<label for="titleFld">Title:</label>
	<input id="titleFld" name="title" value="${title}"/>
</script>

In this template there is a little logic to handle the cases for new and existing books. An existing book can be recognized by its id property. If the id property is filled, it should be set and send to the web service by adding the hidden input field.
The JavaScript handle the form events is a little bit more complicated.

opts.entryForm.submit(function (event) {
	var form = $(this);
	var data = form.serialize();
	var id = $('#'+opts.entryIdField).val();
	if (id) {
		doAjaxCall('PUT', url(opts.url, id), data, function(data) {
			doResetAndReload(opts);
		});
	} else {
		doAjaxCall('POST', opts.url, data, function(data) {
			doResetAndReload(opts);
		});
	}
	event.preventDefault();
});

opts.entryForm.bind('reset', function (event) {
	onSelect(opts, null);
	event.preventDefault();
});

When submitting the form, we want to send the contents as form data to through an ajax call. If the id is not set, a POST method is called to create a new instance. If the id is set, a PUT method should be called with the id appended to the URL. In both cases we want to reset the UI and reload the list on a successful return.

Icing on the cake: Expanding options

This is basically all the code for this master-detail based REST client with jQuery templates. With this code you can do simple CRUD operations on simple books, as shown in the screenshots below.

Now how about those options that you kept on hearing about? I designed the client to be able to use multiple REST clients for related collections on the same page. Therefore I wanted some mechanism that would allow me to do

new RestClient({name:'book'});
new RestClient({name:'author'});

and the client would figure out which templates, which containers and which forms to use for display and entry purposes. This is accomplished by using configurations that are expanded to include the value for other options.

RestClient = function (options) {
	var opts = $.extend({}, RestClient.defaults, options);
	expand(opts);
	...
};
RestClient.defaults = {
	plural: '{name}s',

	baseUrl: 'services',
	url: '{baseUrl}/{plural}/',

	rowTemplate: '#{name}RowTemplate',
	rowsContainer: '#{plural}Container',

	entryTemplate: '#{name}EntryTemplate',
	entryContainer: '#{name}EntryContainer',
	entryForm: '#{name}Form',
	entryIdField: 'idField',

	selectLinks: '.{name}Select',
	deleteLinks: '.{name}Delete',

	onSelect: $.noop,
	onReload: $.noop
};		

The options passed into the constructor extend the default options, which allows for customizing the options anyway you would want. After the extension is applied, the values are expanded, basically replacing every occurrence of {...} by the option value referenced by that name.

function expand(options) {
	for (var field in options) {
		var val = options[field];
		if ($.type(val) == 'string') {
			while (val.match(/{([^{}]*)}/)) {
				val = val.replace(/{([^{}]*)}/, function (key, group) {
					return options[group];
				});
			}
			if (val.charAt(0) == '#' || val.charAt(0) == '.') {
				var obj = $(val);
				if (val.charAt(0) == '#') {
					if (obj.length == 0) {
						obj = undefined;
					}
				}
				val = obj;
			}
			options[field] = val;
		}
	}
}

The highlighted lines in this code fragment resolve any CSS selectors through jQuery after expanding them.
As an example (and a short preview) the screenshot below shows a page with two Master-Detail implementations, one for books and one for authors, both implemented using this plugin.

Conclusion

Writing a JavaScript client for RESTful web services is very simple with jQuery and jQuery template. The source code for this blog can be downloaded here:
books.html jquery.rest-client.js books.css
Since the backend is not deployed, the page is not functional 😉

Comments (1)

  1. Mariano Fernández - Reply

    September 18, 2011 at 8:38 pm

    Hi,

    I really liked the topic of this post. I've been thinking about this for months now.I think it's important to think CRUD and master-detail in terms of Richardson's REST maturity model levels.
    http://martinfowler.com/articles/richardsonMaturityModel.html
    Companies spend so much time doing CRUD. ¿When will be ever grow up?

    Cheers,

    Mariano.

Add a Comment