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:
<g:selectWithOptGroup name = "song.id" from = "${Song.list()}" optionKey = "id" optionValue = "songName" groupBy = "album" />
And the code to implement this tag:
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 << "<select name=\"${attrs.remove('name')}\" " // process remaining attributes outputAttributes(attrs) writer << '>' writer.println() 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 << " <optgroup label=\"${optGroup.encodeAsHTML()}\">" writer.println() from.eachWithIndex {el, i -> if(el.properties[groupBy].equals(optGroup)) { def keyValue = null writer << '<option ' if (keys) { keyValue = keys[i] writeValueAndCheckIfSelected(keyValue, value, writer) } else if (optionKey) { if (optionKey instanceof Closure) { keyValue = optionKey(el) } else if (el != null && optionKey == 'id' && grailsApplication.getArtefact(DomainClassArtefactHandler.TYPE, el.getClass().name)) { keyValue = el.ident() } else { keyValue = el[optionKey] } writeValueAndCheckIfSelected(keyValue, value, writer) } else { keyValue = el writeValueAndCheckIfSelected(keyValue, value, writer) } 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 << '</option>' writer.println() } } writer << '</optgroup>' writer.println() } } // close tag writer << '</select>' } 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 << '<option value="' << (noSelectionKey == null ? "" : noSelectionKey) << '"' if (noSelectionKey.equals(value)) { out << ' selected="selected" ' } out << '>' << noSelectionValue.encodeAsHTML() << '</option>' } private String optionValueToString(def el, def optionValue) { if (optionValue instanceof Closure) { return optionValue(el).toString().encodeAsHTML() } el[optionValue].toString().encodeAsHTML() } }
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
wzdhck
o73nha