Combobox with custom filtering

Hi,

I would like to implement a Combobox, with a different filter, I know you can set 3 different filters:
FILTERINGMODE_OFF
FILTERINGMODE_STARTSWITH
FILTERINGMODE_CONTAINS

But in my case in the options I have something like:

Barack Obama (USA)
Fidel Castro (CUB)
Angela Merkel (GER)

What I would like to have is that if the user starts to write “B” or “O” or “U” the combobox will suggests “Barack Obama (USA)”, filtering by name, surname, and nation (with the startswith option),.

Cause, with the normal filter (FILTERINGMODE_STARTSWITH), it works only for first name…
There is a way to implement this?

Thank you in advance

Hi,

ComboBox filtering is done on the server-side with a container filter. The filter is created in the buildFilter() method, which is rather simple and you can override it to create a custom filter. Haven’t tried that but I think it should work that way.

See also the
book section on container filters
, you probably need a custom one.

Thank you for your reply, But I didn’t understand your suggestion:

I created a CustomCombobox like this:


public class CustomCombobox extends ComboBox{;

	@Override
	protected Filter buildFilter(String filterString, int filteringMode){
		Filter filter = new MyComboboxFilter(getItemCaptionPropertyId(), filterString, true);
		return filter;
	}
}

and this is my “MyComboboxFilter”:


public final class MyComboboxFilter implements Filter {

	    /**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		final Object propertyId;
	    final String filterString;
	    final boolean ignoreCase;
	    //final boolean onlyMatchPrefix;

	    public MyComboboxFilter(Object propertyId, String filterString,
	            boolean ignoreCase) {
	        this.propertyId = propertyId;
	        this.filterString = ignoreCase ? filterString.toLowerCase()
	                : filterString;
	        this.ignoreCase = ignoreCase;
	       // this.onlyMatchPrefix = onlyMatchPrefix;
	    }

	    public boolean passesFilter(Object itemId, Item item) {
	        final Property p = item.getItemProperty(propertyId);
	        if (p == null || p.toString() == null) {
	            return false;
	        }
	        final String value = ignoreCase ? p.toString().toLowerCase() : p
	                .toString();
	        
	        
	        Scanner scanner = new Scanner(value).useDelimiter("\\s");

			ArrayList<String> array= new ArrayList<String>();

			while (scanner.hasNext())
				array.add(scanner.next());
	        
	        	for (String string:array)
	            if (string.startsWith(filterString)) {
	                return true;
	            }
	           return false;
	    }

	    public boolean appliesToProperty(Object propertyId) {
	        return this.propertyId.equals(propertyId);
	    }

	    @Override
	    public boolean equals(Object obj) {

	        // Only ones of the objects of the same class can be equal
	        if (!(obj instanceof MyComboboxFilter)) {
	            return false;
	        }
	        final MyComboboxFilter o = (MyComboboxFilter) obj;

	        // Checks the properties one by one
	        if (propertyId != o.propertyId && o.propertyId != null
	                && !o.propertyId.equals(propertyId)) {
	            return false;
	        }
	        if (filterString != o.filterString && o.filterString != null
	                && !o.filterString.equals(filterString)) {
	            return false;
	        }
	        if (ignoreCase != o.ignoreCase) {
	            return false;
	        }
	       

	        return true;
	    }

	    @Override
	    public int hashCode() {
	        return (propertyId != null ? propertyId.hashCode() : 0)
	                ^ (filterString != null ? filterString.hashCode() : 0);
	    }

	    /**
	     * Returns the property identifier to which this filter applies.
	     * 
	     * @return property id
	     */
	    public Object getPropertyId() {
	        return propertyId;
	    }

	    /**
	     * Returns the filter string.
	     * 
	     * Note: this method is intended only for implementations of lazy string
	     * filters and may change in the future.
	     * 
	     * @return filter string given to the constructor
	     */
	    public String getFilterString() {
	        return filterString;
	    }

	    /**
	     * Returns whether the filter is case-insensitive or case-sensitive.
	     * 
	     * Note: this method is intended only for implementations of lazy string
	     * filters and may change in the future.
	     * 
	     * @return true if performing case-insensitive filtering, false for
	     *         case-sensitive
	     */
	    public boolean isIgnoreCase() {
	        return ignoreCase;
	    }

Now when I create my Combobox I try to do something like that:


final CustomCombobox customCb= new CustomCombobox();
BeanItemContainer<String> bic = new BeanItemContainer<String>(String.class);
bic.addAll(stringArrayList);
customCb.setContainerDataSource(bic);

But the new Filter is never called…

What is my mistake?

Thank you

But I can see that is called this method: “getFilteredOptions()” of Select class… that filter the results…

ComboBox uses two different approaches to filtering depending on the item caption mode, the types of filters in use etc.

If it finds that it can let the container perform the filtering (which might happen e.g. directly in the database), that is done. If not, in-memory filtering is performed. Container based filtering can only be performed in ITEM_CAPTION_MODE_PROPERTY and there are also some other preconditions for being able to use it.

Your code hooks into buildFilter() which is only meant for container based filtering and does not work with the default item caption mode.

Thank you for your reply,
So if I understood correctly there is no way to override the “in-memory filtering” starting from “ComboBox”… Unless I rewrite the “Select” class… So what is a simple solution to solve my problem?

Thanks

In theory, overriding getFilteredOptions() would be the way but it is using a number of private variables so it is not really possible in practice to override it in the current version. I hope we’ll get to rewriting the ComboBox at some point, but it is a big job especially on the client side and there are other, more urgent tasks before that.

The options I see right now are therefore to either use the ITEM_CAPTION_MODE_PROPERTY (at which point it should just work, but toString() is not used to render item captions) or create a copy of the class ComboBox in your project with the appropriate modifications.

If you take the latter approach, I would recommend that the only changes you do in your copy are making some fields ans possibly methods protected, and doing all the real changes in a subclass. The copy should be in the same package as the original so it “overrides” the one in Vaadin as your code is first on the classpath.

Thank you very much, I did as suggested by you! It works!

Have a nice day :wink:

Hi , I still dont get it, can you please share your code? By the way, is there way to highlight characters in the selection as an user types. Thanks.

Hi,

I needed to change the ComboBox filter in order to add the accent-insensitive behavior. Here you have my implementation (I do not use ComboBox directly anymore), I don’t know if it is the best way to do but this is how I’ve done it:

/**
 * @author "Léo Millon <leo.millon@declasin.com>"
 *
 */
public class SimpleComboBox extends ComboBox {
	
	private boolean ignoreAccents;
	private List<Object> customFilteredOptions;
	private String customPrevfilterstring;
	private String customFilterstring;
	
	/**
	 * @return the ignoreAccents
	 */
	public boolean isIgnoreAccents() {
		return ignoreAccents;
	}

	/**
	 * @param ignoreAccents the ignoreAccents to set
	 */
	public void setIgnoreAccents(boolean ignoreAccents) {
		this.ignoreAccents = ignoreAccents;
	}

	@Override
	public void changeVariables(Object source, Map<String, Object> variables) {
		super.changeVariables(source, variables);
		customFilterstring = (String) variables.get("filter");
	}
	
	@Override
	protected List<?> getFilteredOptions() {
		if (ignoreAccents) {
			if (null == customFilterstring || "".equals(customFilterstring)
	                || FILTERINGMODE_OFF == getFilteringMode()) {
	            customPrevfilterstring = null;
	            customFilteredOptions = new LinkedList<Object>(getItemIds());
	            return customFilteredOptions;
	        }

	        if (customFilterstring.equals(customPrevfilterstring)) {
	            return customFilteredOptions;
	        }

	        Collection<?> items;
	        if (customPrevfilterstring != null
	                && customFilterstring.startsWith(customPrevfilterstring)) {
	            items = customFilteredOptions;
	        } else {
	            items = getItemIds();
	        }
	        customPrevfilterstring = customFilterstring;

	        customFilteredOptions = new LinkedList<Object>();
	        for (final Iterator<?> it = items.iterator(); it.hasNext();) {
	            final Object itemId = it.next();
	            String caption = getItemCaption(itemId);
	            switch (getFilteringMode()) {
	            case FILTERINGMODE_CONTAINS:
	            	if (TextUtils.passesFilter(customFilterstring, caption, FilteringMode.CONTAINS)) {
		            	customFilteredOptions.add(itemId);
		            }
	                break;
	            case FILTERINGMODE_STARTSWITH:
	            default:
	            	if (TextUtils.passesFilter(customFilterstring, caption, FilteringMode.STARTS_WITH)) {
		            	customFilteredOptions.add(itemId);
		            }
	                break;
	            }
	            
	        }

	        return customFilteredOptions;
		}
		else {
			return super.getFilteredOptions();
		}
	}
	
}
TextUtils.passesFilter(customFilterstring, caption, FilteringMode.STARTS_WITH)

This checkes if the caption passes the filter.

Hope, it helped you!

Thanks a LOT Léo, this really helped me.

Adding a simple implementation of a “passesFilter” method for CONTAINS filtering, for the SimpleComboBox posted above to allow filtering ignoring special characters such as accents and ignoring case.

 private boolean passesFilter(String customFilterString, String caption, FilteringMode filteringMode) {
        switch (filteringMode) {
        case CONTAINS:
            if (caption.toLowerCase().contains(customFilterString.toLowerCase())) {
                return true;
            } else if (Normalizer.normalize(caption, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]
", "").toLowerCase().contains(customFilterString.toLowerCase())) {
                return true;
            } else {
                return false;
            }
        default:
            return false;
        }
    }