Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CellTable with custom Header containing SearchBox and Focus Problem

I am trying to implement a CellTable with a custom Column Header which displays a SearchBox (simple Textbox) below the normal Column text.
The SearchBox should allow the user to filter the CellTable. It should look something like this:

  |Header  1|Header 2 |
  |SEARCHBOX|SEARCHBOX|
  -------------------------------------------------------
  |    ROW 1 
  ------------------------------------------------------
  |    ROW 2 

As soon as the user types in a character into the SearchBox a RangeChangeEvent is fired which leads to a server requests and the CellTable is updated with the new filtered list.

Basically everything works fine. However as soon as the CellTable is refreshed the SearchBox loses its focus and the user has to click with the mouse into the SearchBox again to type in a new character.

This is probably related to the fact that the render method of the custom header and its cell is called after the CellTable refresh.
Is there any way how to set the focus back to the SearchBox? I tried to set tabindex=0 but it didn't help.

Custom Header Class

public static class SearchHeader extends Header<SearchTerm> {
    @Override
    public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
    }
    private SearchTerm searchTerm;
    public SearchHeader(SearchTerm searchTerm,ValueUpdater<SearchTerm> valueUpdater) {
        super(new SearchCell());
        setUpdater(valueUpdater);
        this.searchTerm = searchTerm;
    }
    @Override
    public SearchTerm getValue() {
        return searchTerm;
    }
 }

Custom Search Cell (used in the custom Header)

The isChanged boolean flag is set to true when the user types something into the SearchBox and is set back to false if the SearchBox loses its focus. I added this flag in order to distinguish which SearchBox gets the focus (in case I use multiple SearchBoxes)

public static class SearchCell extends AbstractCell<SearchTerm> {

    interface Template extends SafeHtmlTemplates {
        @Template("<div style=\"\">{0}</div>")
        SafeHtml header(String columnName);

        @Template("<div style=\"\"><input type=\"text\" value=\"{0}\"/></div>")
        SafeHtml input(String value);
    }

    private static Template template;
    private boolean isChanged = false;

    public SearchCell() {
        super("keydown","keyup","change","blur");
        if (template == null) {
            template = GWT.create(Template.class);
        }
    }

    @Override
    public void render(com.google.gwt.cell.client.Cell.Context context,
        SearchTerm value, SafeHtmlBuilder sb) {
        sb.append(template.header(value.getCriteria().toString()));
        sb.append(template.input(value.getValue()));
    }

    @Override
    public void onBrowserEvent(Context context,Element parent, SearchTerm value,NativeEvent event,ValueUpdater<SearchTerm> valueUpdater) {
        if (value == null)
            return;
        super.onBrowserEvent(context, parent, value, event, valueUpdater);
        if ("keyup".equals(event.getType()))
        {
            isChanged = true;
            InputElement elem = getInputElement(parent);
            value.setValue(elem.getValue());
            if (valueUpdater != null)
                valueUpdater.update(value);
        }
        else if ("blur".equals(event.getType())) {
            isChanged =false;
        }
     }

     protected InputElement getInputElement(Element parent) {
         Element elem = parent.getElementsByTagName("input").getItem(0);
         assert(elem.getClass() == InputElement.class);
         return elem.cast();
     }
}

Init Code for the CellTable

NameColumn is the implementation of the abstract Column class with the appropriate types. It uses a TextCell internally.

ValueUpdater<SearchTerm> searchUpdater = new ValueUpdater<SearchTerm>() {
    @Override
    public void update(AccessionCellTableColumns.SearchTerm value) {
        // fires a server request to return the new filtered list
        RangeChangeEvent.fire(table, new Range(table.getPageStart(), table.getPageSize())); 
    }
};

table.addColumn(new NameColumn(searchTerm),new SearchHeader(searchTerm,searchUpdater));
like image 466
Ümit Avatar asked Jun 21 '11 09:06

Ümit


1 Answers

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:

  1. make sure your grid is wrapped by a LayoutPanel
  2. make sure your grid's columns are managed by a collection/list
  3. create custom column header to create an area for your filtering
  4. create filtering container to place you textboxes into
  5. layout your grid containers children (grid, filter, pager)
  6. use css techniques to position filters into column headers
  7. add event handlers to filters
  8. add timer to handle filter input delays
  9. 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!

like image 96
1-14x0r Avatar answered Oct 21 '22 10:10

1-14x0r