Some days ago I had a bit of trouble with the data binding utilities of Grails. I wanted to bind multiple values from a GSP form to a List inside a command object. My code looked something like this:
<g:form> <g:textField name="bars[0].baz" /> <g:textField name="bars[1].baz" /> ... </g:form>
class FooCommand { List<BarCommand> bars }
class BarCommand { String baz }
class FooController { def test(FooCommand cmd) { // ... } }
In the controller I expected to get a FooCommand which contains two elements of BarCommand in the bars collection. Unfortunately this code resulted in an
java.lang.IndexOutOfBoundsException, Message: Index: 0, Size: 0
Reason for this is that the Grails data binding does not (like Spring) automatically create new list elements. Grails tries access the elements zero and one of the bars list during data binding. Since the list is empty an IndexOutOfBoundsException is thrown.
The ugly workaround is to initialize bars with a couple of BarCommand instances:
List bars = [new BarCommand(), new BarCommand()]
However, this does only work if you know how much elements of the list will have. Luckily Groovy's List methods provide a better solution:
List bars = [].withLazyDefault { return new BarCommand() }
This creates an empty list that grows whenever it is called with a non-existent index value. So if Grails tries to access bars[0] and bars[1] during data binding these values will be initialized with a new BarCommand.
Another point to note is that withLazyDefault fills gaps with null. This means the following form would result in a list with four elements where the third element is null
<g:form> <g:textField name="bars[0].baz" /> <g:textField name="bars[1].baz" /> <!-- index 2 is missing --> <g:textField name="bars[3].baz" /> ... </g:form>
These null elements can easily be filtered in a controller using Groovy's grep() function:
List listWithoutNullValues = cmd.bars.grep()
grep() returns a new sublist of the elements that evaluate to true. Since null evaluates to false in Groovy all null elements are removed.
Comments
Pascal - Thursday, 10 October, 2013
What version of grails are you using? I tried with the latest 2.3.0 and it doesn't work for me. I have a class Contact {
String firstName
String lastName
List phones = [].withLazyDefault { new Phone () }
}
class Phone {
int index
String number
String type
}
when I pass to my controller params with those values [lastName:'Doe', firstName:'Joe',phones[0].type:M, phones[0].number:9495551212, phones[0].new:true, phones[0].deleted:false, phones[0].id:]
The phones list is still empty
Michael Scharhag - Saturday, 12 October, 2013
I was using Grails 2.1.x. You are right, this doesn't seem to work with Grails 2.3. According to the documentation significant changes to the data binding have been made with Grails 2.3. See: http://grails.org/doc/2.3.0/guide/introduction.html#whatsNew23
The solution with [].withLazyDefault { .. } seems still to work if you change the configuration value grails.databinding.useSpringBinder to true.
However, I noticed there is a much better way in Grails 2.3. Adding a generic type parameter to the List seems to do the work (this didn't work in Grails 2.1). The following code worked for me with Grails 2.3:
class Contact {
...
List<Phone> phones
}
Hmmm... - Thursday, 6 February, 2020
in 2.5.3 you can do same without extra wrapping command class and you can have gaps in items numeration with no exceptions:
<g:form>
<g:textField name="bars[6].baz" />
<g:textField name="bars[9].baz" />
</g:form>
class MyController {
class BarCommand { String baz }
def save( /* Doesn't accept List as a parameter here */ ) {
List<BarCommand> listWithoutNullValues = bindData(new Object(){ List<BarCommand> bars}, params).bars.findAll{it}
}
}
Also, keep in mind, action does not allow anonymous stuff as command parameter, so it doesn't take List, Map, any other interfaces, def, Object, as well as inline classes, while bindData() does, so BarCommand can be inlined into controller in my example above.
Leave a reply