4

I want to suppress the display of some items in a Grid widget in Vaadin Flow 14.

For example, if the user enters a year in a IntegerField widget, I want the grid to show only items with an associated date before that year. Items with a date on or after that date should disappear. If the user changes the year number, the filtering should be re-applied with fewer or more items displayed appropriately in the grid.

I have seen filters on Grid discussed but cannot wrap my head around the various moving parts. Perhaps someone could show a simple example?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154

2 Answers2

4

I am going to submit my way of filtering a Grid using a ListDataProvider. The goal of my approach is to be able to add filter fields for any column you want, and that the filters can be combined with each other, and also that each filter value can be changed to something else at any time. With my solution, you will also be able to use other Input Fields than TextField. So can also use ComboBoxes for enums, etc.

The crux is that when you use the setFilter method of the ListDataProvider, any other filter is disregarded so multiple filters wont work simultaneously. But when you use addFilter exclusively you can never change a filter you defined, only add another filter on top of that. You could let the user clear all filters with a button that is dedicated for this but I would like to avoid that.

This is how I avoid this conundrum: Every time that any filter value changes, I reset the current filter using setFilter. But within that new Filter, I will check the values of ALL filter fields, and not only the value of the field whose value just changed. In other words, I always have only one single filter active, but that filter accounts for all defined filter-values.

Here is the full and working code for a View with a grid (see images below) using the same example as OP's original answer. You can filter this grid by string representation, year, month, or dayOfWeek simultaneously.

@Route(value = "Test", layout = MainView.class)
public class TestView extends VerticalLayout {
    private Grid<LocalDate> grid;
    private TextField toStringFilter, yearFilter;
    private ComboBox<Month> monthFilter;
    private ComboBox<DayOfWeek> dayOfWeekFilter;

    public TestView() {
        grid = new Grid<>(LocalDate.class, false);
        List< LocalDate > dates = List.of(
                LocalDate.of( 2020 , Month.JANUARY , 23 ) ,
                LocalDate.of( 2019 , Month.FEBRUARY , 24 ) ,
                LocalDate.of( 2022 , Month.MARCH , 25 ) ,
                LocalDate.of( 2011 , Month.APRIL , 26 ) ,
                LocalDate.of( 2022 , Month.APRIL , 23 )
        );
        grid.setItems( new ArrayList< LocalDate >( dates ) );
        grid.addColumn( LocalDate :: toString ).setHeader("String Representation").setKey("tostring");
        grid.addColumn( LocalDate :: getYear ).setHeader("Year").setKey("year");
        grid.addColumn( LocalDate :: getMonth ).setHeader("Month").setKey("month");
        grid.addColumn( LocalDate :: getDayOfWeek ).setHeader("Day Of Week").setKey("dayofweek");

        prepareFilterFields();
        add(grid);
    }

    private void prepareFilterFields() {
        HeaderRow headerRow = grid.appendHeaderRow();

        toStringFilter = gridTextFieldFilter("tostring", headerRow);
        yearFilter = gridTextFieldFilter("year", headerRow);
        monthFilter = gridComboBoxFilter("month", headerRow, Month::toString, Month.values());
        dayOfWeekFilter = gridComboBoxFilter("dayofweek", headerRow, DayOfWeek::toString, DayOfWeek.values());
    }

    private <T> ComboBox<T> gridComboBoxFilter(String columnKey, HeaderRow headerRow, ItemLabelGenerator<T> itemLabelGenerator, T... items) {
        ComboBox<T> filter = new ComboBox<>();
        filter.addValueChangeListener(event -> this.onFilterChange());
        filter.setItemLabelGenerator(itemLabelGenerator);
        filter.setItems(items);
        filter.setWidthFull();
        filter.setClearButtonVisible(true);
        headerRow.getCell(grid.getColumnByKey(columnKey)).setComponent(filter);
        return filter;
    }

    private TextField gridTextFieldFilter(String columnKey, HeaderRow headerRow) {
        TextField filter = new TextField();
        filter.setValueChangeMode(ValueChangeMode.TIMEOUT);
        filter.addValueChangeListener(event -> this.onFilterChange());
        filter.setWidthFull();
        headerRow.getCell(grid.getColumnByKey(columnKey)).setComponent(filter);
        return filter;
    }

    private void onFilterChange(){
        ListDataProvider<LocalDate> listDataProvider = (ListDataProvider<LocalDate>) grid.getDataProvider();
        // Since this will be the only active filter, it needs to account for all values of my filter fields
        listDataProvider.setFilter(item -> {
            boolean toStringFilterMatch = true;
            boolean yearFilterMatch = true;
            boolean monthFilterMatch = true;
            boolean dayOfWeekFilterMatch = true;

            if(!toStringFilter.isEmpty()){
                toStringFilterMatch = item.toString().contains(toStringFilter.getValue());
            }
            if(!yearFilter.isEmpty()){
                yearFilterMatch = String.valueOf(item.getYear()).contains(yearFilter.getValue());
            }
            if(!monthFilter.isEmpty()){
                monthFilterMatch = item.getMonth().equals(monthFilter.getValue());
            }
            if(!dayOfWeekFilter.isEmpty()){
                dayOfWeekFilterMatch = item.getDayOfWeek().equals(dayOfWeekFilter.getValue());
            }

            return toStringFilterMatch && yearFilterMatch && monthFilterMatch && dayOfWeekFilterMatch;
        });
    }
}

Unfiltered Grid: grid with filters, no active filter


Filtered By Year: grid with filters, filtered by year


Filtered By Year AND Month: grid with filters, filtered by year and month

kscherrer
  • 5,486
  • 2
  • 19
  • 59
2

Filtering grids is quite slick, and fairly easy once you get the hang of it.

First, understand that the Grid object is not actually filtered. The DataProvider backing the Grid is filtered. The Grid automatically updates its own display to match changes to the data provider.

Below is an entire example app. When it first appears, we see four items it the Grid widget. These items are simply java.time.LocalDate objects.

before filtering

When the user enters a year number in the IntegerField, we apply a filter to the ListDataProvider object backing our grid. If the user clears the year number from that IntegerField, we clear all filters from our data provider.

after filtering

Setting and clearing the filter(s) takes effect immediately. This point threw me, with me thinking that I must somehow “apply” the filters after calling setFilter or addFilter. But, no, the Grid automatically updates its display — with no further code on my part. The predicate test defined as part of your filter is automatically applied to each item in the ListDataProvider object’s data set. Those items that pass the test are displayed, while items that fail the test are suppressed from display.

The setting and clearing of filters is done in a value-change listener on the IntegerField widget. When a user enters a number (and leaves the field, such as by pressing Tab or Return), our listener object automatically is called, with an event object passed. Notice that we test for a null being retrieved from the IntegerField. A null means the user cleared the existing entry, leaving the field blank.

Where did we define our ListDataProvider object to back our Grid object? That data provider was instantiated automatically for us when we passed a List to the Grid::setItems method.

By the way, you can have a Grid automatically display columns using the JavaBeans-style naming conventions (getter/setter methods). But here in this example we defined our columns explicitly, their values generated by calling the LocalDate :: method references we pass.

package work.basil.example;

import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.router.Route;

import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * The main view contains a button and a click listener.
 */
@Route ( "" )
//@PWA ( name = "Project Base for Vaadin", shortName = "Project Base" )
@CssImport ( "./styles/shared-styles.css" )
@CssImport ( value = "./styles/vaadin-text-field-styles.css", themeFor = "vaadin-text-field" )
public class MainView extends VerticalLayout
{
    private Grid < LocalDate > grid;
    private IntegerField yearField;

    public MainView ( )
    {
        yearField = new IntegerField( "Filter years before: " );
        yearField.addValueChangeListener(
                ( AbstractField.ComponentValueChangeEvent < IntegerField, Integer > event ) -> {
                    Integer year = event.getValue();
                    // If the user cleared the field, its value is null. In such a case, clear all filters.
                    // If the user entered a year number into this field, specify a filter.
                    if ( Objects.isNull( year ) )
                    {
                        ( ( ListDataProvider < LocalDate > ) grid.getDataProvider() ).clearFilters();
                    } else
                    {
                        ( ( ListDataProvider < LocalDate > ) grid.getDataProvider() ).setFilter( ( LocalDate localDate ) -> localDate.getYear() < year );
                    }
                }
        );

        grid = new Grid <>();
        List < LocalDate > dates = List.of(
                LocalDate.of( 2020 , Month.JANUARY , 23 ) ,
                LocalDate.of( 2019 , Month.FEBRUARY , 24 ) ,
                LocalDate.of( 2022 , Month.MARCH , 25 ) ,
                LocalDate.of( 2011 , Month.APRIL , 26 )
        );
        grid.setItems( new ArrayList < LocalDate >( dates ) );
        grid.addColumn( LocalDate :: toString );
        grid.addColumn( LocalDate :: getYear );
        grid.addColumn( LocalDate :: getDayOfWeek );

        this.add( yearField , grid );
    }
}

You can install multiple filters by calling addFilter instead of setFilter. The predicates are effective combined as AND rather than as OR. So all the predicate tests must pass for an item to be displayed. Call clearFilters to remove any filters. Calling setFilter clears any existing filters and installs a single filter.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Is it possible to have two filter fields this way that can work simultaneously? for example add a filter here that filters by month in addition to existing year filter? I think this is not easily doable using this approach – kscherrer Feb 17 '20 at 07:59
  • One clarification: the new filter isn't applied absolutely immediately. Instead, loading new items based on a changed filter is done right before the message is sent to the browser (`beforeClientResponse`). This means that if e.g. the same event handler runs `setFilter` twice, then the first filter will be ignored with very minimal performance impact. – Leif Åstrand Feb 17 '20 at 08:02
  • 2
    @kscherrer you can use `addFilter` instead of `setFilter` to add multiple filters. `setFilter` will override any previous filters, `addFilter` will append. – ollitietavainen Feb 17 '20 at 08:03
  • @ollitietavainen but addFilter doesn't clear previous filters, does it? this is something I've seen done in other code examples but I don't understand how it works – kscherrer Feb 17 '20 at 08:06
  • @kscherrer correct, `addFilter` does not clear previous filters. `setFilter` does. Example: you want to filter year 2000 and months before June. First call `setFilter(year == 2000)` and then `addFilter(month < June)`. – ollitietavainen Feb 17 '20 at 08:25
  • yes but then you'll have to clear the filters at some point, when you want to filter by something else after that. How do you know when to clear the filters without giving the user a clear-filters-button? – kscherrer Feb 17 '20 at 08:56
  • @kscherrer If you are filtering by year *and* month, two widgets, when the user changes the state of either widget you will need to clear all filters, and call `addFilter` twice, one for each widget. You cannot remove selectively just one of several filters. Alternatively, you can write one single filter that uses the state of both widgets. – Basil Bourque Feb 17 '20 at 09:04
  • One single filter with `setFilter` is probably the most flexible approach, as then it's up to your code to implement the filtering as you'd like. It depends on the workflow of the UI actions. Sometimes stacking filters with `addFilter` makes sense - if you're narrowing down a date range, for example, it might make sense to start with a year first and add more filters on top of that. Then if you change the year, you might want to reset everything else so use `setFilter`. – ollitietavainen Feb 17 '20 at 10:23
  • This example shows how to filter a grid using one single value. The solution is not really expandable to multiple filters. If you want multiple filters, see the other answer. – kscherrer Feb 21 '20 at 16:08