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");
}
}
}
you can see that it requires your collection of columns in order to properly create the filter inputs that go into your container Additionally the filter class also gets passed in the column in order to bind the column to the specific filter. This allows you to access the fields and such
public class Filter extends TextBox {
final private GridStringColumn<T> boundColumn_;
public Filter(GridStringColumn<T> column) {
super();
boundColumn_ = column;
addStyleName("filterInput " + boundColumn_.getDataStoreName());
addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
filterTimer.cancel();
filterTimer.schedule(FILTER_DELAY);
}
});
}
public GridStringColumn<T> getBoundColumn() {
return boundColumn_;
}
}
Step 5: Add Your Components To Your LayoutPanel
now when you init your grid to add your pager and grid into the layout container we do not account for the vertical height that the filter should normally take up. Since it is set to relative position with a z-index greated then what the grid and columns have, it will appear to actually be in the header. MAGIC!!!
public void setupDataGrid() {
add(pagerContainer_);
setWidgetTopHeight(pagerContainer_, 0, Unit.PX, PAGER_HEIGHT, Unit.PX);
add(filterContainer_);
setWidgetTopHeight(filterContainer_, PAGER_HEIGHT + FILTER_HEIGHT, Unit.PX, FILTER_HEIGHT, Unit.PX);
add(dataGrid_);
setWidgetTopHeight(dataGrid_, PAGER_HEIGHT, Unit.PX, ScreenManager.getScreenHeight() - PAGER_HEIGHT - BORDER_HEIGHT, Unit.PX);
pager_.setVisible(true);
dataGrid_.setVisible(true);
}
and now for some constants
final private static int PAGER_HEIGHT = 32;
final private static int FILTER_HEIGHT = 32;
final private static int BORDER_HEIGHT = 2;
Border height is for specific css styling your app might have, technially this is slug space to make sure everything fits tightly.
Step 6: Use CSS Magic
the specific css that positions the filters onto your columns from above is this
.gridContainer .filterContainer {
position: relative;
z-index: 1000;
top: 28px;
}
that will move the container over the columns and position there layer above your headers
next we need to make sure that the cells in the filterContainer line up with the ones in our datagrid
.gridContainer .filterContainer td {
padding: 0 13px 0 5px;
width: auto;
text-align: center;
}
next make sure our inputs scale according to the size of the containers cell they live in
.gridContainer .filterContainer .filterInput {
width: 100%;
}
lastly we want to move our sorting image indicator up so that the filter input textboxes do not hide them
.gridData table .columnHeader div img {
position: relative;
top: -18px;
}
now when you compile you should see the filters over the column headers. you may need to tweak the css to get them to line up exactly. This also assumes you do not have any special column widths set. if you do, you will need to create some additional functionality to manually set the cell sizes and style the widths to sync with the columns. I have ommited this for santity.
*now its time for a break, your almost there!^________^*
Step 7 & 8: Add event handlers
this is the easy part. if you look at the filter class from above take note of this method body
addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
filterTimer.cancel();
filterTimer.schedule(FILTER_DELAY);
}
});
create your filter timer
private FilterTimer filterTimer = new FilterTimer();
make sure you specify the field in the root of the class body and not inline.
private class FilterTimer extends Timer {
@Override
public void run() {
updateDataList();
}
}
a timer is required so that the event isn't fired everytime a user enters a value. Alot of people have added onblur or other silly handlers, but its pointless. A user can only be entering data into one field at a time, so its a mute point. just use a onKeyUp handler..
Step 9: Update your grid
now when we call updateDataList (which should also be called from your onRangeChanged event (for sorting and data loading) we want to iterate though our collection of Filters for our applied filters the user has entered in. Personally i store all of the request parameters into a hashmap for easy access and updating. Then just pass the entire map into my request engine that does your RPC or RequestFactory stuff
public void updateDataList() {
initParameters();
// required parameters controlled by datagrid
parameters_.put("limit", limit_ + "");
parameters_.put("offset", offset_ + "");
// sort parameters
if (sortField_.equals("") || sortOrder_.equals("")) {
parameters_.remove("sortField");
parameters_.remove("sortDir");
} else {
parameters_.put("sortField", sortField_);
parameters_.put("sortDir", sortOrder_);
}
// filter parameters
for (Filter filter : filters_) {
if (!filter.getValue().equals("")) {
CGlobal.LOG.info("filter: " + filter.getBoundColumn().getDataStoreName() + "=" + filter.getValue());
parameters_.put(filter.getBoundColumn().getDataStoreName(), filter.getValue());
}
}
RequestServiceAsync requestService = (RequestServiceAsync) GWT.create(RequestService.class);
requestService.getRequest("RPC", getParameters(), new ProviderAsyncCallback());
}
you can see how and why we need to bind the filter to a column, so when we iterate though the filters we can get the stored field name. usually i just pass the fieldname and filter value as a query parameter rather then pass all of the filters as a single filter query parameter. This is way more extensible, and rarely are there edgecases that your db columns should == reserved words for your query parameters like sortDir or sortField above.
*Done <_____>*
well i hope that helps all of you with some advanced gwt datagrid stuff. I know this was a pain to create myself, so hopefully this will save you all a bunch of time in the future. Goodluck!