AJAX Validators (1)

Let's say you need a validator that behaves like any other client-side validator except that it asks the server to do the validation, for example to validate that a username or email address is unique.

Well, here we present a reusable mixin built for the job. It's called AjaxValidator and we use it here to validate that the first and last names are unique.
: (required, maxLength=10, new name only)
: (required, maxLength=10, new name only)

Here are the names already in use:

First Name[Sortable]Last Name[Sortable]
My1Momma
rararakumar
Standa1Bandaa
testcdf
xwangSpoon12
Mixin location is important. Mixins must be put in a package called mixins. Tapestry gives this package special treatment.

References: Component Mixins, Ajax and Zones, Request.

Home

The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.
The source for CustomForm and CustomError is shown in the No Validation Bubbles example.

AjaxValidators1.tml


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<head>
    <link rel="stylesheet" type="text/css" href="${context:css/examples/js.css}"/>
    <style type="text/css">
        form                { background-color: #eee; padding: 14px; }
        .error-label        { color: red; }
        .required-field     { border-right-color: orange; }
        .error-field        { border-color: red; }
        .error-msg          { color: red; }
    </style>
</head>
<body>
    <h1>AJAX Validators (1)</h1>

    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    Let's say you need a validator that behaves like any other client-side validator except that it asks the server to do the validation, 
    for example to validate that a username or email address is unique.<br/><br/> 

    Well, here we present a reusable <strong>mixin</strong> built for the job. It's called <strong>AjaxValidator</strong> and
    we use it here to validate that the first and last names are unique.

    <div class="eg">
        <form t:type="CustomForm" t:id="inputs">
            <t:errors/>
            <table>
                <tr>
                    <td><t:label for="firstName"/>:</td>
                    <td><input t:type="TextField" t:id="firstName" t:validate="required, maxlength=10" size="10"
                        t:mixins="ajaxValidator"/></td>
                    <td>(required, maxLength=10, new name only)</td>
                </tr>
                <tr>
                    <th></th>
                    <td colspan="2"><t:customerror for="firstName"/></td>
                </tr>
                <tr>
                    <td><t:label for="lastName"/>:</td>
                    <td><input t:type="TextField" t:id="lastName" t:validate="required, maxLength=10" size="10"
                        t:mixins="ajaxValidator"/></td>
                    <td>(required, maxLength=10, new name only)</td>
                </tr>
                <tr>
                    <th></th>
                    <td colspan="2"><t:customerror for="lastName"/></td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Display"/></td>
                    <td></td>
                </tr>
            </table>
        </form><br/>
        
        Here are the names already in use:<br/><br/>
        
        <table t:type="grid" t:source="persons" t:row="var:person" include="firstName,lastName">[Grid here]</table>
    </div>

    Mixin location is important.  Mixins must be put in a package called <code>mixins</code>.  Tapestry gives this package 
    special treatment.<br/><br/>
    
    References: 
    <a href="http://tapestry.apache.org/component-mixins.html">Component Mixins</a>, 
    <a href="http://tapestry.apache.org/ajax-and-zones.html">Ajax and Zones</a>, 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/services/Request.html">Request</a>.<br/><br/>
    
    <a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>
    
    The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.<br/>
    The source for CustomForm and CustomError is shown in the No Validation Bubbles example.<br/><br/>
    
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxValidators1.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxValidators1.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/js.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/AjaxValidator.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/AjaxValidator.js"/>
</body>
</html>

AjaxValidators1.java


package jumpstart.web.pages.examples.ajax;

import java.util.List;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;

import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.Request;

public class AjaxValidators1 {
    static private final int MAX_RESULTS = 30;

    // Screen fields

    @Property
    private String firstName;

    @Property
    private String lastName;

    @Property
    private List<Person> persons;

    // Other pages

    @InjectPage
    private AjaxValidators2 page2;

    // Useful bits and pieces

    @Inject
    private Request request;

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    // The code

    void setupRender() {
        persons = personFinderService.findPersons(MAX_RESULTS);
    }

    JSONObject onAjaxValidateFromFirstName() {
        String firstName = request.getParameter("param");

        try {
            validateFirstNameIsUnique(firstName);
        }
        catch (Exception e) {
            return new JSONObject().put("error", e.getMessage());
        }

        return new JSONObject();
    }

    JSONObject onAjaxValidateFromLastName() {
        String lastName = request.getParameter("param");

        try {
            validateLastNameIsUnique(lastName);
        }
        catch (Exception e) {
            return new JSONObject().put("error", e.getMessage());
        }

        return new JSONObject();
    }

    Object onSuccess() {
        page2.set(firstName, lastName);
        return page2;
    }

    void validateFirstNameIsUnique(String firstName) throws Exception {
        if (firstName != null) {
            List<Person> persons = personFinderService.findPersonsByFirstName(firstName);

            if (persons.size() > 0) {
                throw new Exception("The name is not available.");
            }
        }
    }

    void validateLastNameIsUnique(String lastName) throws Exception {
        if (lastName != null) {
            List<Person> persons = personFinderService.findPersonsByLastName(lastName);

            if (persons.size() > 0) {
                throw new Exception("The name is not available.");
            }
        }
    }
}

js.css


body            { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
                    line-height: 17px; }
h1              { font-size: 26px; line-height: 20px; } /* For IE 7 */
form            { margin: 0; }

.eg             { margin: 20px 0; padding: 20px; color: #888; 
                    border: 1px solid #ddd; border-radius: 4px; -webkit-border-radius: 4px; -mox-border-radius: 4px; }

a               { text-decoration: none; color: #3D69B6; }
a:hover         { text-decoration: underline; }

/* For javascript examples. */
.js-required    { color: red; display: block; margin-bottom: 14px; }
.js-recommended { color: red; display: block; margin-bottom: 14px; }

.grid           { border-collapse: collapse; border-spacing: 0; border: 1px solid #dddddd; font-size: 13px; }
.grid tr.odd        { background-color: #f8f8f8; }
.grid tr:hover      { background-color: #eeeeee; }
.grid th        { padding: 3px 5px; text-align: left; width: 130px; border: 1px solid #dddddd; 
                    font-weight: normal; background-color: #eeeeee; 
                    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbfbfb', endColorstr='#e4e4e4'); /* for IE */
                    background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#e4e4e4)); /* for webkit browsers */
                    background: -moz-linear-gradient(top, #fbfbfb, #e4e4e4); /* for firefox 3.6+ */ }
.grid td        { padding: 3px 5px; text-align:left; }

AjaxValidator.java


/**
 * A simple mixin for attaching javascript that invokes a listener in the component via AJAX.
 * Based on http://tinybits.blogspot.com/2010/03/new-and-better-zoneupdater.html
 * and http://tinybits.blogspot.com/2009/05/simple-onevent-mixin.html.
 */
package jumpstart.web.mixins;

import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

// The @Import tells Tapestry to put a link to the file in the head of the page so that the browser will pull it in. 
@Import(library = "AjaxValidator.js")
public class AjaxValidator {

    // Useful bits and pieces

    @Inject
    private ComponentResources componentResources;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    /**
     * The element we attach ourselves to
     */
    @InjectContainer
    private ClientElement clientElement;

    // The code

    void afterRender() {

        // Tell the Tapestry.Initializer to do the initializing of an AjaxValidator, which it will do when the DOM has
        // been fully loaded.

        JSONObject spec = new JSONObject();
        spec.put("elementId", clientElement.getClientId());
        spec.put("listenerURI", componentResources.createEventLink("ajaxValidate").toAbsoluteURI());
        javaScriptSupport.addInitializerCall("ajaxValidator", spec);
    }
}

AjaxValidator.js


// A class that invokes a listener in the component via AJAX.
// Based on http://tinybits.blogspot.com/2010/03/new-and-better-zoneupdater.html
// and http://tinybits.blogspot.com/2009/05/simple-onevent-mixin.html
// and tapestry.js.

AjaxValidator = Class.create( {

    initialize : function(spec) {
        this.field = $(spec.elementId);
        this.listenerURI = spec.listenerURI;

        // Set up a listener that validates the field - asynchronously in the server - on change of focus

        document.observe(Tapestry.FOCUS_CHANGE_EVENT, function(event) {
            
            // If changing focus *within the same form* then perform validation.  
            // Note that Tapestry.currentFocusField does not change
            // until after the FOCUS_CHANGE_EVENT notification.
            
            if (Tapestry.currentFocusField == this.field && this.field.form == event.memo.form) {
                this.asyncValidateInServer();
            }
            
        }.bindAsEventListener(this) );
    },
    
    asyncValidateInServer : function() {
        var value = this.field.value;
        var listenerURIWithValue = this.listenerURI;
                
        if (value) {
            listenerURIWithValue = addQueryStringParameter(listenerURIWithValue, 'param', value);
            
            new Ajax.Request(listenerURIWithValue, {
                method: 'get',
                onFailure: function(t) {
                    alert('Error communication with the server: ' + t.responseText.stripTags());
                },
                onException: function(t, exception) {
                    alert('Error communication with the server: ' + exception.message);
                },
                onSuccess: function(t) {
                    if (t.responseJSON.error) {
                        this.field.showValidationMessage(t.responseJSON.error);
                    }
                }.bind(this)
            });
        }
    }
    
} )

function addQueryStringParameter(url, name, value) {
    if (url.indexOf('?') < 0) {
        url += '?'
    } else {
        url += '&';
    }
    value = escape(value);
    url += name + '=' + value;
    return url;
}

// Extend the Tapestry.Initializer with a static method that instantiates an AjaxValidator.

Tapestry.Initializer.ajaxValidator = function(spec) {
    new AjaxValidator(spec);
}