Oftentimes when designing a UI and working with field limits (such as an HTML input’s “maxlength” attribute), I’ve found myself wondering why it’s not more common to have the constraint come directly from the server. The frequent scenario seems to be that the database has one constraint for the field, and the UI has another — even if they happen to match.
For example:
Maximum length: <span>50</span>
<input name='username' maxlength='50' />
Shouldn’t the UI limit come from the object’s field constraint? It would certainly make it easier to maintain should it ever change; plus it’s more clear on the UI where the value comes from. Grails has a great ValidationTagLib to report errors after-the-fact, but as far as I’m aware, there’s no easy way to get these constraints beforehand.
I decided to take a stab at creating a taglib that will do just this. It should hopefully be extendable to other fields in the future (such as “nullable” or “blank”), but this should be a good start. First let’s introduce our sample GORM object:
package com.igor
import grails.validation.Validateable
@Validateable
class GormTagLibUser {
String username
String firstName
String middleName
String lastName
Integer age
static constraints = {
username(maxSize: 50)
firstName(size: 0..25)
middleName()
lastName(size: 0..20, maxSize: 30)
}
}
Note that it utilizes both the “maxSize” and the “size” constraints (sometimes both, for testing purposes). Other fields do not have any constraints. Ideally we would want the smallest valid value to be returned, otherwise some flag (null/empty string) when the field does not have such constraints. So let’s take a look at a potential implementation:
package com.igor
import org.codehaus.groovy.grails.validation.ConstrainedProperty
class GormTagLib {
static namespace = 'gorm'
/**
* Returns the max length of a field for a given domain.
* If both 'maxSize' and 'size' are used, returns the lesser. If neither is found, returns empty string.
*
* @attr clazz REQUIRED The full class name of the domain object being referenced
* @attr field REQUIRED The field for which to look up the max length
*/
def limit = { attrs ->
String className = attrs.clazz
String field = attrs.field
if (!className || !field) {
throwTagError("Tag [limit] is missing required attribute [clazz] and/or [field].")
}
Class clazz
try {
clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader())
} catch (ClassNotFoundException e) {
throwTagError("Tag [limit] could not find a class with name [$className]. Be sure to include the package name.")
}
if (!clazz.newInstance().hasProperty(field)) {
throwTagError("Tag [limit] could not find field [$field] on class [$className].")
}
ConstrainedProperty fieldConstraint = clazz.constraints."$field"
// The field exists but does not have any constraints, so return empty string
if (!fieldConstraint) {
out << ''
return
}
Integer maxSize = fieldConstraint.getAppliedConstraint('maxSize')?.maxSize
Integer size = fieldConstraint.getAppliedConstraint('size')?.range?.to
if (maxSize && size) {
out << Math.min(maxSize, size)
return
}
out << (maxSize ?: size ?: '').toString()
}
}
Walking through the code we see that an exception is raised when the proper parameters aren’t passed, or when the specified class or field do not exist. If the field does exist, we will take the lesser of “maxSize” and “size”, and return an empty string if no such constraint exists. I chose to use Class.forName() and specify the ClassLoader (rather than the simpler grailsApplication.getClassForName() approach) as it worked better for my tests.
And of course, some tests to illustrate the different scenarios:
package com.igor
import grails.test.mixin.*
import org.codehaus.groovy.grails.web.taglib.exceptions.GrailsTagException
import spock.lang.Specification
import spock.lang.Unroll
@TestFor(GormTagLib)
class GormTagLibSpec extends Specification {
void "limit() should throw an exception if attributes are missing or wrong"() {
when:
tagLib.limit(clazz: className, field: fieldName)
then:
thrown(GrailsTagException)
where:
className | fieldName
null | 'username'
GormTagLibUser.canonicalName | null
'NoSuchClass' | 'username'
GormTagLibUser.simpleName | 'username' // Needs package name
GormTagLibUser.canonicalName | 'NoSuchField'
}
@Unroll
void "limit() should return the max size of a field if it is present, or otherwise null"() {
when:
String value = tagLib.limit(clazz: GormTagLibUser.canonicalName, field: fieldName)
then:
expectedValue.toString() == value
where:
fieldName | expectedValue
'username' | 50
'firstName' | 25
'middleName' | ''
'lastName' | 20
'age' | ''
}
}
Now that we have the taglib done, we can invoke the “limit” method with:
Maximum length: <span><gorm:limit clazz='com.igor.GormTagLibUser' field='username'/></span>
<input name='username' maxlength="${gorm.limit(clazz: 'com.igor.GormTagLibUser', field: 'username')}" />
A bit more code than the alternative, but hopefully a lot more maintainable as well. I’ve posted the taglib up on GitHub in case I (or anyone else!) want to extend the functionality in the future. Hope this helps!
Igor Shults