Saturday, November 08, 2008

Expanding Columns - Wicket

Recently I wanted a component(table like) which expands column wise.
I googled a bit but found no appropirate component. So decided to build my own panel which can expand column-wise.
Thats the beauty of wicket! You can create custom components by using more than one out of box components easily.

So for my proto, I created two models, Row and Column.
A custom panel which takes up number of rows required in the constructor. And columns can be added to / removed from using defined
methods.

For example, I have made each column to have a radio button which has a radio group binded to it horizontally.
It can be expanded very easily to contain multiple copies of columns.
Let us see the code.


Row.java
import java.io.Serializable;

/**
 * Row Model
 * @author Srikanth NT
 */
public class Row implements Serializable {
    private static final long serialVersionUID = 1L;

    private int id;

    private String desc;

    public Row(final int id, final String desc) {
        super();
        this.id = id;
        this.desc = desc;
    }

    public int getId() {
        return id;
    }

    public void setId(final int id) {
        this.id = id;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(final String desc) {
        this.desc = desc;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((desc == null) ? 0 : desc.hashCode());
        result = prime * result + id;
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Row other = (Row) obj;
        if (desc == null) {
            if (other.desc != null) {
                return false;
            }
        } else if (!desc.equals(other.desc)) {
            return false;
        }
        if (id != other.id) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Row [" + id + ":" + desc + "]";
    }

}

Column.java

import java.io.Serializable;

/**
 * Column Model.
 *
 * @author Srikanth NT
 */
public class Column implements Serializable {
    private static final long serialVersionUID = 1L;

    private int id;

    private String desc;

    public Column(final int id, final String desc) {
        super();
        this.id = id;
        this.desc = desc;
    }

    public int getId() {
        return id;
    }

    public void setId(final int id) {
        this.id = id;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(final String desc) {
        this.desc = desc;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((desc == null) ? 0 : desc.hashCode());
        result = prime * result + id;
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Column other = (Column) obj;
        if (desc == null) {
            if (other.desc != null) {
                return false;
            }
        } else if (!desc.equals(other.desc)) {
            return false;
        }
        if (id != other.id) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Column [" + id + ":" + desc + "]";
    }

}



My custom panel is :
CustomPanel.javaimport java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Radio;
import org.apache.wicket.markup.html.form.RadioGroup;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.Model;

/**
 * Custom Panel with expanding columns.
 *
 * @author Srikanth NT
 */
public class CustomPanel extends Panel {

    private List<Row> rows;

    private Map<Row, List<Column>> elements = new LinkedHashMap<Row, List<Column>>();

    private final ListView rowRepeater;

    public CustomPanel(final String id, final List<Row> rowsToAdd) {
        super(id);
        rows = rowsToAdd;

        for (Row row : rows) {
            elements.put(row, new ArrayList<Column>());
        }

        rowRepeater = new ListView("rowRepeater", rows) {
            private static final long serialVersionUID = 1L;

            @Override
            protected void populateItem(final ListItem item) {
                Row row = (Row) item.getModelObject();
                item.add(new Label("rowDesc", row.getDesc()));
                final RadioGroup columnSelectionGroup = new RadioGroup("columnSelectionGroup", new Model());
                final ListView columnRepeater = new ListView("columnRepeater", elements.get(row)) {
                    private static final long serialVersionUID = 1L;

                    @Override
                    protected void populateItem(final ListItem item) {
                        Column column = (Column) item.getModelObject();
                        item.add(new Radio("columnSelection", new Model(column)));
                    }
                };
                columnRepeater.setReuseItems(true);
                columnSelectionGroup.add(columnRepeater);
                item.add(columnSelectionGroup);
            }
        };
        rowRepeater.setReuseItems(true);
        add(rowRepeater);
        rowRepeater.setOutputMarkupId(true);

    }

    public void addColumn(final Column column) {
        for (Entry<Row, List<Column>> entry : elements.entrySet()) {
            if(!entry.getValue().contains(column)) {
                entry.getValue().add(entry.getValue().size(), column);
            }
        }
    }

    public void removeColumn(final Column column) {
        for (Entry<Row, List<Column>> entry : elements.entrySet()) {
            while (entry.getValue().contains(column)) {
                entry.getValue().remove(column);
            }
        }
    }

    public Map<Row, Column> getRowColumnsMapping() {
        final Map<Row, Column> rowColumnsMapping = new LinkedHashMap<Row, Column>();
        for (final Iterator it = rowRepeater.iterator(); it.hasNext();) {
            final ListItem item = (ListItem) it.next();
            final Row row = (Row) item.getModelObject();

            item.visitChildren(new IVisitor() {

                public Object component(final Component component) {
                    if (component instanceof RadioGroup) {
                        Column column = (Column) component.getModelObject();
                        rowColumnsMapping.put(row, column);
                    }
                    return CONTINUE_TRAVERSAL;
                }
            });
        }
        return rowColumnsMapping;
    }
}


And its corresponding html:
CustomPanel.html

<wicket:panel>
    <table>
        <tr wicket:id="rowRepeater">
            <td wicket:id="rowDesc"></td>
            <span wicket:id="columnSelectionGroup">
            <span wicket:id="columnRepeater">
            <td><input type="radio" wicket:id="columnSelection"></td>
            </span>
            </span>
        </tr>
    </table>
</wicket:panel>



And now a test page to check my custom component.
CustomTestPage.java

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;

/**
 * Test Page.
 *
 * @author Srikanth NT
 */
public class CustomTestPage extends WebPage {

    public CustomTestPage() {
        final Form columnSelectorForm = new Form("columnSelectorForm");
        add(columnSelectorForm);

        List<Row> rowList = new ArrayList<Row>();
        rowList.add(new Row(1, "Row-1"));
        rowList.add(new Row(2, "Row-2"));

        final CustomPanel columnSelectorPanel = new CustomPanel("columnSelectorPanel", rowList);
        columnSelectorPanel.setOutputMarkupId(true);
        columnSelectorForm.add(columnSelectorPanel);

        columnSelectorForm.add(new AjaxButton("addColumn1", columnSelectorForm) {
            private static final long serialVersionUID = 1L;

            @Override
            protected void onSubmit(final AjaxRequestTarget target, final Form form) {
                target.addComponent(columnSelectorPanel);

                Column column = new Column(1, "Column 1");
                columnSelectorPanel.addColumn(column);
            }

        });

        columnSelectorForm.add(new AjaxButton("removeColumn1", columnSelectorForm) {
            private static final long serialVersionUID = 1L;

            @Override
            protected void onSubmit(final AjaxRequestTarget target, final Form form) {
                target.addComponent(columnSelectorPanel);

                Column column = new Column(1, "Column 1");
                columnSelectorPanel.removeColumn(column);
            }

        });

        columnSelectorForm.add(new AjaxButton("addColumn2", columnSelectorForm) {
            private static final long serialVersionUID = 1L;

            @Override
            protected void onSubmit(final AjaxRequestTarget target, final Form form) {
                target.addComponent(columnSelectorPanel);

                Column column = new Column(1, "Column 2");
                columnSelectorPanel.addColumn(column);
            }

        });

        columnSelectorForm.add(new AjaxButton("removeColumn2", columnSelectorForm) {
            private static final long serialVersionUID = 1L;

            @Override
            protected void onSubmit(final AjaxRequestTarget target, final Form form) {
                target.addComponent(columnSelectorPanel);

                Column column = new Column(1, "Column 2");
                columnSelectorPanel.removeColumn(column);
            }

        });

        columnSelectorForm.add(new Button("submit") {
            @Override
            public void onSubmit() {
                Map<Row, Column> list = columnSelectorPanel.getRowColumnsMapping();
                for (Entry<Row, Column> entry : list.entrySet()) {
                    System.out.println(entry.getKey() + " is assigned to " + entry.getValue());
                }
            }
        });
    }
}

and related html:CustomTestPage.html

<html xmlns="http://www.w3.org/1999/xhtml">
<body>
    <form wicket:id="columnSelectorForm">
    <div style="border: 1px solid blue;">
    <table cellpadding="5px">
        <tr>
            <td>Column 1</td>
            <td><input value="Add" wicket:id="addColumn1" type="button" ></td>
            <td><input value="Remove" wicket:id="removeColumn1" type="button" ></td>
        </tr>
        <tr>
            <td>Column 2</td>
            <td><input value="Add" wicket:id="addColumn2" type="button" ></td>
            <td><input value="Remove" wicket:id="removeColumn2" type="button" ></td>
        </tr>
    </table>
    </div>
    <div style="border: 1px solid red;">
        <span wicket:id="columnSelectorPanel"> </span>
    </div>
    <input type="submit" value="Submit" wicket:id="submit">
    </form>
</body>
</html>


I have used some ajax functionality built into it.


On submit of the form, you would see some log like
Row [1:Row-1] is assigned to Column [1:Column 2]
Row [2:Row-2] is assigned to Column [1:Column 1]

( I have added the column 2 followed by column1.)