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)); }
<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".
Anyway, both implementations don't assume that items could be removed by the user. For example, if you have an empty form, and then:
<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.
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:
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. |