sethmuss
sethmuss

Reputation: 179

Grails Command Object binding failing for a Map of Lists

(Grails version: 2.3.11, Groovy version: 2.2.2 )

I'm new to Groovy and Grails so forgive me if I am missing something obvious. I have a command object that contains a Map, keys are Integer (although I've tried Strings and they don't work either), values are lists of Details objects:

class Details {
  Integer volume
  Double price
}
class TheCommand {
  Map<Integer, List<Details>> details = [:].withDefault { [].withLazyDefault { new Details() } }
  String location
}

I have this in my GSP:

<g:form controller="mapOfLists" action="create">
  <g:textField name="location" size="7"/>
    <table>
      <g:each in="${(1..24)}" var="hour">
        <tr>
          <td>${hour}</td>
          <g:each in="${(0..4)}" var="column">
            <td><g:textField name="details[${hour}][${column}].price" size="4"/></td>
            <td><g:textField name="details[${hour}][${column}].volume" size="3"/></td>
          </g:each>
        </tr>
      </g:each>
    </table>
  <g:submitButton name="Submit" value="Submit"/>
</g:form>

And the action:

// trying the different binding approaches, none work
def create(TheCommand theCommand) {
  // theCommand.hasErrors() -> false
  // at this point theCommand's details is an empty map, I can see the details[x][y] values in the params object
  bindData(theCommand, params)
  // the bindData above didn't work, details is still an empty map
  theCommand.properties['details'] = params
  // the setting via properties above didn't work, details is still an empty map
}

Two questions: 1) Any ideas on what to try? I have seen there is a way to use a custom binder, but this seems like a case that grails should handle, so I'm giving this a shot before I go down that road.

2) Are there more powerful data-binders? I've stepped through the relevant SimpleDataBinder code, it appears that it only supports single-ly indexed properties

thank you very much, Seth

Upvotes: 0

Views: 1085

Answers (1)

Gregor Petrin
Gregor Petrin

Reputation: 2931

I can't get it to work without resorting to custom binders :(

As for more powerful data binders, there is @BindUsing which allows you to either define a closure or implement the BindingHelper interface to format your input into whatever you need. You can even use GORM finders in there to populate properties with domain instances.

I managed to get this much:

class TheCommand {
    @BindUsing({ obj, source ->
        def details = new HashMap<Integer, List<Details>>().withDefault { [].withLazyDefault { new Details() } }
        source['details']?.collect { mapKey, mapValue ->
            if (mapKey.integer) {
                mapValue.collect { arrayKey, arrayValue ->
                    if (arrayKey.integer) {
                        def detailsObj = new Details(volume:new Integer(arrayValue.volume), price: new Double(arrayValue.price))
                        details[new Integer(mapKey)].add(new Integer(arrayKey), detailsObj)
                    }
                }
            }
        }
        return details
    })
    Map<Integer, List<Details>> details

    String location
}

It works for the following request curl http://localhost:8080/test/test/index --data "location=here&details.0.0.volume=20&details.0.0.price=2.2&details.0.1.volume=30&details.0.1.price=2"

It's powerful, but ugly (though my code has a few parts that could be implemented nicer). I don't know why a simple new Details(arrayValue) doesn't work in there but I may be missing something obvious.

Upvotes: 2

Related Questions