Wednesday, 14 January 2009

grails: Inline Editing For Collection Elements

Consider the scenario, where a User is viewing data on a page and then realises that a change needs to be made to a particular field. Usually this would require navigating to an 'edit' page, modifying the field and then resubmitting the form before viewing the original page to confirm the changes.

Javascript offers a better alternative, an approach called Inline Editing whereby the User can dynamically edit & validate a field, update the server in the process without the need to perform form submission and page refreshes.

In this post I'll demonstrate how to implement this feature using the Scriptaculous library. The example is based upon a simplified issue tracker as shown below.



This is the create.gsp page, which allows the User to create a new Issue and assign it a priority which can assume a value of either Low, Medium, or High.

The following image shows list.gsp, where we display a typical 'list' view of the issues collected.



Now it would be useful if the User could adjust the priorities for the various issues without having to perform the typical edit/submit/refresh cycle just to alter a single field. By implementing Inline Editing for the priority field we would enhance User productivity and application usability in one fell swoop.

So let's follow through the steps to implement this feature.

Step 1: Import scriptaculous

<g:javascript library="scriptaculous"/>




Step 2: Code up the Ajax

In list.gsp locate the line as follows, which simply outputs the priority for a given issue

<td>${fieldValue(bean:issue, field:'priority')}</td>





and replace it with

<td>
    <span id="${issue.id}" class="editInPlace">${fieldValue(bean:issue, field:'priority')}</span>

<script type="text/javascript">
new Ajax.InPlaceCollectionEditor("${issue.id}",
"/inlineApp/issue/updatePriority",
{ collection: [['Low','Low'], ['Medium','Medium'], ['High','High']], ajaxOptions: {method: 'post'} })
</script>

</td>




The replacement snippet consists of wrapping a <span> element around our original statement and inserting some javascript code. The <span> element is assigned an id equal to the identifier of our Issue object and a styling class of 'editInPlace' which is defined in step 4.

Below the <span> element we insert some javascript code that invokes the Ajax.InPlaceCollectionEditor function defined in Scriptaculous. This function is specifically for creating editable fields for collection types and it works in the following way. Firstly it binds a listener to an HTML element (e.g. <span> )to detect for mouse clicks. In this event, it will replace the element with a drop down list populated with a collection of values, e.g. Low, Medium and High. Once the User has made a selection, the new value is transmitted to a grails action via an Ajax call. The action will update the relevant Issue object on the server with the new value. Finally the function will update the page by replacing the drop down list with the newly updated value.

The function takes a number of arguments, the first of which specifies the id for the element that needs to be editable. In our case this is the <span> element. Note that this argument serves as the id of the editable element as well as the identifier for the Issue object that needs to be updated on the server. The second argument is the URL pointing to the grails action that will update the relevant Issue object on the server with the new priority value selected by the User. Thirdly we define a collection of items to be displayed in the drop down list when the User clicks on the editable field. These simply match the options available on the create.gsp page, when a User creates an Issue. Lastly we specify the HTML transport method.

Step 3: Implement the grails action


def updatePriority = {

if(params.editorId)
{
Issue issue = Issue.get(params.editorId)

issue.priority = params.value

issue.save()
}

render params.value

}




This is quite a simple action. It takes the id for the selected Issue, retrieves its corresponding object and then resets its priority value. The updated Issue object is then persisted. Finally the action returns the newly updated value to be displayed on the page. In principle this action can validate any new data provided by the User, but in this example there is no requirement to do so since the User is restricted to a list of options.

Step 4: Apply styling via css

The final step is to apply css styling,

span.editInPlace {
color: blue;
background:#efefef;
border-bottom: 1px solid black
}






The page list.gsp now looks like this



Note that the styling on the priority fields allows the User to discern which fields can be edited inline or not. Now when the User clicks on a priority value, e.g. for the first record, this field is replaced by a drop down list from which the User can select a new value, as shown below



The drop down list is accompanied by two buttons, one for submitting the change, and the other to cancel the operation. The image below shows the result when the User has selected a priority of 'High' before clicking on the 'ok' button. This has updated the corresponding Issue object on the server and the value on the page.



Unfortunately this not the end of the story. The images shown above were viewed under FireFox. Attempting to do the same thing with IE6 results in a viewing problem, where the Priority field is not visible once it's been clicked on. This defect is attributed to a bug within earlier versions of the scriptaculous library but has been rectified for the most recent versions of the api. However the versions of the javascript libraries bundled with grails are not the most recent and are still susceptible to this defect. If I recall correctly, even grails 1.1 still employs version 1.8.0 for scriptaculous and 1.6.0 for prototype. This raises an interesting question as to when these libraries will be updated for the grails distribution.

In the meantime, there are two possible solutions to this problem. The most obvious is to manually upgrade the javascript libraries supplied with grails to the most recent versions. However it's not readily apparent what implications this would have in terms of incompatibilities, since some grails code such as the Ajax tags depend on these libraries. It may be the case that such an approach would have no significant impact, but it is difficult to adopt this solution without further investigation.

Alternatively and this is the approach I've taken, is to apply the fix documented here. The solution requires us to replace this line in control.js [line 761]

var list = document.createElement('select');

with this

list=new Element('select');

Redeploying the app and viewing it under IE6 shows that the problem has indeed disappeared.

On a final note, we could tidy up the code by abstracting the javascript code into a custom grails tag in a similar fashion to the inline editing example given by Graeme Rocher in the first edition of his excellent book.