Wednesday, December 29, 2010

Dynamic forms, LazyList and transparent items removal


Browsing internet you can easily find a common method of implementing dynamic forms in JEE world. This is usually done using LazyList from apache commons-collections, or AutoPopulatingList from Spring. I don't want to repeat these descriptions, you can find nice examples here (using LazyList) or here (using AutoPopulatingList).

In a nutshell, you can implement such list in your object representing form and dynamically add items using javascript:

public class MyFormObject {
  private List elements = 
    LazyList.decorate(new ArrayList(),
      FactoryUtils.instantiateFactory(MyElement.class));
}

Then you can create the form:

<input name="myFormObject.elements[0].property" />
<input name="myFormObject.elements[1].property" />

And in JavaScript add dynamically new elements, for example:

<input name="myFormObject.elements[0].property" />
<input name="myFormObject.elements[1].property" />
<input name="myFormObject.elements[2].property" /> <!-- dynamically added element--!>
<input name="myFormObject.elements[3].property" /> <!-- dynamically added element--!>

After submitting this form, using whatever Ultimate Binding JEE Engine your're using, you will have new items auto populated into your list.

This example shows LazyList usage, but looking in the AutoPopulatingList source it seems that Spring guys chose approach, what I can call "we want to have LazyList but let's rewrite it from the scratch in the same way using different name".

The problem with items removal

Anyway, both implementations don't assume that items could be removed by the user. For example, if you have an empty form, and then:

In step 1 user is adding two fields:

<input name="myFormObject.elements[0].property" />
<input name="myFormObject.elements[1].property" />

In step 2 user is removing first (index=0) field:

<input name="myFormObject.elements[1].property" />

We have situation in which our automatic list, after first call to (index=1) element is populating both (index=0) and (index=1) elements to assure that we have list with requested size.

The solution

There are many solutions for this problem, from reindexing forms in js before submitting (a hard one, having for example 3 levels of nested dynamic forms in single form) or playing around collections on server side. It is the same as solving any other programmatic problem. But here I'd like to present the most simple working solution I figured out.

It's based on two observations. First is that our lists are filled first by null-s, and after that the factory creates items of our type. In the beginning unused elements are just null-s.

Second one is about what we usually are doing next with the list. The most frequently we are using iterator to access list items to do something with them:

1) by explicit iteration using iterator:

for (Iterator i=collection.iterator(); i.hasNext(); ) {}

2) by implicit iteration using iterator:

for (Object o: collection) {}

3) or, for example, using <c:foreach> jsp tag:

<c:foreach items="${collection}" var="item">
 ...
</c:foreach>

4) and even doing:

myOtherCollection.addAll(collection);

Each of these methods use the same collection iterator. Thus, because this is common way to use collections after binding, the most simple idea I had is to clean each collection before access it with iterator. It's very simple to achieve with LazyList, and I believe that it's simple with AutoPopulatingList as well.

The implementation

First we will write our own list decorator class:

public class ShrinkableLazyList extends LazyList {

  protected ShrinkableLazyList(List list, Factory factory) {
    super(list, factory);
  }

  /**
  * Decorates list with shrinkable lazy list.
  */
  public static List decorate(List list, Factory factory) {
    return new ShrinkableLazyList(list, factory);
  }

  public void shrink() {
    for (Iterator i=getList().iterator(); i.hasNext();)
      if (i.next()==null)
        i.remove();
  }

  @Override
  public Iterator iterator() {
    shrink();
    return super.iterator();
  }

}

Then we just need to have a little change in our original code:

public class MyFormObject {
  private List elements = 
    ShrinkableLazyList.decorate(new ArrayList(),
       FactoryUtils.instantiateFactory(MyElement.class));
}

Using this solution I refactored existing code very quickly and got working collection items removal instantly.

[EDIT] If you like to check how to apply the dynamic form binding to any object (even for Hibernate entities) check my another post.

12 comments:

  1. The only source code related to this article is the ShrinkableLazyList class, which is completely shown in the content.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Thank for your post, but you must declare your collection as List without generic types, otherwise, Spring will instanciate elements because it will grow the collection by himself if type is specified.

    The code is in BeanWrapperImpl.growCollectionIfNecessary()

    if (index >= collection.size()) {
    Class elementType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(), nestingLevel);
    if (elementType != null) {
    for (int i = collection.size(); i < index + 1; i++) {
    collection.add(newValue(elementType, name));
    }
    }
    }

    For my case, I initially have this :
    List getContacts()
    and now, I must have this :
    List getContacts()

    And it works with this, but the issue is we have raw types for collection.

    Sorry for my bad english.

    ReplyDelete
  4. Laurent, probably your problem could be fixed using the following snippet in your controller:
    */
    @InitBinder
    public void initBinder(WebDataBinder binder) {

    binder.setAutoGrowNestedPaths(Boolean.FALSE);
    }

    "http://stackoverflow.com/questions/4657599/weird-problem-when-binding-a-dynamic-list

    ReplyDelete
  5. Can you please help me on this. Initially LazyList contains 2 elements and 2 rows will be shown in JSP using forEach tag in jsp. Later user is adding 2 more elements and removing 2nd element and 4th element. In such case, i believed that i will get item1, null, item3 and null. Instead i am getting item1, item2, item3 and null. How to know that item2 is removed?

    ReplyDelete
  6. Gobi, the order of binding items to collection depends on your internal items order (indexes) on client side, prepared by used javascript mechanism. Check your post data when you submit the form, what's the order of elements sent from there?

    ReplyDelete
  7. Your solution works great when creating a new master entity, but it's a bit more complicated when you want to update a previously created master entity. I found a solution using a boolean flag that indicates whether the dynamically added child entity should be removed or not. You can find a full working example there: http://stackoverflow.com/questions/9671640/spring-3-mvc-managing-a-one-to-many-relation-within-a-dynamic-form-using-a

    ReplyDelete
  8. Looks nice, thanks for sharing.

    ReplyDelete
  9. If my JSP form elements look like your example ones, should the code work using the LazyList? Or would I need to add all of the tags into my JSP first, as demonstrated in http://mattfleming.com/node/134?

    ReplyDelete
    Replies
    1. It should work using plain HTML, of course the POST data should be directed properly to the server side binder. If it doesn't work for you, you need to review what exactly goes with your POST data and how the binder works when receive it.

      Delete
  10. this code is wrong

    ReplyDelete