Tuesday, November 10, 2009

Ajax-Enabled Checkboxes in Grails

In Grails: A Quick-Start Guide, we have a task list to help TekEvent organizers and volunteers keep track of what's left to be done for their event. It's pretty handy, but it could be a bit easier to use.

Specifically, it would be great if we didn't have to click on a task to open the show view and then click again to edit it, just to mark a task as completed. What would be really nifty is if we could mark a task as completed by clicking on a simple checkbox. And it turns out that Grails provides an Ajax tag - <g:remotefunction> - that's just what we need.

Here's a picture of our current list view. We'll put our checkbox in the last column, since we don't really need to repeat the Event name.



We'll modify the list view in two places. First, in the <head> section, we'll replace this:

<tr>

<g:sortableColumn property="id" title="Id" />

<g:sortableColumn property="title" title="Title" />

<g:sortableColumn property="notes" title="Notes" />

<th>Assigned To</th>

<g:sortableColumn property="dueDate" title="Due Date" />

<th>Event</th>

</tr>

With this:

<tr>

<g:sortableColumn property="id" title="Id" />

<g:sortableColumn property="title" title="Title" />

<g:sortableColumn property="notes" title="Notes" />

<th>Assigned To</th>

<g:sortableColumn property="dueDate" title="Due Date" />

<th>Completed</th>

</tr>

All we did here is change the text of the column heading. (Trivial, yet necessary.)

Next, we will modify the table row that currently displays the Event name. Let's replace this:

<td>${fieldValue(bean:taskInstance, field:'event')}</td>

with this:

<td>

<g:checkBox name='completed' value="${taskInstance.completed}"
onclick="${remoteFunction(action:'toggleCompleted', id:taskInstance.id,
params:'\'completed=\' + this.checked')}" />
</td>

Here we are using a <g:checkBox> tag to show whether a Task is completed or not. We will also use it to toggle the completed value. For that, we use a tag, but we call it as a method and assign it to the onclick event of the checkBox. We'll talk a bit more about the remoteFunction, but first, there's one more thing we need.

In order to use any of Grails' JavaScript tags, we need to set a JavaScript library to be used. We do this with the <g:javascript> tag, which can be placed anywhere in the <head> section of our page. Like so:

<g:javascript library=”prototype” />

The remoteFunction tag can take a controller, action, id and params. We will use all except the controller, since we are on a page produced by the TaskController, which is the controller we'll be using. This tag can also take an update attribute, but we don't want to update anything on the page, so we'll skip that one too. For the action we'll use 'toggleCompleted' (which we will write shortly). The id will be the id of the taskInstance being displayed. Then comes the params.

The params property can be a Map just as in other GSP tags, but it can also be a String formatted like request parameters. That's the way we use it here: params:'\'completed=\' + this.checked'. Since we are already inside a double quoted GString, we have to use single quotes for the params value, and that means escaping the single quotes that need to appear in the final value. The phrase “this.checked” is a literal that will be passed into the resulting JavaScript function, where it will be evaluated to the checked value of the checkbox. This value will end up in our controller as either 'true' or 'false'.

If we were to look at the page with Firebug (or a similar tool) we would see that the value being assigned to the onclick event is something like this:

new Ajax.Request('/TekDays/task/toggleCompleted/1',{asynchronous:true,evalScripts:true,parameters:'completed=' + this.checked});

Here's what our page looks like now:



As I mentioned earlier, we don't need to update anything on the page when the user clicks the checkbox. The checkbox already gives the visual cue we are looking for. What we want is to set the completed property on the corresponding Task instance and then save it. So, let's implement the toggleCompleted action in our TaskController. Open /grails-app/controllers/TaskController.groovy and add the following action:

def toggleCompleted = {
def task = Task.get(params.id)
if (task){
task.completed = params.completed == 'true'
task.save()
}
render ''
}

The first step in this action is to get the Task based on the id passed in. Then if we get that successfully, we'll set its completed property based on the value of the completed parameter. You might be tempted to try task.completed = params.completed, but params.completed is a String (either 'true' or 'false') and not a Boolean value. So we have to compare against a String literal.

Next we save the Task instance, and finally render a blank String to prevent any attempt to render something back to the browser.

That's all there is to it. We can now mark a task complete or incomplete right from the list view - no form needed. Pretty handy.

(Now we could also clean up the page in other ways, as we did on our TekEvent list view in section 6.2, “Modifying the Scaffolded Views,” on page 92.)

12 comments:

  1. Great post! I struggled with the remoteFunction usage for a while and was slightly confused as to whether the params parameter had to be in a map or the format you used since I was also using the prototype library.

    Great example to follow in the future.

    Thanks!

    ReplyDelete
  2. Dave, I posted a question over on the Prag Prog Grails Quick Start book forum where I can't fire up the TekDays app because the app insists on downloading the 1.1.2-hibernate plugin into a Grails 1.1.1 environment. I'm new to Grails so I'm sure there's a straight forward answer, but at the moment I'm stuck.

    Thanks!

    ReplyDelete
  3. @Dan McHarness: I replied on the pragprog.com forum. If you can't get passed this. Feel free to email me daveklein at usa dot net.

    @Mike Miller: Thanks! And thanks for the suggestion on the syntax highlighter! I started using it on my other blog (http://dave-klein.blogspot.com) and will do so on this one from now on.

    ReplyDelete
  4. Hi, I followed you instruction and made something similar for myself. However, the same code wouldnt work if my column of checkboxes is the first column of the table; it started working after I remodel the view like the one you put up. Why is that the case?

    ReplyDelete
  5. Hi Steve,

    That's weird but true. I don't know why it does that, but since I haven't needed checkboxes as the first column, I haven't taken the time to dig into it much deeper.

    Does anyone else out there know why this is?

    Thanks,
    Dave

    ReplyDelete
  6. So, I have a parent class called 'event' which has many 'bookings'. In the event/show view there is an unordered list of the related bookings for that event.

    I'm trying to create an AJAX checkbox for the 'confirmed' attribute of 'booking' in the same td, because you can't create a new cell within a list item. I've managed at least to get the checkbox to match the state of the booking, but I'm having a really hard time making the AJAX functionality to work.

    Any thoughts? Let me know if my description isn't clear enough. Would really appreciate some help with this.

    Thanks,
    Oren

    ReplyDelete
  7. Hi Oren,

    I'm not quite sure what you're trying to do or what you're difficulty is. If you want to send me the code you're use to produce the unordered list and checkboxes, along with an explanation of the problem you are having with the Ajax functionality, I can take a look at it. Send it to daveklein at usa dot net. - Thanks

    ReplyDelete
  8. Dave Hi,
    i need to pass multiple parameters to the controller during the request. but it is not working if you try to pass the map - then you can't pass the check box status/value

    ReplyDelete
  9. Hi Meni,

    The params in the remoteFunction call is just a String of standard request parameters. So, you can add more parameters to it by separating them with the & like so:
    params:'\'completed=\' + this.checked + \'&foo=123\''

    If the values you need to pass are in other field elements you can add them with basic Javascript code. Here's an example using prototype:

    params:'\'completed=\' + this.checked + \'&foo=$(\'foo\').value'

    or without Prototype:

    params:'\'completed=\' + this.checked + \'&foo=document.getElementById(\'foo\').value'

    It can be a little tricky to get your quotation marks all straight and escaped properly. Firebug is a big help with that.

    ReplyDelete
  10. Hi Dave,
    I'm trying to pass a GSP variable in the params but without any success, is this even possible ? i.e I have ${gameInstance.referee.name} that I want to pass in the params

    ReplyDelete
  11. I'm almost certain there's a way to do this directly but every time I have this need, I wrestle with quotation marks and escaping for a few minutes and then I give up and use a hidden field. Something like this:

    <input type="hidden" id="refName" value="${gameInstance.referee.name}" />

    onclick="${remoteFunction(action:'myAction',
    params:'\'refName=\' + document.getElementById(\'refName\').value ')}"

    Either that or I'll create a custom tag. Ooh, that gives me an idea for another blog post. Thanks!

    ReplyDelete
  12. Well, I've not been wrestling with quotation marks and escape for minutes but for hours yesterday :- )
    I came to the same conclusion as you but the hidden field solution is not optimal for me because I'm generating select elements dynamically from a list. The generated HTML will be quite ugly with all these hiddenfields, I will also take a look to providing a custom tag wrapping this. Thanks for your quick reaction !

    ReplyDelete