com.vaadin.ui.ComboBox.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.ui.ComboBox.java

Source

/*
 * Copyright 2000-2018 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.vaadin.ui;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

import com.vaadin.data.provider.CallbackDataProvider;
import com.vaadin.data.provider.DataChangeEvent;
import com.vaadin.data.provider.DataCommunicator;
import com.vaadin.data.provider.DataGenerator;
import com.vaadin.data.provider.DataKeyMapper;
import com.vaadin.data.provider.DataProvider;
import com.vaadin.data.provider.InMemoryDataProvider;
import com.vaadin.data.provider.ListDataProvider;
import org.jsoup.nodes.Element;

import com.vaadin.data.HasFilterableDataProvider;
import com.vaadin.data.HasValue;
import com.vaadin.data.ValueProvider;
import com.vaadin.event.FieldEvents;
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcDecorator;
import com.vaadin.event.FieldEvents.FocusEvent;
import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.server.ConnectorResource;
import com.vaadin.server.KeyMapper;
import com.vaadin.server.Resource;
import com.vaadin.server.ResourceReference;
import com.vaadin.server.SerializableBiPredicate;
import com.vaadin.server.SerializableConsumer;
import com.vaadin.server.SerializableFunction;
import com.vaadin.server.SerializableToIntFunction;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.DataCommunicatorConstants;
import com.vaadin.shared.ui.combobox.ComboBoxClientRpc;
import com.vaadin.shared.ui.combobox.ComboBoxConstants;
import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
import com.vaadin.shared.ui.combobox.ComboBoxState;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignFormatter;

import elemental.json.JsonObject;

/**
 * A filtering dropdown single-select. Items are filtered based on user input.
 * Supports the creation of new items when a handler is set by the user.
 *
 * @param <T>
 *            item (bean) type in ComboBox
 * @author Vaadin Ltd
 */
@SuppressWarnings("serial")
public class ComboBox<T> extends AbstractSingleSelect<T>
        implements FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, HasFilterableDataProvider<T, String> {

    /**
     * A callback method for fetching items. The callback is provided with a
     * non-null string filter, offset index and limit.
     *
     * @param <T>
     *            item (bean) type in ComboBox
     * @since 8.0
     */
    @FunctionalInterface
    public interface FetchItemsCallback<T> extends Serializable {

        /**
         * Returns a stream of items that match the given filter, limiting the
         * results with given offset and limit.
         * <p>
         * This method is called after the size of the data set is asked from a
         * related size callback. The offset and limit are promised to be within
         * the size of the data set.
         *
         * @param filter
         *            a non-null filter string
         * @param offset
         *            the first index to fetch
         * @param limit
         *            the fetched item count
         * @return stream of items
         */
        public Stream<T> fetchItems(String filter, int offset, int limit);
    }

    /**
     * Handler that adds a new item based on user input when the new items
     * allowed mode is active.
     * <p>
     * NOTE 1: If the new item is rejected the client must be notified of the
     * fact via ComboBoxClientRpc or selection handling won't complete.
     * </p>
     * <p>
     * NOTE 2: Selection handling must be completed separately if filtering the
     * data source with the same value won't include the new item in the initial
     * list of suggestions. Failing to do so will lead to selection handling
     * never completing and previous selection remaining on the server.
     * </p>
     *
     * @since 8.0
     * @deprecated Since 8.4 replaced by {@link NewItemProvider}.
     */
    @Deprecated
    @FunctionalInterface
    public interface NewItemHandler extends SerializableConsumer<String> {
    }

    /**
     * Provider function that adds a new item based on user input when the new
     * items allowed mode is active. After the new item handling is complete,
     * this function should return {@code Optional.of(text)} for the completion
     * of automatic selection handling. If automatic selection is not wished
     * for, always return {@code Optional.isEmpty()}.
     *
     * @since 8.4
     */
    @FunctionalInterface
    public interface NewItemProvider<T> extends SerializableFunction<String, Optional<T>> {
    }

    /**
     * Item style generator class for declarative support.
     * <p>
     * Provides a straightforward mapping between an item and its style.
     *
     * @param <T>
     *            item type
     * @since 8.0
     */
    protected static class DeclarativeStyleGenerator<T> implements StyleGenerator<T> {

        private StyleGenerator<T> fallback;
        private Map<T, String> styles = new HashMap<>();

        public DeclarativeStyleGenerator(StyleGenerator<T> fallback) {
            this.fallback = fallback;
        }

        @Override
        public String apply(T item) {
            return styles.containsKey(item) ? styles.get(item) : fallback.apply(item);
        }

        /**
         * Sets a {@code style} for the {@code item}.
         *
         * @param item
         *            a data item
         * @param style
         *            a style for the {@code item}
         */
        protected void setStyle(T item, String style) {
            styles.put(item, style);
        }
    }

    private ComboBoxServerRpc rpc = new ComboBoxServerRpc() {
        @Override
        public void createNewItem(String itemValue) {
            // New option entered
            boolean added = false;
            if (itemValue != null && !itemValue.isEmpty()) {
                if (getNewItemProvider() != null) {
                    Optional<T> item = getNewItemProvider().apply(itemValue);
                    added = item.isPresent();
                    // Fixes issue
                    // https://github.com/vaadin/framework/issues/11343
                    // Update the internal selection state immediately to avoid
                    // client side hanging. This is needed for cases that user
                    // interaction fires multi events (like adding and deleting)
                    // on a new item during the same round trip.
                    item.ifPresent(value -> {
                        setSelectedItem(value, true);
                        getDataCommunicator().reset();
                    });
                } else if (getNewItemHandler() != null) {
                    getNewItemHandler().accept(itemValue);
                    // Up to the user to tell if no item was added.
                    added = true;
                }
            }

            if (!added) {
                // New item was not handled.
                getRpcProxy(ComboBoxClientRpc.class).newItemNotAdded(itemValue);
            }
        }

        @Override
        public void setFilter(String filterText) {
            getState().currentFilterText = filterText;
            filterSlot.accept(filterText);
        }

        @Override
        public void resetForceDataSourceUpdate() {
            getState().forceDataSourceUpdate = false;
        }
    };

    /**
     * Handler for new items entered by the user.
     */
    @Deprecated
    private NewItemHandler newItemHandler;

    /**
     * Provider function for new items entered by the user.
     */
    private NewItemProvider<T> newItemProvider;

    private StyleGenerator<T> itemStyleGenerator = item -> null;

    private SerializableConsumer<String> filterSlot = filter -> {
        // Just ignore when neither setDataProvider nor setItems has been called
    };

    /**
     * Constructs an empty combo box without a caption. The content of the combo
     * box can be set with {@link #setDataProvider(DataProvider)} or
     * {@link #setItems(Collection)}
     */
    public ComboBox() {
        this(new DataCommunicator<T>() {
            @Override
            protected DataKeyMapper<T> createKeyMapper(ValueProvider<T, Object> identifierGetter) {
                return new KeyMapper<T>(identifierGetter) {
                    @Override
                    public void remove(T removeobj) {
                        // never remove keys from ComboBox to support selection
                        // of items that are not currently visible
                    }
                };
            }
        });
    }

    /**
     * Constructs an empty combo box, whose content can be set with
     * {@link #setDataProvider(DataProvider)} or {@link #setItems(Collection)}.
     *
     * @param caption
     *            the caption to show in the containing layout, null for no
     *            caption
     */
    public ComboBox(String caption) {
        this();
        setCaption(caption);
    }

    /**
     * Constructs a combo box with a static in-memory data provider with the
     * given options.
     *
     * @param caption
     *            the caption to show in the containing layout, null for no
     *            caption
     * @param options
     *            collection of options, not null
     */
    public ComboBox(String caption, Collection<T> options) {
        this(caption);

        setItems(options);
    }

    /**
     * Constructs and initializes an empty combo box.
     *
     * @param dataCommunicator
     *            the data comnunicator to use with this ComboBox
     * @since 8.5
     */
    protected ComboBox(DataCommunicator<T> dataCommunicator) {
        super(dataCommunicator);
        init();
    }

    /**
     * Initialize the ComboBox with default settings and register client to
     * server RPC implementation.
     */
    private void init() {
        registerRpc(rpc);
        registerRpc(new FocusAndBlurServerRpcDecorator(this, this::fireEvent));

        addDataGenerator(new DataGenerator<T>() {

            /**
             * Map for storing names for icons.
             */
            private Map<Object, String> resourceKeyMap = new HashMap<>();
            private int counter = 0;

            @Override
            public void generateData(T item, JsonObject jsonObject) {
                String caption = getItemCaptionGenerator().apply(item);
                if (caption == null) {
                    caption = "";
                }
                jsonObject.put(DataCommunicatorConstants.NAME, caption);
                String style = itemStyleGenerator.apply(item);
                if (style != null) {
                    jsonObject.put(ComboBoxConstants.STYLE, style);
                }
                Resource icon = getItemIcon(item);
                if (icon != null) {
                    String iconKey = resourceKeyMap.get(getDataProvider().getId(item));
                    String iconUrl = ResourceReference.create(icon, ComboBox.this, iconKey).getURL();
                    jsonObject.put(ComboBoxConstants.ICON, iconUrl);
                }
            }

            @Override
            public void destroyData(T item) {
                Object itemId = getDataProvider().getId(item);
                if (resourceKeyMap.containsKey(itemId)) {
                    setResource(resourceKeyMap.get(itemId), null);
                    resourceKeyMap.remove(itemId);
                }
            }

            private Resource getItemIcon(T item) {
                Resource icon = getItemIconGenerator().apply(item);
                if (icon == null || !(icon instanceof ConnectorResource)) {
                    return icon;
                }

                Object itemId = getDataProvider().getId(item);
                if (!resourceKeyMap.containsKey(itemId)) {
                    resourceKeyMap.put(itemId, "icon" + (counter++));
                }
                setResource(resourceKeyMap.get(itemId), icon);
                return icon;
            }
        });
    }

    /**
     * {@inheritDoc}
     * <p>
     * Filtering will use a case insensitive match to show all items where the
     * filter text is a substring of the caption displayed for that item.
     */
    @Override
    public void setItems(Collection<T> items) {
        ListDataProvider<T> listDataProvider = DataProvider.ofCollection(items);

        setDataProvider(listDataProvider);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Filtering will use a case insensitive match to show all items where the
     * filter text is a substring of the caption displayed for that item.
     */
    @Override
    public void setItems(Stream<T> streamOfItems) {
        // Overridden only to add clarification to javadocs
        super.setItems(streamOfItems);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Filtering will use a case insensitive match to show all items where the
     * filter text is a substring of the caption displayed for that item.
     */
    @Override
    public void setItems(T... items) {
        // Overridden only to add clarification to javadocs
        super.setItems(items);
    }

    /**
     * Sets a list data provider as the data provider of this combo box.
     * Filtering will use a case insensitive match to show all items where the
     * filter text is a substring of the caption displayed for that item.
     * <p>
     * Note that this is a shorthand that calls
     * {@link #setDataProvider(DataProvider)} with a wrapper of the provided
     * list data provider. This means that {@link #getDataProvider()} will
     * return the wrapper instead of the original list data provider.
     *
     * @param listDataProvider
     *            the list data provider to use, not <code>null</code>
     * @since 8.0
     */
    public void setDataProvider(ListDataProvider<T> listDataProvider) {
        // Cannot use the case insensitive contains shorthand from
        // ListDataProvider since it wouldn't react to locale changes
        CaptionFilter defaultCaptionFilter = (itemText, filterText) -> itemText.toLowerCase(getLocale())
                .contains(filterText.toLowerCase(getLocale()));

        setDataProvider(defaultCaptionFilter, listDataProvider);
    }

    /**
     * Sets the data items of this listing and a simple string filter with which
     * the item string and the text the user has input are compared.
     * <p>
     * Note that unlike {@link #setItems(Collection)}, no automatic case
     * conversion is performed before the comparison.
     *
     * @param captionFilter
     *            filter to check if an item is shown when user typed some text
     *            into the ComboBox
     * @param items
     *            the data items to display
     * @since 8.0
     */
    public void setItems(CaptionFilter captionFilter, Collection<T> items) {
        ListDataProvider<T> listDataProvider = DataProvider.ofCollection(items);

        setDataProvider(captionFilter, listDataProvider);
    }

    /**
     * Sets a list data provider with an item caption filter as the data
     * provider of this combo box. The caption filter is used to compare the
     * displayed caption of each item to the filter text entered by the user.
     *
     * @param captionFilter
     *            filter to check if an item is shown when user typed some text
     *            into the ComboBox
     * @param listDataProvider
     *            the list data provider to use, not <code>null</code>
     * @since 8.0
     */
    public void setDataProvider(CaptionFilter captionFilter, ListDataProvider<T> listDataProvider) {
        Objects.requireNonNull(listDataProvider, "List data provider cannot be null");

        // Must do getItemCaptionGenerator() for each operation since it might
        // not be the same as when this method was invoked
        setDataProvider(listDataProvider,
                filterText -> item -> captionFilter.test(getItemCaptionOfItem(item), filterText));
    }

    // Helper method for the above to make lambda more readable
    private String getItemCaptionOfItem(T item) {
        String caption = getItemCaptionGenerator().apply(item);
        if (caption == null) {
            caption = "";
        }
        return caption;
    }

    /**
     * Sets the data items of this listing and a simple string filter with which
     * the item string and the text the user has input are compared.
     * <p>
     * Note that unlike {@link #setItems(Collection)}, no automatic case
     * conversion is performed before the comparison.
     *
     * @param captionFilter
     *            filter to check if an item is shown when user typed some text
     *            into the ComboBox
     * @param items
     *            the data items to display
     * @since 8.0
     */
    public void setItems(CaptionFilter captionFilter, @SuppressWarnings("unchecked") T... items) {
        setItems(captionFilter, Arrays.asList(items));
    }

    /**
     * Gets the current placeholder text shown when the combo box would be
     * empty.
     *
     * @see #setPlaceholder(String)
     * @return the current placeholder string, or null if not enabled
     * @since 8.0
     */
    public String getPlaceholder() {
        return getState(false).placeholder;
    }

    /**
     * Sets the placeholder string - a textual prompt that is displayed when the
     * select would otherwise be empty, to prompt the user for input.
     *
     * @param placeholder
     *            the desired placeholder, or null to disable
     * @since 8.0
     */
    public void setPlaceholder(String placeholder) {
        getState().placeholder = placeholder;
    }

    /**
     * Sets whether it is possible to input text into the field or whether the
     * field area of the component is just used to show what is selected. By
     * disabling text input, the comboBox will work in the same way as a
     * {@link NativeSelect}
     *
     * @see #isTextInputAllowed()
     *
     * @param textInputAllowed
     *            true to allow entering text, false to just show the current
     *            selection
     */
    public void setTextInputAllowed(boolean textInputAllowed) {
        getState().textInputAllowed = textInputAllowed;
    }

    /**
     * Returns true if the user can enter text into the field to either filter
     * the selections or enter a new value if new item provider or handler is
     * set (see {@link #setNewItemProvider(NewItemProvider)} (recommended) and
     * {@link #setNewItemHandler(NewItemHandler)} (deprecated)). If text input
     * is disabled, the comboBox will work in the same way as a
     * {@link NativeSelect}
     *
     * @return true if text input is allowed
     */
    public boolean isTextInputAllowed() {
        return getState(false).textInputAllowed;
    }

    @Override
    public Registration addBlurListener(BlurListener listener) {
        return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, BlurListener.blurMethod);
    }

    @Override
    public Registration addFocusListener(FocusListener listener) {
        return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, FocusListener.focusMethod);
    }

    /**
     * Returns the page length of the suggestion popup.
     *
     * @return the pageLength
     */
    public int getPageLength() {
        return getState(false).pageLength;
    }

    /**
     * Returns the suggestion pop-up's width as a CSS string. By default this
     * width is set to "100%".
     *
     * @see #setPopupWidth
     * @since 7.7
     * @return explicitly set popup width as CSS size string or null if not set
     */
    public String getPopupWidth() {
        return getState(false).suggestionPopupWidth;
    }

    /**
     * Sets the page length for the suggestion popup. Setting the page length to
     * 0 will disable suggestion popup paging (all items visible).
     *
     * @param pageLength
     *            the pageLength to set
     */
    public void setPageLength(int pageLength) {
        getState().pageLength = pageLength;
    }

    /**
     * Returns whether the user is allowed to select nothing in the combo box.
     *
     * @return true if empty selection is allowed, false otherwise
     * @since 8.0
     */
    public boolean isEmptySelectionAllowed() {
        return getState(false).emptySelectionAllowed;
    }

    /**
     * Sets whether the user is allowed to select nothing in the combo box. When
     * true, a special empty item is shown to the user.
     *
     * @param emptySelectionAllowed
     *            true to allow not selecting anything, false to require
     *            selection
     * @since 8.0
     */
    public void setEmptySelectionAllowed(boolean emptySelectionAllowed) {
        getState().emptySelectionAllowed = emptySelectionAllowed;
    }

    /**
     * Returns the empty selection caption.
     * <p>
     * The empty string {@code ""} is the default empty selection caption.
     *
     * @return the empty selection caption, not {@code null}
     * @see #setEmptySelectionAllowed(boolean)
     * @see #isEmptySelectionAllowed()
     * @see #setEmptySelectionCaption(String)
     * @see #isSelected(Object)
     * @since 8.0
     */
    public String getEmptySelectionCaption() {
        return getState(false).emptySelectionCaption;
    }

    /**
     * Sets the empty selection caption.
     * <p>
     * The empty string {@code ""} is the default empty selection caption.
     * <p>
     * If empty selection is allowed via the
     * {@link #setEmptySelectionAllowed(boolean)} method (it is by default) then
     * the empty item will be shown with the given caption.
     *
     * @param caption
     *            the caption to set, not {@code null}
     * @see #isSelected(Object)
     * @since 8.0
     */
    public void setEmptySelectionCaption(String caption) {
        Objects.nonNull(caption);
        getState().emptySelectionCaption = caption;
    }

    /**
     * Sets the suggestion pop-up's width as a CSS string. By using relative
     * units (e.g. "50%") it's possible to set the popup's width relative to the
     * ComboBox itself.
     * <p>
     * By default this width is set to "100%" so that the pop-up's width is
     * equal to the width of the combobox. By setting width to null the pop-up's
     * width will automatically expand beyond 100% relative width to fit the
     * content of all displayed items.
     *
     * @see #getPopupWidth()
     * @since 7.7
     * @param width
     *            the width
     */
    public void setPopupWidth(String width) {
        getState().suggestionPopupWidth = width;
    }

    /**
     * Sets whether to scroll the selected item visible (directly open the page
     * on which it is) when opening the combo box popup or not.
     * <p>
     * This requires finding the index of the item, which can be expensive in
     * many large lazy loading containers.
     *
     * @param scrollToSelectedItem
     *            true to find the page with the selected item when opening the
     *            selection popup
     */
    public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
        getState().scrollToSelectedItem = scrollToSelectedItem;
    }

    /**
     * Returns true if the select should find the page with the selected item
     * when opening the popup.
     *
     * @see #setScrollToSelectedItem(boolean)
     *
     * @return true if the page with the selected item will be shown when
     *         opening the popup
     */
    public boolean isScrollToSelectedItem() {
        return getState(false).scrollToSelectedItem;
    }

    @Override
    public ItemCaptionGenerator<T> getItemCaptionGenerator() {
        return super.getItemCaptionGenerator();
    }

    @Override
    public void setItemCaptionGenerator(ItemCaptionGenerator<T> itemCaptionGenerator) {
        super.setItemCaptionGenerator(itemCaptionGenerator);
    }

    /**
     * Sets the style generator that is used to produce custom class names for
     * items visible in the popup. The CSS class name that will be added to the
     * item is <tt>v-filterselect-item-[style name]</tt>. Returning null from
     * the generator results in no custom style name being set.
     *
     * @see StyleGenerator
     *
     * @param itemStyleGenerator
     *            the item style generator to set, not null
     * @throws NullPointerException
     *             if {@code itemStyleGenerator} is {@code null}
     * @since 8.0
     */
    public void setStyleGenerator(StyleGenerator<T> itemStyleGenerator) {
        Objects.requireNonNull(itemStyleGenerator, "Item style generator must not be null");
        this.itemStyleGenerator = itemStyleGenerator;
        getDataCommunicator().reset();
    }

    /**
     * Gets the currently used style generator that is used to generate CSS
     * class names for items. The default item style provider returns null for
     * all items, resulting in no custom item class names being set.
     *
     * @see StyleGenerator
     * @see #setStyleGenerator(StyleGenerator)
     *
     * @return the currently used item style generator, not null
     * @since 8.0
     */
    public StyleGenerator<T> getStyleGenerator() {
        return itemStyleGenerator;
    }

    @Override
    public void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
        super.setItemIconGenerator(itemIconGenerator);
    }

    @Override
    public IconGenerator<T> getItemIconGenerator() {
        return super.getItemIconGenerator();
    }

    /**
     * Sets the handler that is called when user types a new item. The creation
     * of new items is allowed when a new item handler has been set. If new item
     * provider is also set, the new item handler is ignored.
     *
     * @param newItemHandler
     *            handler called for new items, null to only permit the
     *            selection of existing items, all options ignored if new item
     *            provider is set
     * @since 8.0
     * @deprecated Since 8.4 use {@link #setNewItemProvider(NewItemProvider)}
     *             instead.
     */
    @Deprecated
    public void setNewItemHandler(NewItemHandler newItemHandler) {
        getLogger().log(Level.WARNING, "NewItemHandler is deprecated. Please use NewItemProvider instead.");
        this.newItemHandler = newItemHandler;
        getState(true).allowNewItems = newItemProvider != null || newItemHandler != null;
    }

    /**
     * Sets the provider function that is called when user types a new item. The
     * creation of new items is allowed when a new item provider has been set.
     * If a deprecated new item handler is also set it is ignored in favor of
     * new item provider.
     *
     * @param newItemProvider
     *            provider function that is called for new items, null to only
     *            permit the selection of existing items or to use a deprecated
     *            new item handler if set
     * @since 8.4
     */
    public void setNewItemProvider(NewItemProvider<T> newItemProvider) {
        this.newItemProvider = newItemProvider;
        getState(true).allowNewItems = newItemProvider != null || newItemHandler != null;
    }

    /**
     * Returns the handler called when the user enters a new item (not present
     * in the data provider).
     *
     * @return new item handler or null if none specified
     * @deprecated Since 8.4 use {@link #getNewItemProvider()} instead.
     */
    @Deprecated
    public NewItemHandler getNewItemHandler() {
        return newItemHandler;
    }

    /**
     * Returns the provider function that is called when the user enters a new
     * item (not present in the data provider).
     *
     * @since 8.4
     * @return new item provider or null if none specified
     */
    public NewItemProvider<T> getNewItemProvider() {
        return newItemProvider;
    }

    // HasValue methods delegated to the selection model

    @Override
    public Registration addValueChangeListener(HasValue.ValueChangeListener<T> listener) {
        return addSelectionListener(event -> listener.valueChange(
                new ValueChangeEvent<>(event.getComponent(), this, event.getOldValue(), event.isUserOriginated())));
    }

    @Override
    protected ComboBoxState getState() {
        return (ComboBoxState) super.getState();
    }

    @Override
    protected ComboBoxState getState(boolean markAsDirty) {
        return (ComboBoxState) super.getState(markAsDirty);
    }

    @Override
    protected void updateSelectedItemState(T value) {
        super.updateSelectedItemState(value);

        updateSelectedItemCaption(value);
        updateSelectedItemIcon(value);
    }

    private void updateSelectedItemCaption(T value) {
        String selectedCaption = null;
        if (value != null) {
            selectedCaption = getItemCaptionGenerator().apply(value);
        }
        getState().selectedItemCaption = selectedCaption;
    }

    private void updateSelectedItemIcon(T value) {
        String selectedItemIcon = null;
        if (value != null) {
            Resource icon = getItemIconGenerator().apply(value);
            if (icon != null) {
                if (icon instanceof ConnectorResource) {
                    if (!isAttached()) {
                        // Deferred resource generation.
                        return;
                    }
                    setResource("selected", icon);
                }
                selectedItemIcon = ResourceReference.create(icon, ComboBox.this, "selected").getURL();
            }
        }
        getState().selectedItemIcon = selectedItemIcon;
    }

    @Override
    public void attach() {
        super.attach();

        // Update icon for ConnectorResource
        updateSelectedItemIcon(getValue());
    }

    @Override
    protected Element writeItem(Element design, T item, DesignContext context) {
        Element element = design.appendElement("option");

        String caption = getItemCaptionGenerator().apply(item);
        if (caption != null) {
            element.html(DesignFormatter.encodeForTextNode(caption));
        } else {
            element.html(DesignFormatter.encodeForTextNode(item.toString()));
        }
        element.attr("item", item.toString());

        Resource icon = getItemIconGenerator().apply(item);
        if (icon != null) {
            DesignAttributeHandler.writeAttribute("icon", element.attributes(), icon, null, Resource.class,
                    context);
        }

        String style = getStyleGenerator().apply(item);
        if (style != null) {
            element.attr("style", style);
        }

        if (isSelected(item)) {
            element.attr("selected", true);
        }

        return element;
    }

    @Override
    protected void readItems(Element design, DesignContext context) {
        setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator()));
        super.readItems(design, context);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    protected T readItem(Element child, Set<T> selected, DesignContext context) {
        T item = super.readItem(child, selected, context);

        if (child.hasAttr("style")) {
            StyleGenerator<T> styleGenerator = getStyleGenerator();
            if (styleGenerator instanceof DeclarativeStyleGenerator) {
                ((DeclarativeStyleGenerator) styleGenerator).setStyle(item, child.attr("style"));
            } else {
                throw new IllegalStateException(
                        String.format("Don't know how " + "to set style using current style generator '%s'",
                                styleGenerator.getClass().getName()));
            }
        }
        return item;
    }

    @Override
    public DataProvider<T, ?> getDataProvider() {
        return internalGetDataProvider();
    }

    @Override
    public <C> void setDataProvider(DataProvider<T, C> dataProvider,
            SerializableFunction<String, C> filterConverter) {
        Objects.requireNonNull(dataProvider, "dataProvider cannot be null");
        Objects.requireNonNull(filterConverter, "filterConverter cannot be null");

        SerializableFunction<String, C> convertOrNull = filterText -> {
            if (filterText == null || filterText.isEmpty()) {
                return null;
            }

            return filterConverter.apply(filterText);
        };

        SerializableConsumer<C> providerFilterSlot = internalSetDataProvider(dataProvider,
                convertOrNull.apply(getState(false).currentFilterText));

        filterSlot = filter -> providerFilterSlot.accept(convertOrNull.apply(filter));

        // This workaround is done to fix issue #11642 for unpaged comboboxes.
        // Data sources for on the client need to be updated after data provider
        // refreshAll so that serverside selection works even before the
        // dropdown
        // is opened. Only done for in-memory data providers for performance
        // reasons.
        if (dataProvider instanceof InMemoryDataProvider) {
            dataProvider.addDataProviderListener(event -> {
                if ((!(event instanceof DataChangeEvent.DataRefreshEvent)) && (getPageLength() == 0)) {
                    getState().forceDataSourceUpdate = true;
                }
            });
        }
    }

    /**
     * Sets a CallbackDataProvider using the given fetch items callback and a
     * size callback.
     * <p>
     * This method is a shorthand for making a {@link CallbackDataProvider} that
     * handles a partial {@link com.vaadin.data.provider.Query Query} object.
     *
     * @param fetchItems
     *            a callback for fetching items
     * @param sizeCallback
     *            a callback for getting the count of items
     *
     * @see CallbackDataProvider
     * @see #setDataProvider(DataProvider)
     */
    public void setDataProvider(FetchItemsCallback<T> fetchItems, SerializableToIntFunction<String> sizeCallback) {
        setDataProvider(new CallbackDataProvider<>(
                q -> fetchItems.fetchItems(q.getFilter().orElse(""), q.getOffset(), q.getLimit()),
                q -> sizeCallback.applyAsInt(q.getFilter().orElse(""))));
    }

    /**
     * Predicate to check {@link ComboBox} item captions against user typed
     * strings.
     *
     * @see ComboBox#setItems(CaptionFilter, Collection)
     * @see ComboBox#setItems(CaptionFilter, Object[])
     * @since 8.0
     */
    @FunctionalInterface
    public interface CaptionFilter extends SerializableBiPredicate<String, String> {

        /**
         * Check item caption against entered text.
         *
         * @param itemCaption
         *            the caption of the item to filter, not {@code null}
         * @param filterText
         *            user entered filter, not {@code null}
         * @return {@code true} if item passes the filter and should be listed,
         *         {@code false} otherwise
         */
        @Override
        public boolean test(String itemCaption, String filterText);
    }

    private static Logger getLogger() {
        return Logger.getLogger(ComboBox.class.getName());
    }
}