Skip to content


Grails one-to-many dynamic forms

After searching around trying to find a good way to implement one-to-many dynamic forms in Grails, I have finally come across this post which does a very good job at explaining the details.

What I was basically looking for is a clean way to implement saving my domain objects in the backend, rather than the hacked way I did by hand picking request parameters and manually setting up my domain objects (I’m still fairly new to Grails), and the official docs fail to shed the light on the subtle details I found in this post, which is why I decided to post my own version of the one-to-many dynamic forms but with a little bit more complex domain objects to illustrate the use of enums which also I found is a bit of a gray area in the docs (or at least maybe for me).
Make sure you head over and read the original post, as I will not go through all the details already mentioned over there. This example was developed using Grails version 1.3.3

The source code for everything I will be talking about can be grabbed as a .zip here


Update (28 Aug 2010): As Brad pointed out, removing a new phone that was still not persisted did not really remove the phone, this is because the dom elements were hidden instead of removed from the phone which caused the values to be submitted anyways. I’ve updated the source code in the post to fix that by adding a ‘new’ flag when it is still ‘true’ the form elements should be completely removed, while when it is false, they should stay since we want the deleted flag to be submitted.

Update (19 Oct 2010): Updated the source code to incorportate the fixes from the previous update as well as some other bugs. Make sure you grab this updated sources for a better reference. Also fixed a typo in the post as pointed out by Brad.

Update (11 Feb 2011): Added a column name mapping for the Phone domain class which caused problems creating the table in MySQL since the column name “index” is a reserved word


I will be building a simple ‘Phone Book’ application that allows you to create Contacts were each contact can have multiple phone numbers. I will start by defining the domain classes. First up, our Contact

package blog.omarello

import org.apache.commons.collections.list.LazyList;
import org.apache.commons.collections.FactoryUtils;

class Contact {
    static constraints = {
        firstName(blank:false)
        lastName(blank:false)
    }
    String firstName
    String lastName
    String nickName
    List phones = new ArrayList()
    static hasMany = [ phones:Phone ]

    static mapping = {
        phones cascade:"all-delete-orphan"
    }

    def getPhonesList() {
        return LazyList.decorate(
              phones,
              FactoryUtils.instantiateFactory(Phone.class))
    }

    def String toString() {
        return "${lastName}, ${firstName}"
    }
}

The important thing to note here is the LazyList and the cascade mapping. according to the above our contact will be able to store a list of phones of type Phone. Our next domain class is the Phone class which looks like this.

package blog.omarello

class Phone {
    public enum PhoneType{
        H("Home"),
        M("Mobile"),
        W("Work")

        final String value;

        PhoneType(String value) {
            this.value = value;
        }
        String toString(){
            value;
        }
        String getKey(){
            name()
        }
        static list() {
            [H, M, W]
        }
    }

    static constraints = {
        index(blank:false, min:0)
        number(blank:false)
        type(blank:false, inList:PhoneType.list(), minSize:1, maxSize:1)
    }

    static mapping = {
        index column:"phone_index"
    }

    int index
    String number
    PhoneType type
    boolean deleted
    static transients = [ 'deleted' ]
    static belongsTo = [ contact:Contact ]

    def String toString() {
        return "($index) ${number} - ${type.value}"
    }
}

I have added an enum here to show how you can persist enums and use them in your constraints (there might be better ways to do this I am open to suggestions). The enum will define the phone types that I can use for each phone number, and will be rendered as a select list on the page. The index integer is just to keep track of the position of each phone number in the list (I might come back to this in a later post), the rest is quite similar to the original post. The deleted boolean will be used to keep track of which objects were deleted in the view so that they can be later deleted from the database which is why is defined in the transients list (since we don’t want to persist this property)

Now that we have defined that a contact can have many phone numbers and a phone number belongs to contact, we can go ahead and start writing our views. You can stub out a controller and the corresponding views using grails generate-all blog.omarello.Contact and then edit them accordingly.

What we will do now, is create three template .gsp files which we will use to keep things clean and avoid repetition in multiple files:

  • _contact.gsp : This will be used to render the shared input fields within the create.gsp and edit.gsp file. Keeps things cleaner and avoids repetition
  • _phones.gsp: This will contain out main javascript logic (using jquery) and a loop to render each phone entry
  • _phone.gsp : This will represent an individual phone entry

_contact.gsp will basically contain the view contents that were stubbed out by the generate-all command, make sure to check the source code for the full picture, but the most important part is rendering the _phones.gsp template in place of the default list we get from ‘generate all’, notice how we pass our contact instance object to the template.

....
....
<tr class="prop">
   <td valign="top" class="name">
      <label for="phones"><g:message code="contact.phones.label" default="Phones List" /></label>
   </td>
   <td valign="top" class="value ${hasErrors(bean: contactInstance, field: 'phones', 'errors')}">
       <!-- Render the phones template (_phones.gsp) here -->
       <g:render template="phones" model="['contactInstance':contactInstance]" />
       <!-- Render the phones template (_phones.gsp) here -->
   </td>
</tr>
....
....

Note that when rendering templates you do not have to include the underscore that is in the file name.

Now our dynamic javascript behavior is contained in the _phones.gsp file (javascript can be included in an external .js file if you want to) which looks something like this.

<script type="text/javascript">
    var childCount = ${contactInstance?.phones.size()} + 0;

    function addPhone(){
      var clone = $("#phone_clone").clone()
      var htmlId = 'phonesList['+childCount+'].';
      var phoneInput = clone.find("input[id$=number]");

      clone.find("input[id$=id]")
             .attr('id',htmlId + 'id')
             .attr('name',htmlId + 'id');
      clone.find("input[id$=deleted]")
              .attr('id',htmlId + 'deleted')
              .attr('name',htmlId + 'deleted');
      clone.find("input[id$=new]")
              .attr('id',htmlId + 'new')
              .attr('name',htmlId + 'new')
              .attr('value', 'true');
      phoneInput.attr('id',htmlId + 'number')
              .attr('name',htmlId + 'number');
      clone.find("select[id$=type]")
              .attr('id',htmlId + 'type')
              .attr('name',htmlId + 'type');

      clone.attr('id', 'phone'+childCount);
      $("#childList").append(clone);
      clone.show();
      phoneInput.focus();
      childCount++;
    }

    //bind click event on delete buttons using jquery live
    $('.del-phone').live('click', function() {
        //find the parent div
        var prnt = $(this).parents(".phone-div");
        //find the deleted hidden input
        var delInput = prnt.find("input[id$=deleted]");
        //check if this is still not persisted
        var newValue = prnt.find("input[id$=new]").attr('value');
        //if it is new then i can safely remove from dom
        if(newValue == 'true'){
            prnt.remove();
        }else{
            //set the deletedFlag to true
            delInput.attr('value','true');
            //hide the div
            prnt.hide();
        }
    });

</script>

<div id="childList">
    <g:each var="phone" in="${contactInstance.phones}" status="i">
        <!-- Render the phone template (_phone.gsp) here -->
        <g:render template='phone' model="['phone':phone,'i':i,'hidden':false]"/>
        <!-- Render the phone template (_phone.gsp) here -->
    </g:each>
</div>
<input type="button" value="Add Phone" onclick="addPhone();" />

I have used a different approach than the original post here, I am basically relying on cloning an existing dom object rather than creating the html by hand since this will get a bit messy when you have some non-trivial form. Using jquery selectors to update my input ids in the addPhone() function (e.g. The selector input[id$=deleted] will find all inputs with an id attribute that ends with deleted). Also I am using jQuery’s live() to bind click events to my remove button, I find this a little bit cleaner.
I will come back to the dom object that will be cloned in a second, but the hidden:’false’ part in the render is related to the clone.

The above template loops over all the phones that a contact has, and renders them using this template (_phone.gsp)

<div id="phone${i}" class="phone-div" <g:if test="${hidden}">style="display:none;"</g:if>>
    <g:hiddenField name='phonesList[${i}].id' value='${phone?.id}'/>
    <g:hiddenField name='phonesList[${i}].deleted' value='false'/>
    <g:hiddenField name='phonesList[${i}].new' value='false'/>

    <g:textField name='phonesList[${i}].number' value='${phone?.number}' />
    <g:select name="phonesList[${i}].type"
        from="${blog.omarello.Phone.PhoneType.values()}"
        optionKey="key"
        optionValue="value"
        value = "${phone?.type?.key}"/>

    <span class="del-phone">
        <img src="${resource(dir:'images/skin', file:'icon_delete.png')}"
            style="vertical-align:middle;"/>
    </span>
</div>

The important thing to note here is the use of classes that are used for the jQuery selectors (class=”phone-div” for the main div and class=”del-phone” for the delete button), also the use of the g:select tag to render the enum list with the appropriate key/value.

Now we need to add the html part that will be cloned, and for that we will render the _phone.gsp template in the create.gsp and edit.gsp files (outside of the g:form tag) but we will set the hidden attribute to true so that this div is not visible to the user, but can be cloned by our javascript code like so

......
......
        </g:form>
    </div>
    <!-- Render the phone template (_phone.gsp) hidden so we can clone it -->
    <g:render template='phone' model="['phone':null,'i':'_clone','hidden':true]"/>
    <!-- Render the phone template (_phone.gsp) hidden so we can clone it -->
  </body>
</html>

Our views are now ready for prime time. The most important thing that we have accomplished here is that we are keeping track of the deleted flag on the hidden input which will be used in the controller to delete the phones from the database.

Now we need to make a little change to the controller so that we can remove the phones marked as deleted , plus I have added an extra loop to reset/update the index numbers of my phones (not relevant now, might talk about it later). Note that due to the naming convention of our form inputs, grails can automatically bind the request parameters and auto-magically create the domain class object for us. The controller’s update action should contain the following

......
......

contactInstance.properties = params

// find the phones that are marked for deletion
def _toBeDeleted = contactInstance.phones.findAll {(it?.deleted || (it == null))}

// if there are phones to be deleted remove them all
if (_toBeDeleted) {
    contactInstance.phones.removeAll(_toBeDeleted)
}

//update my indexes
contactInstance.phones.eachWithIndex(){phn, i ->
    phn.index = i
}
if (!contactInstance.hasErrors() && contactInstance.save(flush: true)) {
......
......

That’s it, we should be all set. Let give it a spin…. grails run-app and fireup your browser. here are some action shots.

Create a new contact

Create Contact

Contact Created

Update contact

Update Contact

List contacts

List Contacts

One thing I wanted to show was validation, but I could not make it work for the phone numbers, as you can see in this image, I am getting a validation error for only one of the empty phone numbers, and I did not know how to highlight the input box that was triggering this error, so if you have an ideas please don’t be shy to share them.

Validation

I hope someone finds this useful, as I have struggled to find resources that outline how to persist enums as well as how to deal with request parameters for one-to-many relationships without having to hack your way into hand crafting the domain classes. Off course kudos to the original poster for his comprehensive example.

Update (28 Aug 2010): As Brad pointed out, removing a new phone that was still not persisted did not really remove the phone, this is because the dom elements were hidden instead of removed from the phone which caused the values to be submitted anyways. I’ve updated the source code in the post to fix that by adding a ‘new’ flag when it is still ‘true’ the form elements should be completely removed, while when it is false, they should stay since we want the deleted flag to be submitted.

Posted in Programming.

Tagged with , , , , .


53 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Jerome says

    Thanks a lot, that was very useful.

  2. Brad Rhoads says

    Thanks for the great article. One small problem – On create, if you add a phone and then delete it, you get a validation error and the screen refreshes with the the blank phone that was previously deleted.

  3. omarji says

    Thanks Brad, I’ve updated the post to fix this. The problem was that the div was being hidden instead of removed from the DOM which caused the inputs to submit to the server causing the validation error.

  4. thom says

    Excellent post! Thanks! :-)

  5. John Bertrand says

    It doesn’t look like your latest source code has the new flag on create. Could you update the source? Many thanks

    • omarji says

      I’ve uploaded the source code with the latest updates as well as some other bug fixes.

  6. Brad Rhoads says

    It would be nice to have an updated application to download. But I did get it to work…

    You need to take everything from _phones.gsp. Then you need to update _phone.gsp to add

    Also note there is a minor error in the narrative. It say:

    The above template loops over all the phones that a contact has, and renders them using this template (_phones.gsp)

    but should say:

    The above template loops over all the phones that a contact has, and renders them using this template (_phone.gsp) // notice _phone.gsp vs _phones.gsp

  7. Mathew says

    Great! Thanks for help!

  8. ed209 says

    This is nice, but I can’t get it working with a MySQL DB. I’ve tweaked the DB dialect with no effect, and have the latest connector.jar Thoughts?

    2011-01-28 09:46:20,323 [main] ERROR hbm2ddl.SchemaExport – Unsuccessful: create table phone (id bigint not null auto_increment, version bigint not null, contact_id bigint not null, index integer not null, number varchar(255) not null, type varchar(255) not null, phones_idx integer, primary key (id))
    2011-01-28 09:46:20,325 [main] ERROR hbm2ddl.SchemaExport – You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘integer not null, number varchar(255) not null, type varchar(255) not null, phon’ at line 1
    2011-01-28 09:46:20,348 [main] ERROR hbm2ddl.SchemaExport – Unsuccessful: alter table phone add index FK65B3D6EA9F010F1 (contact_id), add constraint FK65B3D6EA9F010F1 foreign key (contact_id) references contact (id)
    2011-01-28 09:46:20,348 [main] ERROR hbm2ddl.SchemaExport – Table ‘genericdb.phone’ doesn’t exist

    • ben says

      I got the same error too…

      • ben says

        And it’s because the column “index” from the table phone seems to used a reserved word. Just need to modify index into indexo for example.

        Then in mysql if you do:
        create table phone (id bigint not null auto_increment, version bigint not null, contact_id bigint not null, indexo INTEGER not null, number varchar(255) not null, type varchar(255) not null, phones_idx integer, primary key (id));
        instead of
        create table phone (id bigint not null auto_increment, version bigint not null, contact_id bigint not null, index INTEGER not null, number varchar(255) not null, type varchar(255) not null, phones_idx integer, primary key (id));

        it will work,

        To fix that, you have to modify the following files:
        ContactController.groovy
        contactInstance.phones.eachWithIndexo(){phn, i ->
        phn.indexo = i
        phn.indexo = i

        Phone.groovy
        indexo(blank:false, min:0)
        int indexo
        return “($indexo) ${number} – ${type.value}”

        Et voila…

    • omarji says

      @ed What ben suggested would fix the problem since as he mentioned the column “index” is a reserved word.

      You could change the name as he mentioned. Or a better way to do it is change the column name mapping using the static mapping block.
      In the Phone domain class add the following

      static mapping = {
      index column:”phone_index”
      }

      I will update the original post to include this.

      Thanks for the feedback.

  9. Adrian says

    Great post!! You asked about an alternative way to use Enum, I’ve tried to do it using a persisted category list, as I want to be able to add/modify types without having to do any code change. But it keeps failing, I’m new to Grails, is there any obvious reason why it cannot be done that way?

    The error I get is: No signature of method: party.Phonetype.get() is applicable for argument types: (java.lang.String, java.lang.String) values: [445, 444] Possible solutions: get(java.lang.Object), getId(), getAt(java.lang.String), list(), getAll(), ident().

    I have updated my domain and views accordingly (replacing enum by Phonetype object):

    package party
    class Phone {

    Phonetype phonetype
    }
    class Phonetype {

    String typename
    }

    And then on _phone.gsp:

    For ${listItemsPhonetype} I’ve created a service to get the list of phone types, that works fine.

    Finally, on _phones.gsp I have:

    clone.find(“select[id$=phonetype]“)
    .attr(‘id’,htmlId + ‘phonetype.id’)
    .attr(‘name’,htmlId + ‘phonetype.id’);

    Any help much appreciated!

  10. Adrian says

    Hmm, the _phone.gsp code did not get posted. I’ll try again adding some spaces:

  11. Adrian says

    Once more:

    g:select name=”phonesList[${i}].phonetype.id”
    from=”${listItemsPhonetype}”
    optionKey=”id”
    optionValue=”typename”
    value = “${phone?.phonetype?.id}”

  12. Adrian says

    Just to comment on my previous post, in case anyone else wants to do it that way. My code works fine, there is just an error on _phones.gsp:

    clone.find(“select[id$=id]“)
    .attr(‘id’,htmlId + ‘phonetype.id’)
    .attr(‘name’,htmlId + ‘phonetype.id’);

    Once you update that you can get your phonetypes from the DB and do all the rest as usual.
    Thanks!!

    • omarji says

      Thanks for the info Adrian. Could be a good idea to add this information into another post maybe.

  13. Dustin Clark says

    Did anyone figure out a way to do this:

    One thing I wanted to show was validation, but I could not make it work for the phone numbers, as you can see in this image, I am getting a validation error for only one of the empty phone numbers, and I did not know how to highlight the input box that was triggering this error, so if you have an ideas please don’t be shy to share them.

    Thanks.

    • Brad Rhoads says

      I’ve been meaning to post this for awhile. You can loop through the detail records and check if each one is valid.

      def save = {
      log.debug "params:${params}"
      def lookupTableInstance = new LookupTable(params)

      def _toBeRemoved = lookupTableInstance.lookupValues.findAll {!it}
      log.debug "lookupTableInstance.lookupValues:${lookupTableInstance.lookupValues}"
      log.debug "_toBeRemoved:${_toBeRemoved}"

      // if there are lookupvalues to be removed
      if (_toBeRemoved) {
      lookupTableInstance.lookupValues.removeAll(_toBeRemoved)
      }

      def anyErrors = false
      lookupTableInstance.lookupValues.eachWithIndex(){lookupValue, i ->
      lookupValue.idx = i
      lookupValue.lookupTable = lookupTableInstance
      if (!regExpManagerService.testRegexp(lookupTableInstance.regexpression,lookupValue.thevalue)) {
      log.debug "reject!"
      lookupValue.errors.rejectValue("thevalue",
      "invalid.regexp",
      [lookupValue.thevalue,
      g.message(code:'regexName.'+ lookupTableInstance.regexpression.regexName,
      default:lookupTableInstance.regexpression.regexName
      ),
      g.message(code:lookupTableInstance.regexpression.theexpression + ".human",
      default:'Invalid format'
      )
      ] as Object[],
      "The value {0} does not match the regular expression."
      )
      log.debug "errors:${lookupValue.errors}"
      anyErrors = true
      }
      }

      if (!anyErrors && lookupTableInstance.save(flush:true)) {
      log.debug "did the save"
      flash.message = "lookupTable.created"
      flash.args = [lookupTableInstance.id]
      flash.defaultMessage = "LookupTable ${lookupTableInstance.id} created"
      redirect(action: "show", id: lookupTableInstance.id)
      }
      else {
      render(view: "create", model: [lookupTableInstance: lookupTableInstance,thisInstance:lookupTableInstance])
      }
      }

      But, I you can actually do it in the domain. After doing the above, I did the following in a different domain,but using the same technique as the article:


      static constraints = {
      setName(size: 1..45, blank: false)
      dateCreated(nullable: true)
      lastUpdated(nullable: true)
      customer(nullable: true)
      updatedByPerson(nullable: true)
      createdByPerson(nullable: true)
      setTagtypes(validator: {stts,obj,errors ->

      def valid = stts?.tagtype == stts?.tagtype?.unique()
      if (!valid) errors.rejectValue(
      "setTagtypes", "metadataSet.duplicate.tagtype", "Each Tagtype can only be used once.")
      return valid
      })
      }

      • omarji says

        Thanks Brad,

        I’ve actually forgot to update the post with this information after I had figured it out. I might write up a new post in order to show this. Might use some of the infromation you’ve presented if that’s ok.

    • Jiri says

      I tried this. I think it is not “clean” solution but it works.
      In controller I added this line
      contactInstance.phones.each {it.validate}
      just before
      if (!contactInstance.hasErrors() && contactInstance.save(flush: true)) {

      Which fill in errors property directly for each phones instance.
      Than in template you can normaly use (inside each)

      class=”value ${hasErrors(bean: phones, field: ‘phone’, ‘errors’)}”

      And thanks for very useful article!!!

      • Jiri says

        Sorry, small typo
        contactInstance.phones.each {it.validate()}

  14. Thomas says

    Where’s #phone_clone defined? Am not getting the phone.gsp template to be displayed for some reason.

    var clone = $(“#phone_clone”).clone()

    • Thomas says

      ouch found it, you should take your time to read the full post…

  15. Donal says

    You could remove PhoneType.values() and just use the PhoneType.values() which is present on all enums

  16. Brad Rhoads says

    If you’re using this technique with something like dependent list boxes, or more generally, need to have an event handler renamed dynamically, you can do it like this:

    oc = tagtypeSelect.attr(‘onchange’).toString().replace(“${i}”,childCount).replace(“onchange”,”");

    What this does is is retrieve the text of the onchange event of a select , in which I have I call a remoteFunction with update:’tagtype-options-${i}’. Then the ${i} is replaced with the childCount. And finally, function onchange(event){…} get’s changed to just function (event) {..} because jquery .change() expects just a function.

    For clarity, the entire select is:

    Once the string is set I do

    .change(eval(oc);

    Well that doesn’t work in IE, so I actually do:

    .change(eval_global(oc));

    with:

    function eval_global(codetoeval) {
    if (window.execScript)
    window.execScript(‘code_evaled = ‘ + ‘(‘ + codetoeval + ‘)’,”); // execScript doesn’t return anything`
    else
    code_evaled = eval(codetoeval);

    return code_evaled;
    }

  17. Dave says

    I ran your code and everything works great. I tried to convert it to my use-case and for some reason my List isnt getting populated in the create.

    here is a println of the params passed in in your code:
    [lastName:iohjl, phonesList[0].new:true, phonesList[0]:[id:, deleted:false, number:t786796, new:true, type:H], nickName:hh, phonesList[0].id:, phonesList[0].deleted:false, firstName:hjkh, phonesList[0].type:H, create:Create, phonesList[0].number:t786796, action:save, controller:contact]

    So phonesList should be saved as phones in the contact.

    what does the mapping from phonesList to phones?
    in my use-case i have the same println but my timeSlotList isnt getting saved in the timeSlot object in my Schedule.

    I believe i did everything as you did above but i must of missed something that maps the phonesList to phones object step.

    I have been staring at it for about 5 hours now…. any help would be greatly appreciated

    Thanks for the help

    • Dave says

      im sorry i just got it working… i guess all i had to do was ask for help ;) wish i had done that 5 hours ago

      • omarji says

        :) glad you got it working. It would be helpful to share if you had to do something special in order to fix it so I can update the post.

        • Dave says

          it was actually just me mistyping the LazyList part. Your doc is correct. The curse of dynamically typed languages ;)

  18. Amol says

    Hi,
    Thank you for this post! It was a life saver :-)
    However I was trying to check the behavior of Hibernate and the related DB schema.
    It seems like when we insert a child record, there is an insert into the child table with all the columns (including parents_idx column, which I guess GRAILS uses to map the indices of the children).
    However this is followed by an update of the child table to set the idx column above and the foreign key reference column. Any ideas why this is the case? It would be preferable to avoid this behavior if possible – an insert should be a single query. Any inputs?
    TIA!

  19. Dave says

    I am trying to nest templates essentially many to many dynamic forms but am running into issues.
    Essentially i want to have the ability to create a list of days and each day can have a list of timeslots.

    I can successfully create a list of days using this example and have been trying to insert a second template into each day(for the timeslots) Unfortunately every time i add a timeslot it inserts it into the first day.

    I believe the solution is to have a unique name for the timeslot div(corresponding to each day you dynamically add)

    I havent figured out how to pass this information to the template.

    How can i pass in a variable that i can reference in the template?

    Am i trying to approach this in the correct way? Thank you for the help

    • omarji says

      You can use the model attribute for the render tag to pass in variables. For example (from the code above)


      g:render template='phone' model="['phone':null,'i':'_clone','hidden':true]"

      This passes a null phone object, ‘i’ and ‘hidden’ variables to the template.

      Hope this helps.

  20. Bob says

    For some reasons some people uses Prototype & jQuery together.
    For this wonderful dynamic form to work they have to replace all $ (dollar signs) in the script in _phones.gsp with jQuery

    • Bob says

      and you have to write childCount = childCount + 1; instead of childCount++;

  21. Isaid Castri says

    Hi, I’m trying to reuse your technique for a class of photos instead of telephone, in view of the photo entry is a file type input, and when I do submit that taking the name of the picture, the get the name of the photo iterating getFileName () with getFile (fileName). OriginalFilename:

    request.getFileNames().each(){fileName ->
    def uploadedFile = request.getFile(fileName);

    if(!uploadedFile.empty && (uploadedFile.originalFilename != null)){
    def resource = new Photo(name : uploadedFile.originalFilename)

    def webRootDir = servletContext.getRealPath(“/”)
    def resDir = new File(webRootDir, “/res/uploads”)
    resDir.mkdirs()
    entryInstance.photos.add(resource)//here add photo to photo list of entryInstance
    if(resource.validate() && !resource.hasErrors() && entryInstance.validate() && !entryInstance.hasErrors() && resource.save()){

    uploadedFile.transferTo( new File( resDir, resource.id+”_”+resource.name))
    }

    }

    and manually added to the list of type photo of my container class, I mark the validation errors of the extension because the controller is adding the photo as if got it from of text input type, as I have to specify in the controller to pull the photo from the class name instead of request params?

    sorry for my bad English.

  22. Zvi says

    Great sample app – thanks!! We need to make this into a plug-in which will generate one-to-many forms automatically!

  23. sand says

    Thanks for this useful post.

    I was trying to run this application on Grails 2.0.0.RC1. I was able to create contact and phones with no problem. I was also be able to update a contact with NO phones, then add phones.

    However, when I tried to update a contact with existing phones, I got “Not a collection, array, or map: cannot resolve nested value types. Stacktrace follows:” exception at line: contactInstance.properties = params.

    I am wondering if anyone else have this problem?

  24. Alex says

    hey guys.

    I was wondering if you could help me.
    I am doing the same thing with grails 2.0.0.RC1 and on IE works fine, but in Firefox or Chrome the values never reach the server. From what i saw in the source of the generated HTML , the new components aren’t added to the DOM in Firefox/Chrome.
    Any hints on how to resolve this ?
    Thanx.

    • Alex says

      Never mind.
      I forgot the closing tag of a div in the code, and it was all messed up.

  25. Bob says

    I’am using Grails 2.0 and it throws the follwong exception in the save action:
    null id in test.TargetEvaluation entry (don’t flush the Session after an exception occurs)
    org.hibernate.AssertionFailure: null id in ….

    It works fine in 1.3.7. And the data sent is the same. Has anyone the same issue?
    Looks like the cascade does not work…

  26. Paulo says

    Very good article, thanks for sharing. I’m new to Grails, so if anyone can help me, I’d really appreciate. I’m trying to extend this example by associating Phone with a third domain called “product”.
    so, in my phone domain I have a line for:
    static belongsTo = [contact: Contact, product:Product]

    In my Product domain:
    static hasMany =[phones: Phone]

    In my phone view:

    When I try to save, I get an error message:
    Property [product] of class [class com.easyinvt.Phone] cannot be null

    It seems that Grails is not automatically mapping my product id to the Phone class.
    Note: I’m using Grails 2.0

    Thank you for the help
    Paulo

Continuing the Discussion

  1. Blog bookmarks 08/23/2010 « My Diigo bookmarks linked to this post on August 23, 2010

    [...] Train of Thought | Grails one-to-many dynamic forms [...]

  2. Tweets that mention Grails one-to-many dynamic forms -- Topsy.com linked to this post on August 23, 2010

    [...] This post was mentioned on Twitter by kevin's bookmarks and Proggit Articles, omarello. omarello said: #grails how-to: one-to-many relationships with dynamic forms http://is.gd/eumw1 [...]

  3. Introduction to Grails Development - Grails Tutorial linked to this post on September 20, 2010

    [...] Grails one-to-many relationships with dynamic javascript forms by Omar Marji (Aug 2010) [...]

  4. grailstutorial.org linked to this post on January 13, 2011

    Grails one-to-many dynamic forms…

    After searching around trying to find a good way to implement one-to-many dynamic forms in Grails, I have finally come across this post which does a very good job at explaining the details. What I was basically looking for is a clean way to implement s…

  5. Important Links for Grails | JAVA developer and research blog linked to this post on July 20, 2011

    [...] Many-to-Many relations and dynamic form [...]



Some HTML is OK

or, reply to this post via trackback.