Knowledge base‎ > ‎

Adding and removing rows in a Wicket ListView via AJAX

posted Apr 20, 2010, 5:09 AM by Szabolcs Szádeczky-Kardoss   [ updated Apr 22, 2010, 1:45 AM by István Soós ]
As our regular readers already know, Wicket is our favorite web framework and we use it actively in our projects. Wicket is an easy-to-use, well-designed framework and is able to incorporate Ajax in a very nice and easy way. I personally am not a big fan of using Ajax in every corner of the application, however at some points it can make your app much nicer. Let's look at such a case!

Imagine a form where you have to enter possible answers for a survey question, with the number of answers being dynamic. It would be possible to give the user a fixed number of text fields, let's say 8 should be enough, but what happens if the user wants more possible answer? Or what if (s)he only wants 2, and is not really happy about another 6 empty text fields taking up half of the screen? So let's go dynamic and "ajaxify"!

Let's give the user only 2 possible answers to begin with and also a link with the possibily to add more answer rows, and another to remove unused ones. The form can contain a lot of other fields in which we are not interested in, however adding or removing a row should not have any effect on the other fields. What's more the app should keep those values that the user already has entered. Of course the links should be using Ajax and only refresh the dynamic list part of our form page and they should refrain from submitting and/or validating the whole form. If you look up a tutorial or a good book on Wicket it gives you a similar solution:
<!-- DynamicRows.html -->
<form wicket:id="form">
<!-- Other non-repeating fields in the form -->
...
<div wicket:id="rowPanel">
<span wicket:id="rows">
<span wicket:id="index">1.</span>
<input type="text" wicket:id="text"/>
</span>
<a href="#" wicket:id="addRow">Add row</a>
</div>
...
</form>
And here comes the associated Java code:
// Relevant constructor code in DynamicRows.java
...
// Create a panel within the form, to enable AJAX action
final MarkupContainer rowPanel = new WebMarkupContainer("rowPanel");
rowPanel.setOutputMarkupId(true);
form.add(rowPanel);

// List all rows
ArrayList rows = new ArrayList(2);
rows.add(new String());
rows.add(new String());
final ListView lv = new ListView("rows", rows) {
@Override
protected void populateItem(ListItem item) {
int index = item.getIndex() + 1;
item.add(new Label("index", index + "."));

TextField text = new TextField("text", item.getModel()));
item.add(text);
}
};
rowPanel.add(lv);

AjaxSubmitLink addLink = new AjaxSubmitLink("addRow", form) {
@Override
public void onSubmit(AjaxRequestTarget target, Form form) {
lv.getModelObject().add(new String());
if (target != null)
target.addComponent(rowPanel);
}
};
addLink.setDefaultFormProcessing(false);
rowPanel.add(addLink);
...
You can notice, that we have used AjaxSubmitLink for adding a new row into the list. This is needed because the user might already have entered some values in some of the fields and we don't want those to be lost, so the form values have to be submitted. However we would like to avoid getting a validation error in some of the other fields when all we want is to add a new row, so we use addLink.setDefaultFormProcessing(false).

This is a nice solution, however if you try it you'll see that the values entered in the repeating rows get lost, when a new row is added. The reason for this is that after pressing the "Add row" Ajax link the TextFields don't update their backing model (since we have turned off form processing), however the ListView removes and recreates all its TextFields again in its onPopulate() method. And so the "old-new" TextFields will show the original model value.

So what to do now? You can try to update the backing model of all the repeating TextFields in the Ajax "Add row" action, however this is still not a 100% solution in case an invalid value is entered. In case of an invalid value the validation fails, the model doesn't get updated and after recreating the TextFields the invalid value reverts to the last valid value entered. So it turns out that the problem is again caused by the recreation of all the TextFields.

You could cache and reuse all those TextFields in a custom subclass of ListView, as I did for the first time. Or you could browse the source code of ListView and come across a reuseItems property which is just the nice and clean solution to our problem:
  lv.setReuseItems(true);
This will reuse the already created TextFields and will call the populateItem method only for the newly added row. Since the TextFields are reused they will remember the last valid or invalid value entered, and that's what we wanted from the beginning. All the above logic can also be applied to a "Remove row" Ajax action in case it's needed. So, we have made a nice "ajaxified" ListView.


published:2009-07-28, a:Szabolcs, y:2009, l:ajax, l:wicket
Comments