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 , , , , .


40 Responses

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

  1. e cig pros and cons says

    You’re so interesting! I don’t suppose I’ve truly read through anything like that before.
    So nice to discover someone with unique thoughts on this subject.
    Seriously.. thanks for starting this up. This web site is something that is needed on the web,
    someone with a little originality!

  2. Dermajuvenate Skin care says

    Have you ever considered writing an e-book or guest
    authoring on other blogs? I have a blog centered on the same ideas you discuss and would really like to
    have you share some stories/information. I know my readers would
    appreciate your work. If you are even remotely interested, feel free to shoot me an
    e mail.

  3. Pengertian Jquery says

    Wow, marvelous blog layout! How long have you been blogging for?

    you make blogging look easy. The overall look of your web site is wonderful, as well as the content!

  4. Parchet triplu stratificat says

    Your style is unique in comparison to other folks I
    have read stuff from. I appreciate you for posting when you have the opportunity, Guess I’ll
    just bookmark this page.

  5. Parchet Triplu Stratificat says

    Hey I know this is off topic but I was wondering if you knew of any
    widgets I could add to my blog that automatically tweet my newest twitter updates.
    I’ve been looking for a plug-in like this for quite some time and
    was hoping maybe you would have some experience with something like this.
    Please let me know if you run into anything. I truly enjoy reading your blog and I look
    forward to your new updates.

  6. Exclusivepaddle34.Jux.com says

    Un puissant remerciement à l’auteur de ce blog

  7. sommerhuber says

    Wow, that’s what I was seeking for, what a information! existing
    here at this blog, thanks admin of this web page.

  8. Http://Cringehumorshow.Com says

    Ahaa, its noce conversation about thjs piece of
    writing at this place att this weblog, I have reawd
    all that, so at his time me also commenting here.

  9. April says

    Undeniably consider that which you stated. Your favorite justification seemed too be at
    the net the easiest factor to keep in mind of.
    I saay too you, I certainly get irked at the same time as folks think about concerns that they just
    don’t understand about. You controlled to hhit
    the nail upon thhe top and also defined out the entire thing with no need side-effects ,
    folks can take a signal. Will probbly be back
    to get more. Thank you

  10. lavanya says

    I am unable to delete children in edit page

  11. formula t10 says

    As I mentioned before, alcohol inhibits your cells from absorbing these essential nutrients effectively.
    Remember, it is wise to consult your doctor before starting any self-help regimen(s).
    Male characteristics such as a deep voice, body hair and animalistic traits come from testosterone and obviously the more testosterone ion the body the more
    prevalent these characteristics.

  12. web development quiz says

    In facct when someone doesn’t understand the its up to other users that they will assist,so here it takes place.

  13. social media marketing jobs atlanta says

    ʜello it’s me, I am also visitinbg thiѕ website dailʏ, this site is genuinely fastidious and the ρeoplpe
    are in fact ѕaring plewsant thoughtѕ.

  14. Tamera says

    I am regular visitor, how are you everybody? This article posted at this site is in fact pleasant.

  15. Orange County Employment Agency says

    Hi there, all is going perfetly here and ofcourse every one is
    sharing facts, that’s in fac excellent, keep up writing.

  16. best video games says

    Though, this would watch dogs cd keep them entertained.
    It will be unique and incorporates simplicity, the
    fantasy and science-fiction games have been collected.

  17. online degree in education says

    It’s actually a great and helpful piece of information. I’m glad that you simply shared this useful information with us.
    Please stay us up to date like this. Thanks for sharing.

  18. cheap Vikings jerseys china says

    cheap Bucks wholesale jerseys With Paypal Payment

  19. Natasha says

    How hard could it consumers be a daunting
    task. They can be added later. It is important to me that my family.
    Indeed, if consumers there is a licensed contractor, this can be
    confusing. Check out the rough task of installing new or existing home or your earnings.
    This is the best one. For this reason, before any of your home
    or in some areas may have available to commercial jobs like gutter pipe drainage installation and completion dates.

  20. herbal home says

    Hello! Someone in my Facebook group shared this site with us so I came to take a look.
    I’m definitely loving the information. I’m bookmarking and will be
    tweeting this to my followers! Terrific blog and superb style and design.

  21. Jessabelle (2014) Film says

    You have made some good points there. I checked on the internet for more information about the issue and
    found most individuals will go along with your views on this site.

  22. meh-la says

    hi. i’m using grails v.2.4.2 and i was wondering if there’s any way to import the lazylist and factoryutils using this version because when i run the app, importing those two causes error:

    ERROR context.ContextLoader – Context initialization failed
    Message: Error creating bean with name ‘pluginManager’ defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Invocation of init method failed; nested exception is java.lang.ExceptionInInitializerError
    Caused by ExceptionInInitializerError: null
    Caused by RuntimeException: No suitable ClassLoader found for grab

    but if i don’t import those two, the app runs perfectly except it doesn’t returns the phonelist

  23. sprzątanie sprzątanie biur lublin says

    Howdy! Would you mind if I share your blog with my facebook group?
    There’s a lot of people that I think would really enjoy your content.
    Please let me know. Thanks

  24. pit rozliczenie internetu says

    You really make it seem so easy with your presentation but I
    find this matter to be actually something which I think I would never understand.
    It seems too complicated and extremely broad for me. I am looking forward for your next
    post, I’ll try to get the hang of it!

  25. Sports Gambling says

    Incredible! This blog looks just like my old one! It’s on a totally different topic
    but it has pretty much the same layout and design. Superb choice
    of colors!

  26. Jayson says

    Wonderful, what a web site it is! This website provides useful information to us, keep
    it up.

  27. SB Game Hacker apk says

    Hi there mates, good paragraph and fastidious arguments commented here, I am actually enjoying by these.

  28. antiqueslotmachinesforsale.com says

    Wow, that’s what I was looking for, what a stuff! present here at this website, thanks admin of this web page.

  29. Construction Loan Experts says

    Construction lending is often considered to be a more risky type of lending that others.
    Once your construction is completed, everything will be
    rolled into a traditional mortgage at a lower interest rate.
    As soon the money is returned, the interest stops being
    levied and the account operates normally like before.

  30. construction loans in california says

    In the process of constructing a new home, there are two types of loans that are offered
    to customers-New home construction loans and stated income construction loans.

    Bank of America Corporation is an American multinational banking and financial services corporation. I gather
    that, if and to the extent the receiver was
    negligent, it was due in part to inadequate funding by the
    lender.

  31. antennas for HDTVs says

    Thank you, I have recently been looking for info about this subject for
    a while and yours is the greatest I have discovered till now.
    But, what in regards to the bottom line? Are you
    certain about the source?

  32. Florene says

    I have read so many posts regarding the blogger
    lovers but this post is really a nice post, keep it up.

  33. Websiphon review says

    You can certainly see your enthusiasm within the article you write.

    The arena hopes for even more passionate writers like you who are not afraid to say how they believe.

    All the time go after your heart.

  34. Fitness Certification says

    Good article! We will be linking to this particularly great post on our
    site. Keepp up the grat writing.

  35. Google says

    It will also increase the ranking of your website on search engines and will drive more traffic to your website.
    Based on their experience, they could know how much is required before going into details.

    But it seems Memorial Day wasn’t important enough to Google.

Continuing the Discussion

  1. Michelle's Hair Color Boutique linked to this post on May 7, 2014

    Michelle’s Hair Color Boutique

    Grails one-to-many dynamic forms – train of thought

  2. deemoz.net linked to this post on August 17, 2014

    deemoz.net

    Grails one-to-many dynamic forms – train of thought

  3. diy computer desk linked to this post on August 22, 2014

    diy computer desk

    Grails one-to-many dynamic forms – train of thought

  4. diet green coffee linked to this post on August 26, 2014

    diet green coffee

    Grails one-to-many dynamic forms – train of thought

  5. blogspot.fi linked to this post on September 19, 2014

    blogspot.fi

    Grails one-to-many dynamic forms – train of thought



Some HTML is OK

or, reply to this post via trackback.