Adding JavaScript validation to formRemote / Ajax Forms in Grails

On a recent twitter conversation with @mikewillie, the issue of how to add extra JavaScript validation to the Grails formRemote tag came up.

In this post, I will outline the steps needed to validate your data in JavaScript before it gets submitted to the server via Ajax. It allows you conditionally execute your AJAX calls based on previous validation errors.

Let’s say we have a domain object called Book. It looks like this:

package com.tomaslin.testValidation

class Book {
	String title
	String authorEmail

        static constraints = {
	     title( blank: false )
	     authorEmail( nullable: true, email: true )
        }
}

Enabling FormRemote.

For this task, we don’t want a traditional form posting, but rather a fancy Web 2.0. AJAX request for this. So we change the generated scaffold code as follows:

First, call generate-all on this domain class.

Then modify the generated create.gsp file as follows.

Add a library call to the header:

<g:javascript library="prototype"/>

Add a div where the value would be updated:

<div id="updateMe">this div is updated by the form</div>

Change the form tag to a formRemote Tag:

<g:formRemote url="${[action:'save']}" name="bookForm" method="post" update="updateMe"> ... </g:formRemote>

We also want to change the controller code so that it just renders an AJAX response. My save action looks like this:

def save = {
        def bookInstance = new Book(params)
	   if (bookInstance.save(flush: true)) {
            flash.message = "${message(code: 'default.created.message', args: [message(code: 'book.label', default: 'Book'), bookInstance.id])}"
            // redirect(action: "show", id: bookInstance.id)
			render( contentType:'text/plain', text: 'book created successfully')
        }
        else {
			render( contentType:'text/plain', text: bookInstance.errors )
            // render(view: "create", model: [bookInstance: bookInstance])
        }
    }

Adding Client Side validation

If you have changed the above code correctly, we should now have a create screen that is AJAX enabled. If we look at our domain object, we see these constraints on the domain object,

       static constraints = {
	     title( blank: false )
	     authorEmail( nullable: true, email: true )
        }

If you enter an empty title, you will get an error message on the server-side validation stating that the title cannot be empty.

However, it seems pretty wasteful to issue these additional requests to the server when your client side code can do some basic JavaScript validation on this data.

We can use the JavaScriptValidator plugin in Grails to generate pre-submit validation information. We install it via grails install-plugin javascript-validator

In order to use this, we simply add the following tag to our gsp file:

<jv:generateValidation domain="book" form="bookForm"/>

This will generate a popup dialog for every error you have on your page.

Putting it all together

Ok, seems pretty straight forward. I have my javascript validation code and my formRemote tag.

However, if you look in the documentation of the JavaScript validator plugin, you’ll notice that you need to add the

validateForm( this )

method to your form’s onsubmit call. The FormRemote tag does not have a onsubmit method since it autogenerates the onsubmit call to inject the ajax post.

What can we do?

If we look at the formRemote tag definition, we see that we can add a before javascript method to be called before the ajax call.

So let’s try that, change the formRemote tag to

<g:formRemote url="${[action:'save']}" name="bookForm" method="post" update="updateMe" before="validateForm(this)"> ... </g:formRemote>

However, you will notice that this is not what you want. Even though an alert shows up whenever you try to submit this form, it will still send the message to the server.

On closer inspection, we see that the tag generates the following html code

<form onsubmit=" validateForm( this ) ;new Ajax.Updater('updateMe','/testValidation/book/save',{asynchronous:true,evalScripts:true,parameters:Form.serialize(this)}); ;return false" method="post" action="/testValidation/book/save" name="bookForm" id="bookForm">

We see that regardless of the results being returned by the validateForm(this) method ( false if the validation fails, true otherwise ), the Ajax call still proceeds.

The trick here is to realize that you can have any arbitrary JavaScript code in the before and after functions of your formRemote tag.

While the documentation suggests that you should put a Javascript function in either before or after, in Grails 1.2.2 these values simply render out as a string. The hack is to simply provide a before and after method that wrap the generated Ajax call.

In other words, we can add a before call

before= "if( validateForm( this ) ){"

and an after call that closes the if condition

 after=" }; "

Our final tag looks like this:

 <g:formRemote url="${[action:'save']}" name="bookForm" method="post" beforeSend ="validateForm(this)" update="updateMe" before="if( validateForm( this ) ) { " after="}"> 

and the Prototype generated html code looks like this:

<form onsubmit="if( validateForm( this ) ) { ;new Ajax.Updater('updateMe','/testValidation/book/save',{asynchronous:true,evalScripts:true,parameters:Form.serialize(this)});};return false" method="post" action="/testValidation/book/save" name="bookForm" id="bookForm">

An alternative solution that is slightly more elegant is to add a beforeSend property to your formRemote call in jQuery.

This results in a create page that will only submit to the server when the client side validation has passed.

Of course, you can also provide your own validation libraries and methods using the same approach. Hope this helps.

Our final gsp file looks like this:

<%@ page import="com.tomaslin.testValidation.Book" %>
<html>
 <head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 <meta name="layout" content="main" />
 <g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
 <title><g:message code="default.create.label" args="[entityName]" /></title>
 <g:javascript library="prototype"/>
 </head>
 <body>
 <div>
 <span><a href="${createLink(uri: '/')}">Home</a></span>
 <span><g:link action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></span>
 </div>
 <div>
 <h1><g:message code="default.create.label" args="[entityName]" /></h1>
 <g:if test="${flash.message}">
 <div>${flash.message}</div>
 </g:if>
 <g:hasErrors bean="${bookInstance}">
 <div>
 <g:renderErrors bean="${bookInstance}" as="list" />
 </div>
 </g:hasErrors>
 <jv:generateValidation domain="book" form="bookForm"/>
 <div id="updateMe">this div is updated by the form</div>
 <g:formRemote url="${[action:'save']}" name="bookForm" method="post" beforeSend ="validateForm(this)" update="updateMe" before="if( validateForm( this ) ) { " after="}">
 <div>
 <table>
 <tbody>
 <tr>
 <td valign="top">
 <label for="title"><g:message code="book.title.label" default="Title" /></label>
 </td>
 <td valign="top">
 <g:textField name="title" value="${bookInstance?.title}" />
 </td>
 </tr>
 <tr>
 <td valign="top">
 <label for="authorEmail"><g:message code="book.authorEmail.label" default="Author Email" /></label>
 </td>
 <td valign="top">
 <g:textField name="authorEmail" value="${bookInstance?.authorEmail}" />
 </td>
 </tr>
 </tbody>
 </table>
 </div>
 <div>
 <span><input type="submit" value="Submit" />
 </span>
</div>
</g:formRemote>
</div>
</body>
</html>

Quick Reference

8 thoughts on “Adding JavaScript validation to formRemote / Ajax Forms in Grails

  1. Pingback: Blog bookmarks 05/11/2010 « My Diigo bookmarks

  2. Paulo

    Hi, I´m starting a new project using grails and flex and trying to figure out what´s the best choice: AMF or htmlRequest.
    What´s the best choice?
    Looks like AMF choices are very immature.
    Thanks.

    Reply
  3. Raoulinski

    thanks tomas!

    i was looking for this little hack with the before and after attribut for doing some pre-validation.

    greetz

    Reply
  4. doinotlikeit

    Let me add another big ‘thanks’ to the list of folks who have already done so. You hack worked for me too. I spent a lot of time trying to come up with a solution for this.

    Reply

Leave a comment