Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
240 views
in Technique[技术] by (71.8m points)

java - 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));
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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");
            }
        }
 

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...