Gregg is Co-founder and CTO of Wild Bamboo Rocket, LLC. He lives in Wichita, KS with his wife, 3 kids, a cat and a dog. Gregg has posted 6 posts at DZone. View Full User Profile

Better Scaffolding with jQuery - Part II

04.02.2010
| 13674 views |
  • submit to reddit

In Part I of this series I showed how we can improve the user experience by changing the default scaffolding when saving the many side of a one-to-many association.  The server side code changes were minimal and jquery made the client side changes very simple and elegant.  There are still some improvements that can be made and the most glaringly obvious one is how we handle validation errors. I'm going to be using the same code base as last time and just making changes to it.  It might be helpful to have the first article open in a tab especially if you missed it the first time around.

The first area we need to talk about is the server side.  Remember that before all we had to change was when there was a successful save on the reminder we simply wanted to return an instance of the reminder back to the client as JSON.  Now that we need to display errors if validation fails we need a bit more than the domain instance.  Even though the domain instance should contain our errors, picking them out in JavaScript is not a fun task and we also want an easier way to determine if errors exist.  Remember that with JSON we get object properties but not object methods.  So we can't call hasErrors() in JavaScript.  We want to create a wrapper object that can hold the information we need in a very simple way that can then be returned to the client as JSON.  I call this object AjaxPostResponse.groovy.

class AjaxPostResponse {
boolean success
String message
def domainInstance
def errors = [:]
}

Complicated, right?  I think most of the properties are self explanitory.  But just for clarity:

  • success - whether or not save was successful
  • message - a generic optional message
  • domainInstance - we'll need properties of the domain instance if all was good to use in JavaScript
  • errors - a map of the errors if validation fails

We will need to change ReminderController to use this class however, since there's a little bit of code involved with digging out the errors from a domain, I prefer to place the code in a service class.  In some applications I use a generic service for these kinds of purposes but for simplicity and less confusion for this tutorial we'll create a ReminderService, since that is the only place the code is relavent right now.

class ReminderService {
boolean transactional = true
def grailsApplication

def prepareResponse(domainInstance) {
def g = grailsApplication.mainContext.getBean('org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib')
def postResponse = new AjaxPostResponse()
if (domainInstance.hasErrors()) {
g.eachError(bean: domainInstance) {
postResponse.errors."${it.field}" = g.message(error: it)
}
postResponse.success = false
postResponse.message = "There was an error"
} else {
postResponse.success = true
postResponse.message = "Success"
}
postResponse.domainInstance = domainInstance
return postResponse
}
}

This code appears a bit gnarly but it's really quite simple.  We need to define a variable to hold our grails ApplicationTagLib because we're not inside a controller so this isn't already injected for us.  We then need an instance of our AjaxPostResponse.  We then check the domainInstance for errors and if we found some we wire up the errors map in AjaxPostResponse with the errors utalizing some grails taglibs along the way.  Add the success value and message value depending on the error state, our domainInstance, and just return the postResponse.  This service is called from our ReminderController.  The save method is much simpler than it was before:

def save = {
def reminderInstance = new Reminder(params)
reminderInstance.save(flush: true)
render reminderService.prepareResponse(reminderInstance) as JSON
}

Since we're putting all the validation logic in the service and AjaxPostResponse the controller doesn't care if validation failed or not.  We delegate the responsibility back to the client since that's it what needs to know about it.  There's one more addition we need on the server side and that's to make our error messages less generic than the grails defaults.  Add the following to your messages.properties file in the i18n directory:

com.Reminder.duration.nullable=Please enter a Duration
typeMismatch.com.Reminder.duration=Duration must be a number

On the client we need to make a handful of changes.  First we need a place to show any error messages that might come back.  Add a div with a class of 'errors' to the dialog form.  If you recall this is in the event/edit.gsp:

<div id="dialog-form" title="Create new Reminder">
<div class="errors"></div>
<g:form action="save" method="post">
<% -- more code below here --%>
</div>

Next, we need to modify what happens when our response comes back from our ajax request.  Remember that before we were simply returning Reminder as JSON but now we're returning AjaxPostResponse as JSON.  Here is what that bit of code looks like now:

if (data.success) {
var item = $("<li>");
var link = $("<a>").attr("href", contextPath + "/reminder/show/" + data.domainInstance.id).html(data.domainInstance.reminderType.name + " : " + data.domainInstance.duration);
item.append(link);
$('#reminder_list').append(item);
cleanup();
$('#dialog-form').dialog('close');
} else {
showErrors("#dialog-form .errors", data.errors);
}

It's not much different although instead of data.id we have to call data.domainInstance.id (because domainInstance is a property of AjaxPostResponse).  We also check if data.errors is true first.  If it is, we append a reminder to the list, just like before, we do some cleanup (explained in a bit) and close the dialog.  If there were errors we need to show them.  I've created a function called showErrors that accepts a target and the map of errors:

function showErrors(target, errors) {
var errorList = $("<ul>");
for (field in errors) {
errorList.append("<li>" + errors[field] + "</li>")
}
$(target).html("").append(errorList).show(500);
}

We create a UL, loop over each key in the errors map, and create a list item containing the error message that was populed from the server.  Then we clear out any existing messages and append our list and show it.  Speaking of showing the errors list;  tt needs to be hidden when the dialog is displayed the first time.  You can do this simply by calling $("#dialog-form .errors").hide() manually at the top of your JavaScript code or setting a style.  Keep in mind that grails uses the'.error' class also, which is nice for us because it is already styled, but that also means you must take care and use a better selector at getting to them, as I did.  Otherwise you might change existing markup when not expecting to.

If the save was successful we need to do some cleanup.  If there were errors previously these errors need to be cleared out and the dialog's form needs to be reset.  Remember that this dialog is being built when the edit page is first rendered so if you don't reset the form you'll get old data every time you display it.  Our cleanup() method takes care of this, which actually consists of 2 seperate methods, both shown below:

function cleanup() {
$('#dialog-form .errors').html("");
$('#dialog-form .errors').hide();
clearForm('#dialog-form form');
}

function clearForm(target) {
$(':input', target)
.not(':button, :submit, :reset, :hidden')
.val('')
.removeAttr('checked')
.removeAttr('selected');
}

cleanup() clears the errors, hides the div and calls clearForm which simply filters down through all our form elements and sets their values to empty strings and unchecks/unselects any items.  When all is said and done you'll end up with a dialog that looks something like this if you have validation errors:

 

There was a little more involved in getting this from the server to the client but most of the techniques are very reusable across all similar situations.  In the next installment in this series we'll change the code so that the dialog is pulled from an ajax request and in the final installment, Part IV, I'll show how we can edit a reminder using the same dialog.  I hope you are enjoying the series thus far.  Please feel free to offer recommendations for improvement and any general feedback is much appreciated.

Published at DZone with permission of its author, Gregg Bolinger.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Tony Ross replied on Tue, 2010/05/04 - 7:23am

great article - can't wait for Part III

Aaron Epps replied on Fri, 2011/03/18 - 10:04am

I'm getting the following error while trying to use your example...
groovy.lang.MissingPropertyException: No such property: lastName for class: org.springframework.validation.BeanPropertyBindingResult

I've modified your code to use my own Customer class, which looks like:

package grailsapplication1
class Customer {
static constraints = { firstName(blank:false,maxSize:50) lastName(blank:false,maxSize:50) age(nullable:true) emailAddress(nullable:true) } String firstName String lastName Integer age String emailAddress }

Here's the CustomerService

package grailsapplication1
class CustomerService { boolean transactional = true def grailsApplication
def prepareResponse(domainInstance) { def g = grailsApplication.mainContext.getBean('org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib') def postResponse = new AjaxPostResponse() if (domainInstance.hasErrors()) { g.eachError(bean: domainInstance) { postResponse.errors."${it.field}" = g.message(error: it) } postResponse.success = false postResponse.message = "There was an error" } else { postResponse.success = true postResponse.message = "Success" } postResponse.domainInstance = domainInstance return postResponse } }

It's throws the error on the line:

g.eachError(bean: domainInstance) {

Any idea what I'm doing wrong here?

Gregg Bolinger replied on Fri, 2011/03/18 - 12:38pm

Aaron - Are you sure you're passing an instance of your Customer to the prepareResponse method?

Aaron Epps replied on Fri, 2011/03/18 - 3:34pm in response to: Gregg Bolinger

Yes, here's the save method in the controller that I'm using...

def save2 = {
def customerInstance = new Customer(params)
customerInstance.save(flush: true)
render customerService.prepareResponse(customerInstance) as JSON
}

...for what's it worth I'm usinig NetBeans 6.9.1 and Grails 1.3.7

Aaron Epps replied on Fri, 2011/04/08 - 1:49pm

Could you please post the source for this project?

Aaron Epps replied on Mon, 2011/04/11 - 1:06am in response to: Aaron Epps

I had to change the line: def errors in AjaxPostResponse = [:] to LinkedHashMap errorMsgs = [:] due to a couple of issues: Evidentally, Grails (1.3.7), automatically creates a property called "errors" on domain classes, also using "def" for the type doesn't work either, you need to explicitly declare it as a "LinkedHashMap"; other than that it works great, thanks for the info.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.