The Skinny
Unfortunately GWT's support for custom column headers is a bit wonky to say the least. If anyone has had the joy of working with the AbstractCell classes you would know what i mean. Additionally the proper way to implement composite (nested widgets) into your column header cell is a bust, as i have not been able to get it to work proper, nor have found any workable examples of a CompositeCell working.
If your datagrid implements a ColumnSortHandler of sorts (LOL thats phunny) your nested UI objects that might have key or mouse events will trigger a column sort. FAIL. Again i could not find a way to overload the columnsort events to exclude triggers fired by interacting with the nested column header ui components/widgets. Not to mention that you need to abstractly define the nested components by writing inline HTML into the Template interface that builds your cell. Not nearly an elegant choice, as it forces developers to have to write native JavaScript code to create and control the handlers associated with the nested components/widgets in the column header.
This "proper" implementation technique also does not solve the focus problem that this question addresses, and is not nearly a great solution to complex datagrids that need AsyncProvider (or ListProvider) data sets with column cell filtering, or custom rendering. The performance of this is also meh >_> Far from a proper solution IMO
Seriously???
In order to implement a functional column cell filtering, you must tackle this from a more traditional dynamical javascript/css appoarch from the days before GWT and crazy JQuery libraries. My functional solution is a hybrid of the "proper" way with some crafty css.
the psuedo code is as follows:
- make sure your grid is wrapped by a LayoutPanel
- make sure your grid's columns are managed by a collection/list
- create custom column header to create an area for your filtering
- create filtering container to place you textboxes into
- layout your grid containers children (grid, filter, pager)
- use css techniques to position filters into column headers
- add event handlers to filters
- add timer to handle filter input delays
- fire grid update function to refresh data, async or local list
whew, hope i haven't lost you yet, as there is alot to do in order to make this work
Step 1: Setup Grid Class to Extend LayoutPanel
First you need to make sure your class that creates your grid can support and be sized properly in your application. To do this make sure your grid class extends a LayoutPanel.
public abstract class PagingFilterDataGrid<T> extends LayoutPanel {
public PagingFilterDataGrid() {
//ctor initializers
initDataGrid();
initColumns();
updateColumns();
initPager();
setupDataGrid();
}
}
Step 2: Create Managed Columns
This step is also pretty straight forward. Rather then directly added new columns into your datagrid, store them into a list, then programmatically add them into your grid with a foreach statement
ColumnModel (you should be able to create a number or date, or whatever else type of column you want. for simplicity i generally work with string data in web apps, unless i explicitly need special arithmetic or date functionality)
public abstract class GridStringColumn<M> extends Column<VwGovernorRule, String> {
private String text_;
private String tooltip_;
private boolean defaultShown_ = true;
private boolean hidden_ = false;
public GridStringColumn(String fieldName, String text, String tooltip, boolean defaultShown, boolean sortable, boolean hidden) {
super(new TextCell());
setDataStoreName(fieldName);
this.text_ = text;
this.tooltip_ = tooltip;
this.defaultShown_ = defaultShown;
setSortable(sortable);
this.hidden_ = hidden;
}
}
create list in your datagrid class to store your columns into
public abstract class PagingFilterDataGrid<T> extends LayoutPanel {
private List<GridStringColumn<T>> columns_ = new ArrayList<GridStringColumn<T>>();
}
to create your columns create a initColumn method that is called in your datagrid constructor. Usually i extend the a base datagrid class, so that i can put my specific grid initializers into. This adds a column to your column store. MyPOJODataModel is your data structure that you store your records for the datagrid in, usually its a POJO of your hibernate or something from your backend.
@Override
public void initColumns() {
getColumns().add(new GridStringColumn<MyPOJODataModel>("columnName", "dataStoreFieldName", "column tooltip / description information about this column", true, true, false) {
@Override
public String getValue(MyPOJODataModelobject) {
return object.getFieldValue();
}
});
}
create some code now to update your columns into your grid, make sure you call this method after you call initColumns method. the initFilters method we will get to shortly. But if you need to know now, it is the method that sets up your filters based on what columns you have in your collection. You can also call this function whenever you want to show/hide columns or reorder the columns in your grid. i know you love it!
@SuppressWarnings("unchecked")
public void updateColumns() {
if (dataGrid_.getColumnCount() > 0) {
clearColumns();
}
for (GridStringColumn<T> column : getColumns()) {
if (!column.isHidden()) {
dataGrid_.addColumn((Column<T, ?>) column, new ColumnHeader(column.getText(), column.getDataStoreName()));
}
}
initFilters();
}
Step 3: Create Custom Column Header
Now we are starting to get to the fun stuff now that we have the grid and columns ready for filtering. This part is similiar to the example code this question askes, but it is a little different. What we do here is create a new custom AbstractCell that we specific an html template for GWT to render at runtime. Then we inject this new cell template into our custom header class and pass it into the addColumn() method that gwt's data uses to create a new column in your data grid
Your custom cell:
final public class ColumnHeaderFilterCell extends AbstractCell<String> {
interface Templates extends SafeHtmlTemplates {
@SafeHtmlTemplates.Template("<div class="headerText">{0}</div>")
SafeHtml text(String value);
@SafeHtmlTemplates.Template("<div class="headerFilter"><input type="text" value=""/></div>")
SafeHtml filter();
}
private static Templates templates = GWT.create(Templates.class);
@Override
public void render(Context context, String value, SafeHtmlBuilder sb) {
if (value == null) {
return;
}
SafeHtml renderedText = templates.text(value);
sb.append(renderedText);
SafeHtml renderedFilter = templates.filter();
sb.append(renderedFilter);
}
}
If you haven't learned to hate how you make custom cells, you soon will im sure after you get done implementing this. Next we need a header to inject this cell into
column header:
public static class ColumnHeader extends Header<String> {
private String name_;
public ColumnHeader(String name, String field) {
super(new ColumnHeaderFilterCell());
this.name_ = name;
setHeaderStyleNames("columnHeader " + field);
}
@Override
public String getValue() {
return name_;
}
}
as you can see this is a pretty straightforward and simple class. Honestly its more like a wrapper, why GWT has thought of combining these into a specific column header cell rather then having to inject a generic cell into is beyond me. Maybe not a super fancy but im sure it would be much easier to work with
if you look up above to your updateColumns() method you can see that it creates a new instance of this columnheader class when it adds the column. Also make sure you are pretty exact with what you make static and final so you arent thrashing your memory when you create very large data sets... IE 1000 rows at 20 columns is 20000 calls or instances of template or members you have stored. So if one member in your cell or header has 100 Bytes that turns into about 2MB or resources or more just for the CTOR's. Again this isn't as impportant as custom data cell rendering, but it is still important on your headers too!!!
Now dont forget to add your css
.gridData table {
overflow: hidden;
white-space: nowrap;
table-layout: fixed;
border-spacing: 0px;
}
.gridData table td {
border: none;
border-right: 1px solid #DBDBDB;
border-bottom: 1px solid #DBDBDB;
padding: 2px 9px
}
.gridContainer .filterContainer {
position: relative;
z-index: 1000;
top: 28px;
}
.gridContainer .filterContainer td {
padding: 0 13px 0 5px;
width: auto;
text-align: center;
}
.gridContainer .filterContainer .filterInput {
width: 100%;
}
.gridData table .columnHeader {
white-space: normal;
vertical-align: bottom;
text-align: center;
background-color: #EEEEEE;
border-right: 1px solid #D4D4D4;
}
.gridData table .columnHeader div img {
position: relative;
top: -18px;
}
.gridData table .columnHeader .headerText {
font-size: 90%;
line-height: 92%;
}
.gridData table .columnHeader .headerFilter {
visibility: hidden;
height: 32px;
}
now thats the css for all of the stuff your gonna add it in. im too lazy to separate it out, plus i think you can figure that out. gridContainer is the layoutpanel that wraps your datagrid, and gridData is your actual data grid.
Now when you compile you should see a gap below the column heading text. This is where you will position your filters into using css
Step 4: Create Your Filter Container
now we need something to put our filter inputs into. This container also has css applied to it that will move it down into the space we just created in the headers. Yes thats right the filters that are in the header are actually and technically not in the header. This is the only way to avoid the sorting event issue and lose focus issue
private HorizontalPanel filterContainer_ = new HorizontalPanel();
and your filter initialization
public void initFilters() {
filterContainer_.setStylePrimaryName("filterContainer");
for (GridStringColumn<T> column : getColumns()) {
if (!column.isHidden()) {
Filter filterInput = new Filter(column);
filters_.add(filterInput);
filterContainer_.add(filterInput);
filterContainer_.setCellWidth(filterInput, "auto");
}
}