I looked around for a simple example of data binding to handle a one-to-many association and never really found a full example so I decided to create an example and share it. The example uses a Person domain object with a one-to-many association of Address objects. All of the data is captured on a single screen. The example uses DHTML or Dom scripting to dynamically create multiple address form fields and submits all the data from one screen.
Domain objects: Person & Address
The Person constructor contains just a first and last name. Notice the Address field implements Serializable and also implements the hashCode() and equals() methods as required for objects that are to be placed in a collection.
class Person { static hasMany = [addresses:Address] static constraints = { } Person() { addresses = [] } String firstName String lastName List<Address> addresses String toString() { firstName + " " + lastName + " Addresses: "+addresses } } class Address implements Serializable { static constraints = { } String street String city String zip String toString() { street + "\n" + city + ", " + zip } int hashCode() { street?.hashCode() + city?.hashCode() + zip?.hashCode() } boolean equals(o) { if (this.is(o)) return true if (!(o instanceof Address)) return false return street?.equals(o.street) && (city?.equals(o.city)) && (zip?.equals(o.zip)) } }
Sample screen shot of create person screen
Below is a snapshot of a the create.gsp screen after clicking the 'Add Address' button twice and filling in the data. The 'Add Address' button calls a javascript function (see further below) to create an additional address row in the address table. (I wanted to provide the entire page .gsp source but could not get it formatted correctly - if anyone has suggestions, I would appreciate hearing from you.)
Add Address javascript function
Below is the javascript function called from the 'Add Address' button on the form. The important part here is that use of the hidden field, addrCount, to add an index to the address form field names and ids.
function addAddr(tblId) { var tblBody = document.getElementById(tblId).tBodies[0]; var newNode = tblBody.rows[0].cloneNode(true); var count = parseInt(document.getElementById("addrCount").value); count++; document.getElementById("addrCount").value = count; var cells = newNode.cells; for (var i=0; iif (cells[i].firstElementChild.id.indexOf("street") != -1) { cells[i].firstElementChild.id = "addresses["+(count-1)+"].street"; cells[i].firstElementChild.name = "addresses["+(count-1)+"].street"; } else if (cells[i].firstElementChild.id.indexOf("city") != -1) { cells[i].firstElementChild.id = "addresses["+(count-1)+"].city"; cells[i].firstElementChild.name = "addresses["+(count-1)+"].city"; } else if (cells[i].firstElementChild.id.indexOf("zip") != -1) { cells[i].firstElementChild.id = "addresses["+(count-1)+"].zip"; cells[i].firstElementChild.name = "addresses["+(count-1)+"].zip"; } } tblBody.appendChild(newNode); }
Here is the initial statement for the hidden addrCount index field.
<g:hiddenField name="addrCount" value="1" />
PersonController
When the form is submitted, the save action is called. I have created a helper method, bindPerson(), to handle the data binding. The method iterates 'addrCount' times, creating a new Address object, calling the bindData() method provided by Grails with the params starting with the value 'addresses[i]'. Lastly, the address is added to the collection and the Person object is returned.
The controller code below works party because my constructor for the Person object initializes the addresses field to an empty list. Other options might be to create a list in the controller and then set the addresses field to that list, or keep the initialized list and call personInstance.addToAddresses(addr).
class PersonController def save = { def personInstance = bindPerson(params) if(!personInstance.hasErrors() && personInstance.save()) { flash.message = "Person ${personInstance.id} created" redirect(action:show,id:personInstance.id) } else { render(view:'create',model:[personInstance:personInstance]) } } def Person bindPerson(params) { def personInstance = new Person() def count = params.addrCount.toInteger() for (int i=0; i<count; i++) { def addr = new Address() bindData(addr, params["addresses["+i+"]"]) personInstance.addresses[i] = addr } personInstance.properties = params return personInstance }
Results
Here you see the results on the show.gsp screen.
Wrap up:
- First you need to setup your domain objects, including implementing Serialization, hashCode() and equals(), where required.
- Form fields need to be name using index, like addresses[0].city or addresses[3].zip
- Finally, the controller needs to bind the association data using the indexed fields.
Note: Please see an improved solution in Part 2
I hope this helps and as always, constructive criticism is appreciated! Due to the large number of requests, I have created a site over at Groovy sites and you should be able to download from here. Download site is https://sites.google.com/site/mikesgroovystuff/
Hi. I'm new to grails. your article helped me so much, but i wonder how to create that master-detail form (person form). Could you provide the source-code for those GSPs? if you cannot post here, please send to my email. thanks!
ReplyDeletewrajaka [at] yahoo [dot] com
great post. It helped a lot
ReplyDeleteExcelent! Could you provide the source-code for those GSPs? If you cannot post here, please sende to my email. thanks!
ReplyDeleteMarco - please send me your email address so that I can send you the GSPs.
ReplyDeleteThanks for the blog.
ReplyDeleteBravo!!! Could you provide the code for those GSP?
ReplyDeleteacoronadoiruegas[AT]yahoo[dot]com
super ! Could you plz send me the code for those GSP ?
ReplyDeletemy email is minhchau_mychinh@yahoo.com
thank u so much !
super ! cuold u plz send me GSP code ?
ReplyDeleteminhchau_mychinh[at]yahoo[dot]com
thank u so much !
Perfect, could you please provide the source code for those GSP?
ReplyDeletechawki.mguedmini@gmail.com
Great, could you please provide me the source code for those GSP? I would really appreciate it
ReplyDeletevicapa99@gmail.com
hi,
ReplyDeletedid anybody this example running ? would it be possible to provide the complete source ?
cheers
andreas
Andreas - just post your email address and I will send you the complete source. I have it all zipped up and ready to go.
ReplyDeletehello mike,
ReplyDeletethat would be great. pls, send it to andreas "at" habeebee "dot" net
thanks in advance
andreas
Mike - GREAT article. I'd like to see the source as well.
ReplyDeletekelly.humbert@gmail.com
I'd like to see it too, please.
ReplyDeletebdrhoa at gmail dot com
Thanks!
Great post, would you please send me the sources too?
ReplyDeleteues@udo-esser.de
Thx a lot.
Really great article; and like the others here would you mind sending me the sources as well to share what you've accomplished?
ReplyDeletesjlum at yahoo dot com
One suggestion for you is to setup a project at googlecode(free) where people could SVN checkout.
Thanks!
really great post! could u send me the gsp code as well
ReplyDeleteMash - I need your email address to send the code to???
ReplyDeletePlease send me the source code too..
ReplyDeletemy email id is vb1900 at gmail.
Hi! Great post! Could you please send the code to rcartas (at) hotmail.com
ReplyDeleteThanks
Great post. Would you please send the code to sultana.nashid.zaman@gmail.com
ReplyDeleteThanks for the great post. My very limited knowledge of JS is really going to show here. This this method, how can I delete an address row I created when pressing the button but later decided I didn't need it.
ReplyDeleteThanks.
@Me - sorry but I am not going to be your best source for JS. The gist of what you want to do is to get the tbody element and the you can call the deleteRow(idx) method to delete the row referenced by the idx index.
ReplyDeleteHi Mike, please can you send me full source code to vhac@myself.com
ReplyDeletethks
Víctor Hugo
Hello Mikie, your example is exactly what i'm looking for. Can you send the source to swmw[at]poczta.fm
ReplyDeleteThanks
Jan Luca
Hi,
ReplyDeletecan you please share the gsp code.My id: shiuli3@gmail.com
This comment has been removed by the author.
ReplyDeleteHi ,
ReplyDeleteCan u please provide gsp pages source code . My id is ranidurga.lavanya@gmail.com