javacomboboxvaadinvaadin-flow

How to use Custom Combobox Filtering with a DataProvider in Vaadin Flow?


We are currently using Vaadin Flow version 12.0.7 (we cant upgrade for serveral reasons) and we want to override the filtering mechanisms for a ComboBox component. What I mean by that is that we would like to change the way the searching works for the items behind a ComboBox when the user enters input inside the ComboBox.

I was looking at this Vaadin documentation on how to achieve a custom filtering for a combobox. More specifically the section Filtering by a string was looking promising.

According to the Vaadin documentation we implemented a custom interface for adapting the search method of a ComboBox:

public interface CustomerDataFilter {
    List<Customer> fetch(int offset, int limit, String filterText);
    int getCount(String filterText);
}

This is pretty straight forward and matches the documentation example nearly 1:1.

We then created a method according to the documentation to fill the ComboBox with data. Also nearly used 1:1 from the documentation:

private DataProvider<Customer, String>
createDepartmentDataProvider(CustomerDataFilter service)
{
   return DataProvider.fromFilteringCallbacks(query -> {
       // getFilter returns Optional<String>
       String filter = query.getFilter().orElse(null);
       return service.fetch(query.getOffset(),
               query.getLimit(), filter).stream();
   }, query -> {
       String filter = query.getFilter().orElse(null);
       return service.getCount(filter);
   });
}

We call the above method when initializing the data for our ComboBox. At this point we also implement the interface (eventhough that might be ugly). How ever we are not quite sure how to use both of the interface methods. Our first goal was just to return all items at all times and ignore any specific filtering which is why we are returning the entire sorted list at all times. This was just for test purposes to try whether we have changed filtering.

public void updateCustomerList() {
    List<Customer> sorted = CustomerService.getInstance().findAll();
    //for initial sorting
    sorted.sort(new CustomerComperator());

    DataProvider<Customer, String> test = createDepartmentDataProvider(new CustomerDataFilter() {

        @Override
        public int getCount(String filterText) {
            return 0;
        }

        @Override
        public List<Customer> fetch(int offset, int limit, String filterText) {
            return sorted;
        }
    });

    customerCompany.setItems(sorted);
    customerCompany.setDataProvider(test);
}

We tried to increase the getCount value to 1 but as soon as we are setting it higher than 0 the following exceptions occurs:

The number of items returned by the data provider exceeds the limit specified by the query (1).

Our guess is that we have to somehow adjust the getCountand fetch methods in way that they are implementing our customsearch. This would be no problem since we have the data behind the ComboBox and the filterText. How ever we are a bit confused why we need a getCount method as well and how we could bypass the shown exception? Our idea was to let getCount always return like 10 and in fetch return a subset of our sorted list regarding the filterText.

Could anyone elaborate / help us on how to implement custom filtering for a ComboBox or point us to the right track?


Thanks to Steffen Harbichs answer we figured a way out to achieve a filtered dataprovider. Basically this is everthing it takes:

public void updateCustomerList() {
    List<Customer> sorted = CustomerService.getInstance().findAll();
    sorted.sort(new CustomerComperator());
    customerCompany.setItems(sorted);

    ListDataProvider<Customer> listTest = new ListDataProvider<>(sorted);
    customerCompany.setDataProvider(listTest.filteringByPrefix(new CustomerProvider()));
}

The CustomerProvider only implements the applicable DataProvider interface. Afterwards we can then simply apply the filteringByPrefix method to change the filtering behavior.


Solution

  • You're are on the right track but you will need understand the concept of DataProvider first.

    On the client-side (browser), Vaadin request data to be shown in the combo box once the user has some interaction with it. The requested data will be like 'how many items are in the data?' which is the count query. Let's say you have 100 items. Now the next request is 'give me the first 40 items to show' which is the fetch query. The combo box will show the returned items. Once the user scrolls down the list, another request of data is issued 'give me the next 40 items to show' etc.

    To summarize, the DataProvider is first asked to return a count of all items and then the data is fetched page by page on demand. And this applies also when a filter is entered by the user. The difference is that your count and fetch queries should take the filter into account.

    Example: User entered filter 'xyz', now only 50 items match this filter (by name or something else), so you would return 50 in count method and your fetch method should filter accordingly.

    The exception you have encountered just points out that your fetch method returned more items than requested by Vaadin (the 'limit' parameter in query parameter). This is because you don't handle the offset/limit currently.

    You have two options to implement your data provider:

    Use ListDataProvider

    Vaadin provides ListDataProvider which is an implementation of DataProvider for lists. The paging is done already in the implementation, you don't have to care about it. Use the filteringBy method to filter your data according to the entered filter text.

    This way is easier but will not be scalable. That means if you have many items, you will consume a lot of memory and CPU since the whole data is retrieved from backend into the list. If you expect that your item count is low, go for this approach.

    Or implement your own DataProvider

    You could implement your own DataProvider. I recommend to start by extending the AbstractBackendDataProvider class (javadoc). The methods sizeInBackend and fetchFromBackend need to be implemented. Be aware of the offset/limit parameters to implement paging in the fetch method.

    This approach is scalable if you delegate the paging to your database (e.g. SQL or no-sql database). The memory footprint will be low since you just keep one page in memory.