All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.component.combobox.ComboBoxBase Maven / Gradle / Ivy

There is a newer version: 24.5.4
Show newest version
/*
 * Copyright 2000-2024 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.flow.component.combobox;

import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.AbstractSinglePropertyField;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.DomEvent;
import com.vaadin.flow.component.EventData;
import com.vaadin.flow.component.Focusable;
import com.vaadin.flow.component.HasAriaLabel;
import com.vaadin.flow.component.HasPlaceholder;
import com.vaadin.flow.component.HasTheme;
import com.vaadin.flow.component.ItemLabelGenerator;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.combobox.dataview.ComboBoxDataView;
import com.vaadin.flow.component.combobox.dataview.ComboBoxLazyDataView;
import com.vaadin.flow.component.combobox.dataview.ComboBoxListDataView;
import com.vaadin.flow.component.shared.ClientValidationUtil;
import com.vaadin.flow.component.shared.HasAllowedCharPattern;
import com.vaadin.flow.component.shared.HasAutoOpen;
import com.vaadin.flow.component.shared.HasClearButton;
import com.vaadin.flow.component.shared.HasClientValidation;
import com.vaadin.flow.component.shared.HasOverlayClassName;
import com.vaadin.flow.component.shared.HasValidationProperties;
import com.vaadin.flow.component.shared.InputField;
import com.vaadin.flow.component.shared.ValidationUtil;
import com.vaadin.flow.component.shared.internal.ValidationController;
import com.vaadin.flow.data.binder.HasValidator;
import com.vaadin.flow.data.binder.Validator;
import com.vaadin.flow.data.provider.BackEndDataProvider;
import com.vaadin.flow.data.provider.CallbackDataProvider;
import com.vaadin.flow.data.provider.CompositeDataGenerator;
import com.vaadin.flow.data.provider.DataCommunicator;
import com.vaadin.flow.data.provider.DataKeyMapper;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.DataView;
import com.vaadin.flow.data.provider.HasDataView;
import com.vaadin.flow.data.provider.HasLazyDataView;
import com.vaadin.flow.data.provider.HasListDataView;
import com.vaadin.flow.data.provider.InMemoryDataProvider;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.data.provider.ListDataView;
import com.vaadin.flow.data.renderer.Renderer;
import com.vaadin.flow.function.SerializableBiFunction;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.function.SerializableFunction;
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.function.SerializableSupplier;
import com.vaadin.flow.shared.Registration;

/**
 * Provides base functionality for combo box related components, such as
 * {@link ComboBox}
 *
 * @param 
 *            Type of the component that extends from this class
 * @param 
 *            Type of individual items that are selectable in the combo box
 * @param 
 *            Type of the selection / value of the extending component
 */
public abstract class ComboBoxBase, TItem, TValue>
        extends AbstractSinglePropertyField
        implements Focusable, HasAllowedCharPattern, HasAriaLabel,
        HasAutoOpen, HasClearButton, HasClientValidation, HasOverlayClassName,
        HasDataView>,
        InputField, TValue>,
        HasLazyDataView>,
        HasListDataView>, HasTheme,
        HasValidationProperties, HasValidator, HasPlaceholder {

    /**
     * Registration for custom value listeners that disallows entering custom
     * values as soon as there are no more listeners for the custom value event
     */
    private class CustomValueRegistration implements Registration {

        private Registration delegate;

        private CustomValueRegistration(Registration delegate) {
            this.delegate = delegate;
        }

        @Override
        public void remove() {
            if (delegate != null) {
                delegate.remove();
                customValueListenersCount--;

                if (customValueListenersCount == 0) {
                    setAllowCustomValue(false);
                }
                delegate = null;
            }
        }
    }

    private ItemLabelGenerator itemLabelGenerator = String::valueOf;
    private SerializableFunction classNameGenerator = item -> null;
    private final ComboBoxRenderManager renderManager;
    private final ComboBoxDataController dataController;
    private int customValueListenersCount;

    private ComboBoxBaseI18n i18n;

    private Validator defaultValidator = (value, context) -> {
        boolean fromComponent = context == null;

        // Do the required check only if the validator is called from the
        // component, and not from Binder. Binder has its own implementation
        // of required validation.
        boolean isRequired = fromComponent && isRequiredIndicatorVisible();
        return ValidationUtil.validateRequiredConstraint(
                getI18nErrorMessage(ComboBoxBaseI18n::getRequiredErrorMessage),
                isRequired, getValue(), getEmptyValue());
    };

    private ValidationController, TValue> validationController = new ValidationController<>(
            this);

    /**
     * Constructs a new ComboBoxBase instance
     *
     * @param valuePropertyName
     *            name of the value property of the web component that should be
     *            used to set values, or listen to value changes
     * @param defaultValue
     *            the default value of the component
     * @param valuePropertyType
     *            the class that represents the type of the raw value of the
     *            Flow element property
     * @param presentationToModel
     *            a function to convert a raw property value into a value using
     *            the user-specified model type
     * @param modelToPresentation
     *            a function to convert a value using the user-specified model
     *            type into a raw property value
     * @param 
     *            the type of the raw value of the Flow element property
     */
    public  ComboBoxBase(String valuePropertyName,
            TValue defaultValue, Class valuePropertyType,
            SerializableBiFunction presentationToModel,
            SerializableBiFunction modelToPresentation) {
        super(valuePropertyName, defaultValue, valuePropertyType,
                presentationToModel, modelToPresentation);

        // Extracted as implementation to fix serialization issue:
        // https://github.com/vaadin/flow-components/issues/4420
        // Do not replace with method reference
        SerializableSupplier localeSupplier = new SerializableSupplier() {
            @Override
            public Locale get() {
                return ComboBoxBase.this.getLocale();
            }
        };

        renderManager = new ComboBoxRenderManager<>(this);
        dataController = new ComboBoxDataController<>(this, localeSupplier);
        dataController.getDataGenerator().addDataGenerator((item,
                jsonObject) -> jsonObject.put("label", generateLabel(item)));
        dataController.getDataGenerator()
                .addDataGenerator((item, jsonObject) -> jsonObject
                        .put("className", generateClassName(item)));

        // Configure web component to use key property from the generated
        // wrapper items for identification
        getElement().setProperty("itemValuePath", "key");
        getElement().setProperty("itemIdPath", "key");

        // Notify data communicator when selection changes, which allows to
        // free up items / keys in the KeyMapper that are not used anymore in
        // the selection
        addValueChangeListener(
                e -> getDataCommunicator().notifySelectionChanged());

        addValueChangeListener(e -> validate());
    }

    /**
     * Whether the component should automatically receive focus when the page
     * loads.
     *
     * @return {@code true} if the component should automatically receive focus
     */
    public boolean isAutofocus() {
        return getElement().getProperty("autofocus", false);
    }

    /**
     * Sets the whether the component should automatically receive focus when
     * the page loads. Defaults to {@code false}.
     *
     * @param autofocus
     *            {@code true} component should automatically receive focus
     */
    public void setAutofocus(boolean autofocus) {
        getElement().setProperty("autofocus", autofocus);
    }

    /**
     * Gets the page size, which is the number of items fetched at a time from
     * the data provider.
     * 

* The page size is also the largest number of items that can support * client-side filtering. If you provide more items than the page size, the * component has to fall back to server-side filtering. *

* The default page size is 50. * * @return the maximum number of items sent per request * @see #setPageSize(int) */ public int getPageSize() { return getElement().getProperty("pageSize", 50); } /** * Sets the page size, which is the number of items requested at a time from * the data provider. This does not guarantee a maximum query size to the * backend; when the overlay has room to render more new items than the page * size, multiple "pages" will be requested at once. *

* The page size is also the largest number of items that can support * client-side filtering. If you provide more items than the page size, the * component has to fall back to server-side filtering. *

* Setting the page size after the ComboBox has been rendered effectively * resets the component, and the current page(s) and sent over again. *

* The default page size is 50. * * @param pageSize * the maximum number of items sent per request, should be * greater than zero */ public void setPageSize(int pageSize) { if (pageSize < 1) { throw new IllegalArgumentException( "Page size should be greater than zero."); } getElement().setProperty("pageSize", pageSize); dataController.setPageSize(pageSize); } /** * Whether the dropdown is opened or not. * * @return {@code true} if the drop-down is opened, {@code false} otherwise */ @Synchronize(property = "opened", value = "opened-changed") public boolean isOpened() { return getElement().getProperty("opened", false); } /** * Sets whether the dropdown should be opened or not. * * @param opened * {@code true} to open the drop-down, {@code false} to close it */ public void setOpened(boolean opened) { getElement().setProperty("opened", opened); } /** * If {@code true}, the user can input string values that do not match to * any existing item labels, which will fire a {@link CustomValueSetEvent}. * * @return {@code true} if the component fires custom value set events, * {@code false} otherwise * @see #setAllowCustomValue(boolean) * @see #addCustomValueSetListener(ComponentEventListener) */ public boolean isAllowCustomValue() { return getElement().getProperty("allowCustomValue", false); } /** * Enables or disables the component firing events for custom string input. *

* When enabled, a {@link CustomValueSetEvent} will be fired when the user * inputs a string value that does not match any existing items and commits * it eg. by blurring or pressing the enter-key. *

* Note that ComboBox doesn't do anything with the custom value string * automatically. Use the * {@link #addCustomValueSetListener(ComponentEventListener)} method to * determine how the custom value should be handled. For example, when the * ComboBox has {@code String} as the value type, you can add a listener * which sets the custom string as the value of the ComboBox with * {@link #setValue(Object)}. *

* Setting to {@code true} also allows an unfocused ComboBox to display a * string that doesn't match any of its items nor its current value, unless * this is explicitly handled with * {@link #addCustomValueSetListener(ComponentEventListener)}. When set to * {@code false}, an unfocused ComboBox will always display the label of the * currently selected item. * * @param allowCustomValue * {@code true} to enable custom value set events, {@code false} * to disable them * @see #addCustomValueSetListener(ComponentEventListener) */ public void setAllowCustomValue(boolean allowCustomValue) { getElement().setProperty("allowCustomValue", allowCustomValue); } /** * Filtering string the user has typed into the input field. * * @return the filter string */ @Synchronize(property = "filter", value = "filter-changed") protected String getFilter() { return getElement().getProperty("filter"); } /** * Sets the filter string for the filter input. *

* Setter is only required to allow using @Synchronize * * @param filter * the String value to set */ protected void setFilter(String filter) { getElement().setProperty("filter", filter == null ? "" : filter); } /** * Sets whether the user is required to provide a value. When required, an * indicator appears next to the label and the field invalidates if the * value is cleared. *

* NOTE: The required indicator is only visible when the field has a label, * see {@link #setLabel(String)}. * * @param required * {@code true} to make the field required, {@code false} * otherwise */ @Override public void setRequiredIndicatorVisible(boolean required) { super.setRequiredIndicatorVisible(required); } /** * Gets whether the user is required to provide a value. * * @return {@code true} if the field is required, {@code false} otherwise * @see #setRequiredIndicatorVisible(boolean) */ @Override public boolean isRequiredIndicatorVisible() { return super.isRequiredIndicatorVisible(); } /** * Alias for {@link #isRequiredIndicatorVisible()} * * @return {@code true} if the field is required, {@code false} otherwise */ public boolean isRequired() { return isRequiredIndicatorVisible(); } /** * Alias for {@link #setRequiredIndicatorVisible(boolean)}. * * @param required * {@code true} to make the field required, {@code false} * otherwise */ public void setRequired(boolean required) { setRequiredIndicatorVisible(required); } @Override public void setAriaLabel(String ariaLabel) { getElement().setProperty("accessibleName", ariaLabel); } @Override public Optional getAriaLabel() { return Optional.ofNullable(getElement().getProperty("accessibleName")); } @Override public void setAriaLabelledBy(String labelledBy) { getElement().setProperty("accessibleNameRef", labelledBy); } @Override public Optional getAriaLabelledBy() { return Optional .ofNullable(getElement().getProperty("accessibleNameRef")); } /** * Sets the item label generator that is used to produce the strings shown * in the combo box for each item. By default, * {@link String#valueOf(Object)} is used. *

* When the {@link #setRenderer(Renderer)} is used, the ItemLabelGenerator * is only used to show the selected item label. * * @param itemLabelGenerator * the item label provider to use, not null */ public void setItemLabelGenerator( ItemLabelGenerator itemLabelGenerator) { Objects.requireNonNull(itemLabelGenerator, "The item label generator can not be null"); this.itemLabelGenerator = itemLabelGenerator; dataController.reset(); if (getValue() != null) { refreshValue(); } } /** * Gets the item label generator that is used to produce the strings shown * in the combo box for each item. * * @return the item label generator used, not null */ public ItemLabelGenerator getItemLabelGenerator() { return itemLabelGenerator; } /** * Generates a string label for a data item using the current item label * generator * * @param item * the data item * @return string label for the data item */ protected String generateLabel(TItem item) { if (item == null) { return ""; } String label = getItemLabelGenerator().apply(item); if (label == null) { throw new IllegalStateException(String.format( "Got 'null' as a label value for the item '%s'. " + "'%s' instance may not return 'null' values", item, ItemLabelGenerator.class.getSimpleName())); } return label; } /** * Sets the function that is used for generating CSS class names for the * dropdown items in the ComboBox. Returning {@code null} from the generator * results in no custom class name being set. Multiple class names can be * returned from the generator as space-separated. * * @since 24.5 * @param classNameGenerator * the class name generator to set, not {@code null} * @throws NullPointerException * if {@code classNameGenerator} is {@code null} */ public void setClassNameGenerator( SerializableFunction classNameGenerator) { Objects.requireNonNull(classNameGenerator, "Class name generator can not be null"); this.classNameGenerator = classNameGenerator; dataController.reset(); } /** * Gets the item class name generator that is used for generating CSS class * names for the dropdown items in the ComboBox. * * @since 24.5 * @return the item class name generator, not null */ public SerializableFunction getItemClassNameGenerator() { return classNameGenerator; } /** * Generates a string class name for a data item using the current item * class name generator * * @param item * the data item * @return string class name for the data item */ protected String generateClassName(TItem item) { if (item == null) { return ""; } String label = getItemClassNameGenerator().apply(item); if (label == null) { return ""; } return label; } /** * Sets the Renderer responsible to render the individual items in the list * of possible choices of the ComboBox. It doesn't affect how the selected * item is rendered - that can be configured by using * {@link #setItemLabelGenerator(ItemLabelGenerator)}. * * @param renderer * a renderer for the items in the selection list of the * ComboBox, not null *

* Note that filtering of the ComboBox is not affected by the * renderer that is set here. Filtering is done on the original * values and can be affected by * {@link #setItemLabelGenerator(ItemLabelGenerator)}. */ public void setRenderer(Renderer renderer) { Objects.requireNonNull(renderer, "The renderer must not be null"); renderManager.setRenderer(renderer); } @Override public void setValue(TValue value) { if (getDataCommunicator() == null || getDataProvider() instanceof DataCommunicator.EmptyDataProvider) { if (valueEquals(value, getEmptyValue())) { return; } else { throw new IllegalStateException( "Cannot set a value for a ComboBox without items. " + "Use setItems to populate items into the " + "ComboBox before setting a value."); } } super.setValue(value); refreshValue(); } @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); initConnector(); dataController.onAttach(); ClientValidationUtil.preventWebComponentFromModifyingInvalidState(this); } @Override protected void onDetach(DetachEvent detachEvent) { dataController.onDetach(); super.onDetach(detachEvent); } /** * Adds a listener for the event which is fired when user inputs a string * value that does not match any existing items and commits it eg. by * blurring or pressing the enter-key. *

* Note that ComboBox doesn't do anything with the custom value string * automatically. Use this method to determine how the custom value should * be handled. For example, when the ComboBox has {@code String} as the * value type, you can add a listener which sets the custom string as the * value of the ComboBox with {@link #setValue(Object)}. *

* As a side effect, this makes the ComboBox allow custom values. If you * want to disable the firing of custom value set events once the listener * is added, please disable it explicitly via the * {@link #setAllowCustomValue(boolean)} method. *

* The custom value becomes disallowed automatically once the last custom * value set listener is removed. * * @param listener * the listener to be notified when a new value is filled * @return a {@link Registration} for removing the event listener * @see #setAllowCustomValue(boolean) */ public Registration addCustomValueSetListener( ComponentEventListener> listener) { setAllowCustomValue(true); customValueListenersCount++; Registration registration = addInternalCustomValueSetListener(listener); return new CustomValueRegistration(registration); } /** * Adds a custom value event listener to the component. Can be used * internally to register a listener without also enabling allowing custom * values, which is a side-effect of * {@link #addCustomValueSetListener(ComponentEventListener)} */ @SuppressWarnings({ "rawtypes", "unchecked" }) private Registration addInternalCustomValueSetListener( ComponentEventListener> listener) { return addListener(CustomValueSetEvent.class, (ComponentEventListener) listener); } // **************************************************** // List data view implementation // **************************************************** /** * Gets the list data view for the ComboBox. This data view should only be * used when the items are in-memory set with: *

    *
  • {@link #setItems(Collection)}
  • *
  • {@link #setItems(Object[])}
  • *
  • {@link #setItems(ListDataProvider)}
  • *
  • {@link #setItems(ComboBox.ItemFilter, ListDataProvider)}
  • *
  • {@link #setItems(ComboBox.ItemFilter, Object[])}
  • *
  • {@link #setItems(ComboBox.ItemFilter, Collection)}
  • *
* If the items are not in-memory an exception is thrown. When the items are * fetched lazily, use {@link #getLazyDataView()} instead. * * @return the list data view that provides access to the items in the * ComboBox */ @Override public ComboBoxListDataView getListDataView() { return dataController.getListDataView(); } /** * {@inheritDoc} *

* Filtering will use a case insensitive match to show all items where the * filter text is a substring of the label displayed for that item, which * you can configure with * {@link #setItemLabelGenerator(ItemLabelGenerator)}. *

* Filtering will be handled in the client-side if the size of the data set * is less than the page size. To force client-side filtering with a larger * data set (at the cost of increased network traffic), you can increase the * page size with {@link #setPageSize(int)}. *

* Setting the items resets the combo box's value to {@code null}. */ @Override public ComboBoxListDataView setItems(Collection items) { return dataController.setItems(items); } /** * Sets the data items of this combo box and a filtering function for * defining which items are displayed when user types into the combo box. *

* Note that defining a custom filter will force the component to make * server roundtrips to handle the filtering. Otherwise it can handle * filtering in the client-side, if the size of the data set is less than * the {@link #setPageSize(int) pageSize}. *

* Setting the items resets the combo box's value to {@code null}. *

* The returned data view object can be used for further access to combo box * items, or later on fetched with {@link #getListDataView()}. For using * lazy data loading, use one of the {@code setItems} methods which take a * fetch callback parameter instead. * * @param itemFilter * filter to check if an item is shown when user typed some text * into the ComboBox * @param items * the data items to display * @return the in-memory data view instance that provides access to the data * bound to the combo box */ public ComboBoxListDataView setItems( ComboBox.ItemFilter itemFilter, Collection items) { return dataController.setItems(itemFilter, items); } /** * Sets the data items of this combo box and a filtering function for * defining which items are displayed when user types into the combo box. *

* Note that defining a custom filter will force the component to make * server roundtrips to handle the filtering. Otherwise it can handle * filtering in the client-side, if the size of the data set is less than * the {@link #setPageSize(int) pageSize}. *

* Setting the items resets the combo box's value to {@code null}. *

* The returned data view object can be used for further access to combo box * items, or later on fetched with {@link #getListDataView()}. For using * lazy data loading, use one of the {@code setItems} methods which take a * fetch callback parameter instead. * * @param itemFilter * filter to check if an item is shown when user typed some text * into the ComboBox * @param items * the data items to display * @return the in-memory data view instance that provides access to the data * bound to the combo box */ @SuppressWarnings("unchecked") public ComboBoxListDataView setItems( ComboBox.ItemFilter itemFilter, TItem... items) { return dataController.setItems(itemFilter, items); } /** * Sets a ListDataProvider for this combo box and a filtering function for * defining which items are displayed when user types into the combo box. *

* Note that defining a custom filter will force the component to make * server roundtrips to handle the filtering. Otherwise it can handle * filtering in the client-side, if the size of the data set is less than * the {@link #setPageSize(int) pageSize}. *

* Setting the items resets the combo box's value to {@code null}. *

* The returned data view object can be used for further access to combo box * items, or later on fetched with {@link #getListDataView()}. For using * lazy data loading, use one of the {@code setItems} methods which take a * fetch callback parameter instead. * * @param itemFilter * filter to check if an item is shown when user typed some text * into the ComboBox. * @param listDataProvider * ListDataProvider providing items to the component. * @return the in-memory data view instance that provides access to the data * bound to the combo box */ public ComboBoxListDataView setItems( ComboBox.ItemFilter itemFilter, ListDataProvider listDataProvider) { return dataController.setItems(itemFilter, listDataProvider); } @Override public ComboBoxListDataView setItems( ListDataProvider dataProvider) { return dataController.setItems(dataProvider); } // **************************************************** // Lazy data view implementation // **************************************************** /** * Gets the lazy data view for the ComboBox. This data view should only be * used when the items are provided lazily from the backend with: *

    *
  • {@link #setItems(CallbackDataProvider.FetchCallback)}
  • *
  • {@link #setItemsWithFilterConverter(CallbackDataProvider.FetchCallback, SerializableFunction)}
  • *
  • {@link #setItems(CallbackDataProvider.FetchCallback, CallbackDataProvider.CountCallback)}
  • *
  • {@link #setItemsWithFilterConverter(CallbackDataProvider.FetchCallback, CallbackDataProvider.CountCallback, SerializableFunction)} *
  • *
  • {@link #setItems(BackEndDataProvider)}
  • *
* If the items are not fetched lazily an exception is thrown. When the * items are in-memory, use {@link #getListDataView()} instead. * * @return the lazy data view that provides access to the data bound to the * ComboBox * @throws IllegalStateException * if no items fetch callback(s) set */ @Override public ComboBoxLazyDataView getLazyDataView() { return dataController.getLazyDataView(); } /** * Supply items lazily with a callback from a backend, using custom filter * type. The combo box will automatically fetch more items and adjust its * size until the backend runs out of items. Usage example: *

* {@code comboBox.setItemsWithFilterConverter( * query -> orderService.getOrdersByCount(query.getFilter(), * query.getOffset, * query.getLimit()), * orderCountStr -> Integer.parseInt(orderCountStr));} Note: Validations for * orderCountStr are omitted for briefness. *

* Combo box's client-side filter typed by the user is transformed into a * callback's filter through the given filter converter. *

* The returned data view object can be used for further configuration, or * later on fetched with {@link #getLazyDataView()}. For using in-memory * data, like {@link java.util.Collection}, use * {@link #setItems(Collection)} instead. * * @param fetchCallback * function that returns a stream of items from the backend based * on the offset, limit and a object filter * @param filterConverter * a function which converts a combo box's filter-string typed by * the user into a callback's object filter * @param * filter type used by a callback * @return ComboBoxLazyDataView instance for further configuration */ public ComboBoxLazyDataView setItemsWithFilterConverter( CallbackDataProvider.FetchCallback fetchCallback, SerializableFunction filterConverter) { return dataController.setItemsWithFilterConverter(fetchCallback, filterConverter); } /** * Supply items lazily with callbacks: the first one fetches the items based * on offset, limit and an optional filter, the second provides the exact * count of items in the backend. Use this only in case getting the count is * cheap and the user benefits from the component showing immediately the * exact size. Usage example: *

* {@code comboBox.setItemsWithFilterConverter( * query -> orderService.getOrdersByCount(query.getFilter(), * query.getOffset, * query.getLimit()), * query -> orderService.getSize(query.getFilter()), * orderCountStr -> Integer.parseInt(orderCountStr));} Note: Validations for * orderCountStr are omitted for briefness. *

* Combo box's client-side filter typed by the user is transformed into a * custom filter type through the given filter converter. *

* The returned data view object can be used for further configuration, or * later on fetched with {@link #getLazyDataView()}. For using in-memory * data, like {@link java.util.Collection}, use * {@link #setItems(Collection)} instead. * * @param fetchCallback * function that returns a stream of items from the backend based * on the offset, limit and a object filter * @param countCallback * function that return the number of items in the back end for a * query * @param filterConverter * a function which converts a combo box's filter-string typed by * the user into a callback's object filter * @param * filter type used by a callbacks * @return ComboBoxLazyDataView instance for further configuration */ public ComboBoxLazyDataView setItemsWithFilterConverter( CallbackDataProvider.FetchCallback fetchCallback, CallbackDataProvider.CountCallback countCallback, SerializableFunction filterConverter) { return dataController.setItemsWithFilterConverter(fetchCallback, countCallback, filterConverter); } /** * Supply items lazily with a callback from a backend. The ComboBox will * automatically fetch more items and adjust its size until the backend runs * out of items. Usage example without component provided filter: *

* {@code comboBox.setItems(query -> * orderService.getOrders(query.getOffset(), query.getLimit());} *

* Since ComboBox supports filtering, it can be fetched via * query.getFilter(): *

* {@code comboBox.setItems(query -> * orderService.getOrders(query.getFilter(), query.getOffset(), * query.getLimit());} *

* The returned data view object can be used for further configuration, or * later on fetched with {@link #getLazyDataView()}. For using in-memory * data, like {@link Collection}, use * {@link HasListDataView#setItems(Collection)} instead. *

* If item filtering by some value type other than String is preferred and * backend service is able to fetch and filter items by such type, converter * for client side's filter string can be set along with fetch callback. * See: * {@link #setItemsWithFilterConverter(CallbackDataProvider.FetchCallback, SerializableFunction)} * * @param fetchCallback * function that returns a stream of items from the backend based * on the offset, limit and an optional filter provided by the * query object * @return ComboBoxLazyDataView instance for further configuration */ @Override public ComboBoxLazyDataView setItems( CallbackDataProvider.FetchCallback fetchCallback) { return HasLazyDataView.super.setItems(fetchCallback); } /** * Supply items lazily with callbacks: the first one fetches the items based * on offset, limit and an optional filter, the second provides the exact * count of items in the backend. Use this only in case getting the count is * cheap and the user benefits from the ComboBox showing immediately the * exact size. Usage example without component provided filter: *

* {@code comboBox.setItems( * query -> orderService.getOrders(query.getOffset, query.getLimit()), * query -> orderService.getSize());} *

* Since ComboBox supports filtering, it can be fetched via * query.getFilter(): *

* {@code comboBox.setItems( * query -> orderService.getOrders(query.getFilter(), query.getOffset, * query.getLimit()), * query -> orderService.getSize(query.getFilter()));} *

* The returned data view object can be used for further configuration, or * later on fetched with {@link #getLazyDataView()}. For using in-memory * data, like {@link Collection}, use * {@link HasListDataView#setItems(Collection)} instead. *

* If item filtering by some value type other than String is preferred and * backend service is able to fetch and filter items by such type, converter * for client side's filter string can be set along with fetch callback. * See: * {@link #setItemsWithFilterConverter(CallbackDataProvider.FetchCallback, CallbackDataProvider.CountCallback, SerializableFunction)} * * @param fetchCallback * function that returns a stream of items from the back end for * a query * @param countCallback * function that return the number of items in the back end for a * query * @return ComboBoxLazyDataView instance for further configuration */ @Override public ComboBoxLazyDataView setItems( CallbackDataProvider.FetchCallback fetchCallback, CallbackDataProvider.CountCallback countCallback) { return HasLazyDataView.super.setItems(fetchCallback, countCallback); } @Override public ComboBoxLazyDataView setItems( BackEndDataProvider dataProvider) { return dataController.setItems(dataProvider); } // **************************************************** // Generic data view implementation // **************************************************** /** * Gets the generic data view for the ComboBox. This data view can be used * when {@link #getListDataView()} or {@link #getLazyDataView()} are not * applicable for the underlying data provider, or you don't want to * distinct between which type of data view to use. * * @return the generic {@link DataView} implementation for ComboBox * @see #getListDataView() * @see #getLazyDataView() */ @Override public ComboBoxDataView getGenericDataView() { return dataController.getGenericDataView(); } @Override public ComboBoxDataView setItems( DataProvider dataProvider) { return dataController.setItems(dataProvider); } /** * The method is not supported for the {@link ComboBox} component, use * another overloaded method with filter converter * {@link #setItems(InMemoryDataProvider, SerializableFunction)} *

* Always throws an {@link UnsupportedOperationException}. * * @throws UnsupportedOperationException * when using this method with an {@link InMemoryDataProvider} * @see #setItems(InMemoryDataProvider, SerializableFunction) * @deprecated does not work so don't use */ @Deprecated @Override public ComboBoxDataView setItems( InMemoryDataProvider dataProvider) { throw new UnsupportedOperationException( String.format("ComboBox does not support " + "setting a custom in-memory data provider without " + "knowledge of the rules on how to convert internal text filter " + "into a predicate applied to the data provider. Please use%n" + "setItems(InMemoryDataProvider, SerializableFunction>)" + "%noverloaded method instead")); } /** * Sets an in-memory data provider for the combo box to use, taking into * account both in-memory filtering from data provider and combo box's text * filter. *

* Text filter is transformed into a predicate filter through the given * filter converter. Example of filter converter which produces the Person's * name predicate: * {@code (String nameFilter) -> person -> person.getName().equalsIgnoreCase * (nameFilter);} *

* Filtering will be handled in the client-side if the size of the data set * is less than the page size. To force client-side filtering with a larger * data set (at the cost of increased network traffic), you can increase the * page size with {@link #setPageSize(int)}. *

* Note! Using a {@link ListDataProvider} instead of a * {@link InMemoryDataProvider} is recommended to get access to * {@link ListDataView} API by using {@link #setItems(ListDataProvider)}. * * @param inMemoryDataProvider * InMemoryDataProvider to use, not null * @param filterConverter * a function which converts a component's internal filter into a * predicate applied to the data provider * @return DataView providing information on the data */ public ComboBoxDataView setItems( InMemoryDataProvider inMemoryDataProvider, SerializableFunction> filterConverter) { return dataController.setItems(inMemoryDataProvider, filterConverter); } // **************************************************** // Data provider implementation // **************************************************** /** * Sets a generic data provider for the ComboBox to use. *

* ComboBox triggers filtering queries based on the strings users type into * the field. For this reason you need to provide the second parameter, a * function which converts the filter-string typed by the user into * filter-type used by your data provider. If your data provider already * supports String as the filter-type, it can be used without a converter * function via {@link #setItems(DataProvider)}. *

* Using this method provides the same result as using a data provider * wrapped with * {@link DataProvider#withConvertedFilter(SerializableFunction)}. *

* Changing the combo box's data provider resets its current value to * {@code null}. *

* Use this method when none of the {@code setItems} methods are applicable, * e.g. when having a data provider with filter that cannot be transformed * to {@code DataProvider}. */ public void setDataProvider(DataProvider dataProvider, SerializableFunction filterConverter) { dataController.setDataProvider(dataProvider, filterConverter); } /** * Sets a CallbackDataProvider using the given fetch items callback and a * size callback. *

* This method is a shorthand for making a {@link CallbackDataProvider} that * handles a partial {@link com.vaadin.flow.data.provider.Query Query} * object. *

* Changing the combo box's data provider resets its current value to * {@code null}. * * @param fetchItems * a callback for fetching items, not null * @param sizeCallback * a callback for getting the count of items, not * null * @see CallbackDataProvider */ public void setDataProvider(ComboBox.FetchItemsCallback fetchItems, SerializableFunction sizeCallback) { dataController.setDataProvider(fetchItems, sizeCallback); } /** * Gets the data provider used by this ComboBox. *

* To get information and control over the items in the ComboBox, use either * {@link #getListDataView()} or {@link #getLazyDataView()} instead. * * @return the data provider used by this ComboBox */ public DataProvider getDataProvider() { return dataController.getDataProvider(); } /** * Whether the item is currently selected in the combo box. * * @param item * the item to check * @return {@code true} if the item is selected, {@code false} otherwise */ protected abstract boolean isSelected(TItem item); /** * Refresh value / selection of the web component after changes that might * affect the presentation / rendering of items */ protected abstract void refreshValue(); /** * Accesses the render manager that is managing the custom renderer * * @return the render manager */ protected ComboBoxRenderManager getRenderManager() { return renderManager; } /** * Accesses the data controller that is managing data communication with the * web component *

* Can be null if the constructor has not run yet * * @return the data controller */ protected ComboBoxDataController getDataController() { return dataController; } /** * Accesses the data communicator that is managed by the data controller *

* Can be null if the no data source has been set yet, or if the constructor * has not run yet * * @return the data communicator */ protected ComboBoxDataCommunicator getDataCommunicator() { return dataController != null ? dataController.getDataCommunicator() : null; } /** * Accesses the data generator that is managed by the data controller *

* Can be null if the constructor has not run yet * * @return the data generator */ protected CompositeDataGenerator getDataGenerator() { return dataController != null ? dataController.getDataGenerator() : null; } /** * Accesses the key mapper that is managed by the data controller *

* Can be null if the no data source has been set yet, or if the constructor * has not run yet * * @return the key mapper */ protected DataKeyMapper getKeyMapper() { return getDataCommunicator() != null ? getDataCommunicator().getKeyMapper() : null; } /** * Called by the client-side connector, delegates to data controller * * @param id * the update identifier */ @ClientCallable private void confirmUpdate(int id) { dataController.confirmUpdate(id); } /** * Called by the client-side connector, delegates to data controller */ @ClientCallable private void setRequestedRange(int start, int length, String filter) { dataController.setRequestedRange(start, length, filter); } /** * Called by the client-side connector, delegates to data controller */ @ClientCallable private void resetDataCommunicator() { dataController.resetDataCommunicator(); } /** * Helper for running a command in the before client response hook * * @param command * the command to execute */ protected void runBeforeClientResponse(SerializableConsumer command) { getElement().getNode().runWhenAttached(ui -> ui .beforeClientResponse(this, context -> command.accept(ui))); } private void initConnector() { getElement().executeJs( "window.Vaadin.Flow.comboBoxConnector.initLazy(this)"); } @Override public void setManualValidation(boolean enabled) { validationController.setManualValidation(enabled); } @Override public Validator getDefaultValidator() { return defaultValidator; } /** * Validates the current value against the constraints and sets the * {@code invalid} property and the {@code errorMessage} property based on * the result. If a custom error message is provided with * {@link #setErrorMessage(String)}, it is used. Otherwise, the error * message defined in the i18n object is used. *

* The method does nothing if the manual validation mode is enabled. */ protected void validate() { validationController.validate(getValue()); } /** * Event that is dispatched from a combo box component, if the component * allows setting custom values, and the user has entered a non-empty value * that does not match any of the existing items * * @param * The specific combo box component type */ @DomEvent("custom-value-set") public static class CustomValueSetEvent> extends ComponentEvent { private final String detail; public CustomValueSetEvent(TComponent source, boolean fromClient, @EventData("event.detail") String detail) { super(source, fromClient); this.detail = detail; } public String getDetail() { return detail; } } /** * Gets the internationalization object previously set for this component. *

* NOTE: Updating the instance that is returned from this method will not * update the component if not set again using * {@link #setI18n(ComboBoxBaseI18n)} * * @return the i18n object or {@code null} if no i18n object has been set */ protected ComboBoxBaseI18n getI18n() { return i18n; } /** * Sets the internationalization object for this component. * * @param i18n * the i18n object, not {@code null} */ protected void setI18n(ComboBoxBaseI18n i18n) { this.i18n = Objects.requireNonNull(i18n, "The i18n properties object should not be null"); } private String getI18nErrorMessage( Function getter) { return Optional.ofNullable(i18n).map(getter).orElse(""); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy