An approach to processing dynamic one-to-many forms with Grails 2.1

I was recently faced with a (rather common) problem: process a form submission containing fields for a ‘parent’ and several ‘child’ records. For example, a form to create a Team record along with its many Player records. This is not a specialized problem, and certainly many developers have solved it one way or another, using the techniques available and applicable to their projects. I’m going to describe an approach to this problem for a web application using Grails 2.1 and a jQuery UI plugin, writetable.

Here is a brief description of the problem, with a simple example: We have two domain objects, Team and Player. There is a one-to-many association betweenTeam and Player. We want to allow users to create a Team, along with its Players, all in one form. Users should also be able to edit a Team, insert new Players, and edit or remove existing Players.

The Grails documentation does not provide much guidance to solving this problem, other than the brief section titled Data Binding and Many-ended Associations.  You may find, as I did, the following blog post helpful, although it seems to be a bit outdated for Grails 2.1 users: http://omarello.com/2010/08/grails-one-to-many-dynamic-forms/ . My approach is essentially a revision of omarello’s post, updated slightly for Grails 2.1. I’m also using a custom jQuery UI plugin (writetable) to generate the ‘child’ input elements, but that is not the main purpose of this article.

Our goal is to take advantage of Grails’ powerful data binding capabilities, while providing a simple and convenient form for our end-users and maintaining control over how Player objects might be removed. I’ll go over a very simple example to demonstrate the technique generally. Here is our Team class:

class Team {
    String name
    List player

    static hasMany = [players : Player]

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

And here is our Player class:

class Player {   
    String firstName   
    String lastName   
    String position
    boolean deleted
    static transients = [ 'deleted' ]
    static belongsTo = [team : Team]

    static constraints = {       
        firstName(blank: false)       
        lastName(blank: false)       
        position(blank: false)   
    }
    String toString() {       
         "id=$id : ${firstName} $lastName, $position"   
    }
}

The most important things to note here are the transient ‘deleted’ property of Player. As we’ll see, this plays a role in giving us control over how Players are removed from a Team. We define Team.players as a List so we can preserve the order of entries in the collection and access them with an index. Note that our design assumes that a Player is completely dependent on a Team, and can only belong to one Team. The “all-delete-orphan” mapping on Team.players ensures that a Player will be deleted when removed from a Team.  Also note a difference between our Team class and omarello’s Contact class: we do not have to add a special getter method to decorate the ‘players’ list with Apache Commons LazyList to grow the list automatically when an index value greater than the size of the list is used. Apparently Grails now does this for us implicitly.

Now let’s look at the controller code. To create a new Team, would could use a save() action as simple as:

def save() {        
    def team = new Team(params)       
    team.save()       
    redirect(action: 'show', params: [id:team.id])   
}

That was easy. We’re using the data binding features of Grails to automatically populate our Players list. There is a convention we must follow to take advantage of this, as the Grails documentation describes. We need to provide request parameters with the following naming convention: players[n].firstName, players[n].lastName, players[n].position, etc…, where ‘n’ is the 0-based position of a Player in the Team.players List.  The writetable jQuery UI plugin takes care of generating those HTML form inputs for us.

We don’t need any logic here regarding the removal of players because this is only to be used for the creation of new Teams. To edit existing Teams, we use the following update() action:

def update() {       
    def team = Team.get(params.id)       
    team.properties = params
    team.players.removeAll{ it.deleted }

    team.save()       
    redirect(action: 'show', params: [id:team.id])   
}

This is where things are a bit more interesting. When an end-user removes a player from the team, we add a hidden input element named players[n].deleted and give it a value of ‘true’ (again, where ‘n’ is the index value of the row in the players list). Grails will update the removed Players accordingly. All we have to do is use the removeAll method to remove the Players that have been marked for deletion.

While I’m not crazy about ‘polluting’ the model with what I consider to be view/controller concerns (I’m referring to the ‘deleted’ property in Player to support the management of the Team.players list), the alternatives (as my limited imagination was able to recognize) were decidedly worse. For example, I could have implemented some Ajax solution, where an asynchronous call is issued when a user edits/deletes a Player. Actually that could be a good way handle it, although it’d be more ‘chatty’. Another idea was to break up the form into a wizard. Yuck. Anyway – the approach described here seemed to best fit the “Do The Simplest Thing That Could Possibly Work” principle, overall. Thanks and credit to omarello for his earlier description of the technique.

The full example source can be found here: https://github.com/sflahave/writetable-example-with-grails

One thought on “An approach to processing dynamic one-to-many forms with Grails 2.1

  1. Does this really have anything to do with a particular version of Grails?

    Also, you might want to check that the save() calls succeed before redirecting.

  2. sflahave says:

    Hi Burt – Great to hear from you. I would guess that this approach should work fine with older (or newer) versions of Grails, though I haven’t had the time nor the inclination to test it thoroughly on prior versions. As I mentioned, the technique I described is a modification of Omarello’s post. Omarello used Grails 1.3.3. His post was also a revision of an earlier post describing the same general technique, but using Grails 1.1.1: http://www.2paths.com/2009/10/01/one-to-many-relationships-in-grails-forms/ . Even that post is a revision of an earlier post.
    My intent was only to make clear that I used Grails 2.1 to experiment with this technique. Perhaps I should have been more specific: I used Grails 2.1.1.
    Regarding your second point, I agree that you would want to add some validation and other logic in a real-world application. For the example, I kept it simple for the sake of … keeping it simple.
    Thanks,
    Shawn

  3. sflahave says:

    Minor correction: Regarding the point I made about growing the ‘players’ list automatically: I said “Apparently Grails does this for us automatically,” and we therefore do not have to bother with using the Apache Commons LazyList. It is not Grails that does this for us, but a feature of Groovy. Groovy will grow collections for you automatically (like Ruby does). For example:

    def books = []
    books[4] = “Lord of the Flies”

    This result will be: [null, null, null, null, Lord of the Flies] , rather than the IndexOutOfBoundsException you’d get with the following Java code:

    List books = new ArrayList();
    books.set(4, “Lord of the Flies”);

  4. Mikhail says:

    Help, please! How to populate
    rowRemoved: function(event, row) {
    then there are more than one such at page?

    1. sflahave says:

      I’m not sure I understand the question. The example mentioned in the article shows how you might implement a ‘rowRemoved’ callback handler. The intent is that there would be only one such handler for the ‘rowRemoved’ event per ‘writetable’ instance. Normally you’d want to know what row was removed, so that is the second parameter to the callback.

  5. Bj Ghim says:

    I am not able to delete the row in the example.

    Is there different way of binding event?

    $(‘img.deleteRowButton’).on(“click”, function(event)

    1. sflahave says:

      Are you getting any error? What browser/version are you using? The example uses “console.debug” which may not work in certain browsers (such as certain versions of IE). Sorry, I should’ve removed those. It works fine in Chrome.

  6. Sam says:

    How can I expand this example with one of the input field is a combobox?

    1. sflahave says:

      Sam – the writetable plugin currently only works with text fields. It would have to be updated to add support for specifying other types of controls. Feel free to fork the repo and submit a pull request if you’d like.

  7. Sam says:

    Thanks for your reply. What about setting a default value for one of the input field?

  8. Sam says:

    Sorry, I should be more specific. The default value is derived from a variable.

  9. Vimm says:

    This post is awesome, it has been a huge help. I think there’s a typo in the Team class though, it should read “List players” with an “s”.

    Have you tried this with newer Grails releases? For example with Grails 2.3.4 you can streamline it even more.

    def save(Team team) {
    team.save()
    redirect(action: ‘show’, id: team.id)
    }

    def update(Team team) {
    team.players.removeAll{ it.deleted }
    team.save()
    redirect(action: ‘show’, id: team.id)
    }

  10. Chlebik says:

    Hi,

    solution presented is nice but there is a problem – what with situation in which List (in given examples list of players) contains ONLY strings? Not separate domain object but simple String.

    I got problem with it because plugin creates input elements with name: “players[0].” and it causes problems with save action. Of course I can change the name but I assume it would be nice if plugin provided support for such situations.

  11. Rodrigo Barbosa says:

    Hi! I ran into a problem that is the set/get is not being created in the Player controller automatically. So itś not being add to Team in the update() and not being deleted. It was only put them there and everything went fine.
    I’m using Groovy 2.4.4 and grails 2.5.1

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*