Thursday, September 27, 2007

An Ajax Select All checkbox for Wicket

I had a need to enable and disable a button based on whether an item in a list was checked. I also wanted a Select All checkbox that would change the state of the button and all the checkboxes in the list. And finally, I wanted the Select All checkbox to deselect if any of the checkboxes in the list was unchecked.


Wicket has CheckGroupSelector, CheckGroup and Check that all work together to change the state of a group of checkboxes, but they are not Ajax-enabled.


I wanted the simplest possible solution that would achieve this. Luckily, Wicket (1.2.6) came to the rescue yet again and made it quite easy.


I couldn't just Ajax-enable Check because it does not inherit from FormComponent and therefore you cannot add an AjaxFormComponentUpdatingBehavior to it, which is the easiest way to Ajax-enable a form item. So instead I use AjaxCheckBox in two places, for Select All and in the list.



My list class is called MyAjaxEnabledList. Tester.java contains a form that embeds a MyAjaxEnabledList. The MyAjaxEnabledList constructor is passed a list of items of type MyListItem. MyListItem implements the interface MyAjaxEnabledList.ISelected, which guarantees the presence of the methods get/setSelected() that the checkbox requires.


The key is the onUpdate() method of the anonymous subclass of MyAjaxEnabledList. It enables or disables the delete button based on the list's check group model. This is the feature that is not available if you use CheckGroupSelector, CheckGroup and Check


Tester.java


public class Tester extends WebPage
{
private static final long serialVersionUID = 7859298577765782995L;

final Button delete;

public Tester()
{
final List items = Arrays.asList(new MyListItem[]
{ new MyListItem("one"), new MyListItem("two") });

final MyAjaxEnabledList myList = new MyAjaxEnabledList(
"mylist", items)
{
private static final long serialVersionUID = -6274910651773243643L;

@Override
protected void onUpdate(AjaxRequestTarget target)
{
target.addComponent(delete);
delete.setEnabled(getCheckGroupModel().size() > 0);
}
};

delete = new Button("delete", new Model("Delete"))
{
private static final long serialVersionUID = -2732052672075770498L;

@Override
protected void onSubmit()
{
for (MyListItem s : myList.getCheckGroupModel())
{
System.out.println("You are deleting: " + s);
}
setResponsePage(Tester.class);
}
};
delete.setEnabled(false);
delete.setOutputMarkupId(true);

Form form = new Form("myform");
form.add(myList);
form.add(delete);
add(form);
}

class MyListItem implements MyAjaxEnabledList.ISelected
{
private String item;
private boolean selected;

public MyListItem(String item)
{
this.item = item;
}

public boolean isSelected()
{
return selected;
}

public void setSelected(boolean selected)
{
this.selected = selected;
}

@Override
public String toString()
{
return item;
}
}
}

Tester.html


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.sourceforge.net/" xml:lang="en"
lang="en">
<body>
<form wicket:id="myform">
<span wicket:id="mylist">
</span>
<input type="submit" wicket:id="delete" />
</form>
</body>
</html>


MyAjaxEnabledList contains two uses of MyAjaxCheckBox. This is a subclass of AjaxCheckBox that implements the method isChecked() (see below). The first use is the Select All ("allChecked"), and the second is in the list ("selected"). Both update the client ("MyAjaxEnabledList.this.onUpdate(target)") in addition to updating the check group model.


MyAjaxEnabledList.java


public class MyAjaxEnabledList extends
Panel
{
private static final long serialVersionUID = -8101172306096365329L;

/** Maintains a list of checked items. */
private final List checkGroupModel = new ArrayList();
/** The list of items in the table */
private final List rows = new ArrayList();
/** True if all items are selected */
private boolean allChecked;

public MyAjaxEnabledList(String id, final List items)
{
super(id);
this.rows.addAll(items);
final List checks = new ArrayList();
final MyAjaxCheckBox checkGroup = new MyAjaxCheckBox("allChecked",
new PropertyModel(this, "allChecked"))
{
private static final long serialVersionUID = -5765551380737131761L;

@Override
protected void onUpdate(AjaxRequestTarget target)
{
final boolean isSelected = isChecked();
if (isSelected)
{
checkGroupModel.addAll(rows);
}
else
{
checkGroupModel.clear();
}
for (MyAjaxCheckBox check : checks)
{
target.addComponent(check);
}
for (ISelected row : rows)
{
row.setSelected(isSelected);
}
MyAjaxEnabledList.this.onUpdate(target);
}
};
add(checkGroup);

// Initialize the table
add(new ListView("rows", new PropertyModel(this, "rows"))
{
private static final long serialVersionUID = -7831503487781845806L;

// Casting model object to T
@SuppressWarnings("unchecked")
@Override
protected void populateItem(ListItem item)
{
final T row = (T) item.getModelObject();
item.setModel(new CompoundPropertyModel(row));
MyAjaxCheckBox check = new MyAjaxCheckBox("selected")
{
private static final long serialVersionUID = -5766661380737131761L;

@Override
protected void onUpdate(AjaxRequestTarget target)
{
if (isChecked())
{
if (!checkGroupModel.contains(row))
checkGroupModel.add(row);
}
else
{
checkGroupModel.remove(row);
}
setAllChecked(checkGroupModel.size() == rows.size());
target.addComponent(checkGroup);
MyAjaxEnabledList.this.onUpdate(target);
}
};
checks.add(check);
item.add(check);
item.add(new Label("item", row.toString()));
}
});
}

public List getCheckGroupModel()
{
return checkGroupModel;
}

/** Subclasses can override */
protected void onUpdate(AjaxRequestTarget target)
{
// Does nothing
}

public List getRows()
{
return rows;
}

public void setRows(List rows)
{
this.rows.clear();
this.rows.addAll(rows);
}

public boolean isAllChecked()
{
return allChecked;
}

public void setAllChecked(boolean allChecked)
{
this.allChecked = allChecked;
}

public interface ISelected
{
public boolean isSelected();

public void setSelected(boolean selected);
}
}

MyAjaxEnabledList.html


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.sourceforge.net/" xml:lang="en"
lang="en">
<body>
<wicket:panel>
<table>
<tbody>
<tr>
<th>
<input type="checkbox" wicket:id="allChecked" />

Selected</th>

<th>
<wicket:message key="columntitle">
 </wicket:message>
</th>
</tr>

<tr wicket:id="rows">
<td>
<input type="checkbox" wicket:id="selected" />
</td>

<td>
<span wicket:id="item">Item</span>
</td>
</tr>
</tbody>
</table>
</wicket:panel>
</body>
</html>

Finally, here is MyAjaxCheckBox:


MyAjaxCheckBox.java


/**
* An AjaxCheckBox with some convenience methods
*/
public abstract class MyAjaxCheckBox extends AjaxCheckBox
{
private static final long serialVersionUID = 1L;

public MyAjaxCheckBox(final String id)
{
this(id, null);
}

public MyAjaxCheckBox(final String id, IModel model)
{
super(id, model);
}

public boolean isChecked()
{
final String value = getValue();
if (value != null)
{
try
{
return Strings.isTrue(value);
}
catch (StringValueConversionException e)
{
return false;
}
}
return false;
}
}

No comments: