Tuesday, March 2, 2010

Input for One-to-Many Relationships Revisited

In our previous post, we discussed a method for logically displaying and constructing objects in a one to many relationship. However, this was not ideal - a lot of display logic was being written in the taglib, and writing html in code is painful and plain ugly. While this problem can be overlooked when only one additional text input was being displayed, a new feature which requires the display of a full domain object forces us to re-examine our approach.

Our solution uses templates and the ability to pass code to taglibs. Any content written within the tag is passed to the taglib in the 'body' parameter. This content, which can be additional html, a closure, or a template, can then be invoked within the taglib as a method.

Our example is a student report card: each report card has at most one class in a particular category, though it is not required to have a class in every category. First, we have the domain on the many end of the relationship.
class CourseCategoryType {
String category
String status

static constraints = {
category(inList:['History', 'Civics' ,'Math', 'Science', 'Art'])
status(inList:['Active', 'Inactive'])
class Course {
CourseCategoryType courseCategoryType
String name
String courseCode
BigDecimal gpa

Next we have the taglib. Notice we are invoking body as a method, and passing a parameter. The parameter will be available to the content of body as the variable 'it'.
class CourseTagLib {
def courseTag = {attrs, body ->
def from = attrs.from
def courses = attrs.courses

from.each {
def course
// find if the course has been selected
for (Course c: courses) {
if (c.courseCategoryType.id == it.id) {
course = c

// build parameters to pass to the template
def params = [
courseCategory: it,
course: course
out << body(params)

Finally we have the gsp snippet which calls our taglib. Notice that the template is passed 'it' as the bean. Remember, any information passed to body in the taglib is referenced by 'it'.
<g:courseTag from="${CourseCategoryType.findAllByStatus('Active')}" 
<g:render template="course" bean="${it}" />

The template is the Grails generated create gsp with a few changes
1. It's been renamed to _course.gsp
2. The bean being used is different than the default generated template - it is no longer an instance of Course, it is the array passed by the taglib. The bean is also named 'it'. Thankfully, these are cut-and-paste changes.

The disadvantage of our new method is that we're tightly coupling the taglib and template to a domain class; there is no possibility of reuse. However, the taglib is relatively simple to create - it is essentially massaging data into a format to be used by the template.

No comments:

Post a Comment