I've recently started working with Groovy on Grails at work. Groovy is a dynamic language that runs on the JVM (Java Virtual Machine), and Grails is a high-productivity open-source web-application framework that leverages the Groovy language. Grails follows the "convention over configuration" principle, taking away a lot of the grunt work that goes into creating web applications. It also uses proven technology like Hibernate and Spring with a consistent interface. You can have a Grails app up and ready to go with only a few lines of code. This high productivity is also due to the fact that Grails uses Groovy, which is a dynamic language and is much more expressive than Java. Finally, because Groovy runs on the JVM, it has full access to Java's rich API.
Groovy comes with a number of built-in tags which allow you to quickly create forms, form controls, or any number of other HTML constructs or elements. However, I noticed that Groovy didn't provide a built-in tag to build a SELECT that includes OPTGROUPs. OPTGROUPs allow you to group options within a drop-down select-box. But this problem can be solved because Groovy supports the creation of custom tags. After reading a bunch of documentation and looking at a few examples, I was able to create my own SELECT tag that supports OPTGROUPs. Here is an example of the usage of the tag:
[sourcecode language="html"]
And the code to implement this tag:
[sourcecode language="groovy"]
import org.springframework.beans.SimpleTypeConverter
import org.springframework.web.servlet.support.RequestContextUtils as RCU
import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler
class MyAppTagLib {
def selectWithOptGroup = {attrs ->
def messageSource = grailsAttributes.getApplicationContext().getBean("messageSource")
def locale = RCU.getLocale(request)
def writer = out
def from = attrs.remove('from')
def keys = attrs.remove('keys')
def optionKey = attrs.remove('optionKey')
def optionValue = attrs.remove('optionValue')
def groupBy = attrs.remove('groupBy')
def value = attrs.remove('value')
def valueMessagePrefix = attrs.remove('valueMessagePrefix')
def noSelection = attrs.remove('noSelection')
def disabled = attrs.remove('disabled')
Set optGroupSet = new TreeSet();
attrs.id = attrs.id ? attrs.id : attrs.name
if (value instanceof Collection && attrs.multiple == null) {
attrs.multiple = 'multiple'
}
if (noSelection != null) {
noSelection = noSelection.entrySet().iterator().next()
}
if (disabled && Boolean.valueOf(disabled)) {
attrs.disabled = 'disabled'
}
// figure out the groups
from.each {
optGroupSet.add(it.properties[groupBy])
}
writer << "
if (noSelection) {
renderNoSelectionOption(noSelection.key, noSelection.value, value)
writer.println()
}
// create options from list
if (from) {
//iterate through group set
for(optGroup in optGroupSet) {
writer << "
from.eachWithIndex {el, i ->
if(el.properties[groupBy].equals(optGroup)) {
def keyValue = null
writer << '
if (optionValue) {
if (optionValue instanceof Closure) {
writer << optionValue(el).toString().encodeAsHTML()
}
else {
writer << el[optionValue].toString().encodeAsHTML()
}
}
else if (valueMessagePrefix) {
def message = messageSource.getMessage("${valueMessagePrefix}.${keyValue}", null, null, locale)
if (message != null) {
writer << message.encodeAsHTML()
}
else if (keyValue) {
writer << keyValue.encodeAsHTML()
}
else {
def s = el.toString()
if (s) writer << s.encodeAsHTML()
}
}
else {
def s = el.toString()
if (s) writer << s.encodeAsHTML()
}
writer << ''
writer.println()
}
}
writer << ''
writer.println()
}
}
// close tag
writer << ''
}
void outputAttributes(attrs) {
attrs.remove('tagName') // Just in case one is left
attrs.each {k, v ->
out << k << "=\"" << v.encodeAsHTML() << "\" "
}
}
def typeConverter = new SimpleTypeConverter()
private writeValueAndCheckIfSelected(keyValue, value, writer) {
boolean selected = false
def keyClass = keyValue?.getClass()
if (keyClass.isInstance(value)) {
selected = (keyValue == value)
}
else if (value instanceof Collection) {
selected = value.contains(keyValue)
}
else if (keyClass && value) {
try {
value = typeConverter.convertIfNecessary(value, keyClass)
selected = (keyValue == value)
} catch (Exception) {
// ignore
}
}
writer << "value=\"${keyValue}\" "
if (selected) {
writer << 'selected="selected" '
}
}
def renderNoSelectionOption = {noSelectionKey, noSelectionValue, value ->
// If a label for the '--Please choose--' first item is supplied, write it out
out << ''
}
private String optionValueToString(def el, def optionValue) {
if (optionValue instanceof Closure) {
return optionValue(el).toString().encodeAsHTML()
}
el[optionValue].toString().encodeAsHTML()
}
}
[/sourcecode]
This is my very first attempt and so I'm sure some not-so-best practices abound. Feedback and comments are welcome.
Update
- Previously, the tag wouldn't pass through extra attributes that were defined. This has been fixed.
- My previous version didn't take into account a bunch of stuff that Grails' actual SELECT tag does. I was able to find the Grails implementation here. I modified it to take into account OPTGROUPs.
Thank you, most helpful!
You are amazing. I was looking for optgroup and copy pasted your code and it works 🙂
Thanks
Hey Vivin, Really nice work your code helped me out a lot, I made a few modifications and now have a nice taglib for simple group by select lists.
One improvement I would suggest is to check that the group option is not null, so up on line 36ish change the code
// figure out the groups
from.each {
optGroupSet.add(it.properties[groupBy])
}
to
// figure out the groups
from.each {
if (it.properties[groupBy]){
optGroupSet.add(it.properties[groupBy])
}
}
Thanks again