Last updated by lucastex 3 years ago

BeanField tagLib

This page contains details of how to use the tags defined in BeanFieldTagLib.

Introduction

I started on these tags to help reduce the amount of boiler-plate HTML I had to use when showing fields for bean properties (in forms for create/edit or for 'show' when it is readonly). Grails exposes a great deal of information about the domain object's fields and I wanted to maximise the use of this information to avoid repitition and also to support a common look and feel across my application (with the help of this taglib and CSS).

Standard Grails tags support the rendering of the input field with the existing value for the field being shown, so why another variant ?

Note : I do not wish to proliferate variants of form tags. Grails has standard tags for rendering all form input field variants. Wherever possible I use standard Grails tags. The tags defined here do more than just render the form widget (they also help with showing the label and avoiding repetition of domain definitions when rendering input fields). I am heavily reliant on CSS so these tags help reduce the HTML I need to produce to support CSS.

This taglib adds the following to the rendering of an input field (type='text' or 'password'):-

  1. The display of a standard HTML 'label' for the field
  2. Use of 'convention' to look for a resource bundle message for the field label (with an
override option)
  1. Setting an error class on the label if the field has a validation error
  2. An indicator to show whether the field is mandatory (i.e. if the field does not exist in an 'optionals'
field in the domain object it is considered to be mandatory).
  1. Setting the 'size' and 'maxlength' HTML attributes of the input field to the maximum allowed (as defined
in the 'maxLength' constraint for the field on the domain object)

Other tags help maintain consistency of layout when showing read only fields and when using standard Grails form taglibs in conjunction with these tags.

Tags

beanInputField Tag

Description

Shows a bean in 'data entry' mode (when creating/editing the detail for the wrapping bean). Shows the bean label, the current value for the bean, mandatory field indicator for mandatory fields and highlights the label for the field if it has an associated error.

Tag attributes (optional unless indicated by '(M)') :-

  • beanName (M) : the name of the bean (domain object) which must be in scope
  • fieldName (M) : the name of the property on the domain object to render. Can be a simple fieldName (e.g. 'bookName') or compound (e.g. 'author.email').
  • value : the value to render (otherwise use the value obtained from the domain object for the field specified)
  • size : the override for the HTML 'size' attribute. If not specified the bean is interrogated
to find the 'maxLength' constraint for the field which is then used (up to a maximum of 60).
  • maxlength : the override for the HTML 'maxlength' attribute. If not specified the bean is interrogated
to find the 'maxLength' constraint for the field which is then used (up to a maximum of 60).
  • type : the type of input field. Should be 'text' (default) or 'password'
  • mandatoryField : text to show for a mandatory field (default is '*')
  • errorClass : CSS class to use for the label section if an error is present (default = 'errors')
  • label : use this 'as is' for the label
  • labelMsgKey : if no 'label' attribute is specified use this as the key for a resource bundle message
All other attributes are rendered 'as is'.

For the label if neither 'label' or 'labelMsgKey' is specified then the convention of '<beanName>.<fieldName>' will be used as the key for an 118N'd message for the label.

Example

Assuming:-

  • that a bean named 'currentUser' is in scope with a field
whose property name is 'userId'.
  • the field 'userId' is mandatory
  • the field 'userId' has been defined with a 'maxLength' of 25.
  • the field 'userId' has a validation error for a previously submitted value of 'a'
  • a resource bundle message exists with 'currentUser.userId=User Id'
then the following..
<g:beanInputField beanName="currentUser" fieldName="userId"/>
will render..
<div class="formField">
	<span class="fieldLabel">
		<span class="errors">
			<span class="mandatoryField">
				*
			</span>

<label for="userId"> User Id: </label> </span> </span> <span class="inputField"> <input type="text" size="25" maxlength="25" name="userId" value="a"/> </span> </div>

beanShow Tag

Description

Shows a bean in 'show' mode (when viewing the detail for the wrapping bean). Shows the bean label and the current value for the bean.

Tag attributes (optional unless indicated by '(M)') :-

  • beanName (M) : the name of the bean (domain object) which must be in scope
  • fieldName (M) : the name of the property on the domain object to render. Can be a simple fieldName (e.g. 'bookName') or compound (e.g. 'author.email').
  • value : the value to render (otherwise use the value obtained from the domain object for the field specified)
  • label : use this 'as is' for the label
  • labelMsgKey : if no 'label' attribute is specified use this as the key for a resource bundle message
For the label if neither 'label' or 'labelMsgKey' is specified then the convention of '<beanName>.<fieldName>' will be used as the key for an 118N'd message for the label.

Example

Assuming:-

  • that a bean named 'currentUser' is in scope with a field
whose property name is 'email'.
  • the 'email' field has been retrieved with a value of 'joe.bloggs@gmail.com'
  • a resource bundle message exists with 'currentUser.email=Email'
then the following..
<g:beanShow beanName="player" fieldName="email"/>
will render..
<div class="formField">
	<span class="fieldLabel">
		Email:
	</span>

<span class="showField"> joe.bloggs@gmail.com </span> </div>

beanLabel Tag

Description

Shows just the label for a bean. Mainly used for consistency to help with alignment of a mixture of fields rendered by the above tags and other tags that make use of standard Grails tags. The HTML produced is a similar 'span' section as that produced from the above tags.

Tag attributes (optional unless indicated by '(M)'):-

  • beanName : the name of the bean (domain object) which must be in scope
  • fieldName : the name of the property on the domain object to render
  • mandatoryField : text to show for a mandatory field (default is '*')
  • label : use this 'as is' for the label
  • labelMsgKey : if no 'label' attribute is specified use this as the key for a resource bundle message
For the label if neither 'label' or 'labelMsgKey' is specified then the convention of '<beanName>.<fieldName>' will be used as the key for an 118N'd message for the label. The beanName and fieldName attributes (if present) will be used to check whether the field is mandatory and whether the field has errors (with appropriate rendering of mandatory field indicators and field error blocks if appropriate).

Example

Assuming:-

  • a resource bundle message exists with 'company.currency=Currency'
then the following..
<g:beanLabel labelMsgKey="company.currency">
	<g:currencySelect name="myCurrency" value="${currency}" />
</g:beanLabel>

will render..

"code">
"error">code: null
<div class="formField">

<span class="fieldLabel"> Currency: </span> <span class="showField"> <select name="myCurrency"> ...rest of Grails currencySelect tag output </select> </span> </div>

The Code!

/* Copyright 2004-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT c;pWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.springframework.web.servlet.support.RequestContextUtils as RCU;

/** * A taglib to help render bean fields for input (type=text/password), * labels and for 'show' mode in a consistent and non-verbose way. * Relies on CSS to do all of the rendering magic. * * @author Joe Mooney (with modifactions by Diane Jewett) * @since 13-November-2006 */ class BeanFieldTagLib {

private static final int MAX_INPUT_DISPLAY_LENGTH = 60

/* * To show a bean in 'edit' mode (when creating/editing the detail * for the wrapping bean). Shows the bean label, the current value * for the bean, mandatory field indicator for mandatory fields and * highlights the label for the field if it has an associated error. * * For the label * - if 'label' attribute is specified use it 'as is' * - otherwise if 'labelMsgKey' attribute is specified use it * as the key for an 118N'd message * - otherwise use the convention of <beanName>.<fieldName> * as the key for an 118N'd message */ def beanInputField = { attrs, body -> def fieldName = attrs.remove("fieldName") def beanName = attrs.remove("beanName") def overrideValue = attrs.remove("value")

// Get the bean so we can get the current value and check for errors def bean = request.getAttribute(beanName)

if (bean) {

def fieldValue if (!overrideValue) { fieldValue = getFieldValue(bean, fieldName) } else { fieldValue = overrideValue } def label = getLabelForField(attrs, beanName, fieldName)

def fieldDisplaySize = getAttribute(attrs, "size", "") def fieldMaxLangth = getAttribute(attrs, "maxlength", "") def errorClass = getAttribute(attrs, "errorClass", "errors") def type = getAttribute(attrs, "type", "text") def mandatoryFieldIndicator = attrs.remove("mandatoryField")

def hasFieldErrors = false def errorClassToUse = "" def mandatoryFieldFlagToUse = ""

def sizeToUse = getAttributeToUse("size", fieldDisplaySize) def maxLengthToUse = getAttributeToUse("maxlength", fieldMaxLangth)

if (doesFieldHaveErrors(bean, fieldName)) { errorClassToUse = errorClass }

if (isFieldMandatory(bean, fieldName)) { if (mandatoryFieldIndicator) { mandatoryFieldFlagToUse = mandatoryFieldIndicator } else { mandatoryFieldFlagToUse = "*" } }

/* * Now see if we can set size and maxlength if not explicitly set * based on any constraint in the domain object */ def beanConstraints = null try { beanConstraints = bean.constraints } catch (MissingPropertyException mpe) {}

if (beanConstraints) { def constrainedMaxLength = bean.constraints[fieldName]?.maxLength if (constrainedMaxLength) { if (!sizeToUse) { def maxDisplay = constrainedMaxLength if (maxDisplay > MAX_INPUT_DISPLAY_LENGTH) { maxDisplay = MAX_INPUT_DISPLAY_LENGTH } sizeToUse = 'size="' + maxDisplay + '"' } if (!maxLengthToUse) { maxLengthToUse = 'maxlength="' + constrainedMaxLength + '"' } } }

// Get the optional args we do not need so we can echo 'as is' def varArgs = "" attrs.each { k,v -> varArgs += k + "="" << v << "" " }

def renderParams = ["errorClassToUse":errorClassToUse, "mandatoryFieldFlagToUse":mandatoryFieldFlagToUse, "fieldName":fieldName, "fieldValue":fieldValue, "label":label, "type":type, "sizeToUse":sizeToUse, "maxLengthToUse":maxLengthToUse, "varArgs":varArgs] renderInputField(renderParams) } }

/* * To show only the label for a given field. * * For the label * - if 'label' attribute is specified use it 'as is' * - otherwise use the 'labelMsgKey' attribute as * the key for an 118N'd message * - otherwise use the convention of <beanName>.<fieldName> * as the key for an 118N'd message */ def beanLabel = {attrs, body -> def fieldName = attrs.remove("fieldName") def beanName = attrs.remove("beanName") def label = getLabelForField(attrs, beanName, fieldName) def mandatoryFieldIndicator = attrs.remove("mandatoryField") def errorClassToUse = "" def mandatoryFieldFlagToUse = ""

// Get the bean so we can get the current value and check for errors def bean = request.getAttribute(beanName)

if (bean) {

if (doesFieldHaveErrors(bean, fieldName)) { errorClassToUse = errorClass }

if (isFieldMandatory(bean, fieldName)) { if (mandatoryFieldIndicator) { mandatoryFieldFlagToUse = mandatoryFieldIndicator } else { mandatoryFieldFlagToUse = "*" } }

}

def renderParams = [label:label, body:body, errorClassToUse:errorClassToUse, mandatoryFieldFlagToUse:mandatoryFieldFlagToUse]

renderLabel(renderParams, body) }

/* * To show a bean field in 'show' mode (when viewing the detail for * the wrapping bean). Shows the bean label and the current value * for the bean. * * For the label * - if 'label' attribute is specified use it 'as is' * - otherwise if 'labelMsgKey' attribute is specified use it * as the key for an 118N'd message * - otherwise use the convention of <beanName>.<fieldName> * as the key for an 118N'd message */ def beanShow = {attrs, body -> def fieldName = attrs.remove("fieldName") def beanName = attrs.remove("beanName") def overrideValue = attrs.remove("value") def label = getLabelForField(attrs, beanName, fieldName) def fieldValue if (!overrideValue) { def bean = request.getAttribute(beanName) fieldValue = getFieldValue(bean, fieldName) } else { fieldValue = overrideValue }

renderForShow(label, fieldValue) }

def doesFieldHaveErrors = {bean, fieldName -> if (bean.hasErrors()) { def clazzErrors = bean.errors if(clazzErrors.hasFieldErrors(fieldName)) { return true } } return false }

/** * See if the field is mandatory */ def isFieldMandatory = {bean, fieldName -> def fieldIsMandatory = true def optionalFields = null def val = bean def fval = fieldName

try { optionalFields = bean.optionals if(fieldName.indexOf('.') > 0) { String[] tokens = fieldName.split("\\.") if (tokens.length > 1) { val = tokens[tokens.length-2] fval = tokens[tokens.length-1] def bclass = bean.class for (i in 0..tokens.length-2) { bclass = GCU.getProperyType(bclass, tokens[i]) } optionalFields = bclass.optionals } } } catch (MissingPropertyException mpe) {}

if (optionalFields) { def fieldIsOptional = optionalFields?.grep(fval) if (fieldIsOptional) { fieldIsMandatory = false } }

return fieldIsMandatory }

/** * Get the current value for a given field name on a bean. * * fieldName can be a simple fieldName (e.g. 'bookName') or compound (e.g. 'author.email'). */ def getFieldValue = {bean, fieldName -> def fieldValue = "" def val = bean if(fieldName.indexOf('.')) { fieldName.split('\\.').each { val = val[it] } } else { val = bean[fieldName] } fieldValue = val if (!fieldValue) { fieldValue = "" } return fieldValue }

def renderForShow = {label, fieldValue -> out << """ <div class="formField"> <span class="fieldLabel"> ${label}: </span> <span class="showField"> ${fieldValue} </span> </div> """ }

def renderLabel = {renderParams, body -> out << """ <div class="formField"> <span class="fieldLabel"> <span class="${renderParams.errorClassToUse}"> <span class="mandatoryField"> ${renderParams.mandatoryFieldFlagToUse} </span> ${renderParams.label}: </span> </span> <span class="showField"> """ body() out << """ </span> </div> """ }

def renderInputField = {renderParams -> out << """ <div class="formField"> <span class="fieldLabel"> <span class="${renderParams.errorClassToUse}"> <span class="mandatoryField"> ${renderParams.mandatoryFieldFlagToUse} </span> <label for="${renderParams.fieldName}"> ${renderParams.label}: </label> </span> </span> <span class="inputField"> <input type="${renderParams.type}" ${renderParams.sizeToUse} ${renderParams.maxLengthToUse} ${renderParams.varArgs} name="${renderParams.fieldName}" value="${renderParams.fieldValue}"/> </span> </div> """ }

def getMessage = {messageKey -> def appContext = grailsAttributes.getApplicationContext() def messageSource = appContext.getBean("messageSource") def locale = RCU.getLocale(request) def message = messageSource.getMessage( messageKey, null, null, locale) }

/* * Get the label for the field:- * - if 'label' attribute is specified use it 'as is' * - otherwise if 'labelMsgKey' attribute is specified use it * as the key for an 118N'd message * - otherwise use the convention of <beanName>.<fieldName> * as the key for an 118N'd message */ def getLabelForField = {attrs, beanName, fieldName -> def label = attrs.remove("label") def labelMsgKey = attrs.remove("labelMsgKey") if (!label) { if (!labelMsgKey) { labelMsgKey = beanName + "." + fieldName } label = getMessage(labelMsgKey) } if (!label) { label = labelMsgKey } return label }

def getAttribute = {attrs, attrName, defaultValue -> def returnValue = attrs.remove(attrName) if (!returnValue) { returnValue = defaultValue } return returnValue }

def getAttributeToUse = {attrName, attrValue -> def attributeToUse = "" if (attrValue) { attributeToUse = attrName + '="' + attrValue + '"' } return attributeToUse }

}

Sample CSS

The following is sample CSS that I have used, but the whole point is that the HTML rendered should allow you to modify CSS as this to your heart's content !

.dialog {
	position: relative;
}
.dialog .formField .mandatoryField {
	font-weight: bold;
	color: #FF0000;
}
.dialog .formField .fieldLabel {
	width: 15%;
    height: 35px;
	float: left;
	text-align:right;
	font-weight: bold;
	color: #5E5147;
}
.dialog .formField .inputField {
	width: 84%;
    height: 35px;
	float: right;
}
.dialog .formField .showField {
	width: 84%;
    height: 35px;
	float: right;
}