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

com.vaadin.flow.component.combobox.MultiSelectComboBox 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.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.HasThemeVariant;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.provider.DataCommunicator;
import com.vaadin.flow.data.provider.DataKeyMapper;
import com.vaadin.flow.data.provider.IdentifierProviderChangeEvent;
import com.vaadin.flow.data.selection.MultiSelect;
import com.vaadin.flow.data.selection.MultiSelectionEvent;
import com.vaadin.flow.data.selection.MultiSelectionListener;
import com.vaadin.flow.internal.JsonSerializer;
import com.vaadin.flow.shared.Registration;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import elemental.json.JsonType;

/**
 * MultiSelectComboBox allows the user to select one or more values from a
 * filterable list of options presented in an overlay. Compared to
 * {@link ComboBox}, MultiSelectComboBox allows to select multiple values.
 * 

* MultiSelectComboBox supports lazy loading. This means that when using large * data sets, items are requested from the server one "page" at a time when the * user scrolls down the overlay. The number of items in one page is by default * 50, and can be changed with {@link #setPageSize(int)}. *

* MultiSelectComboBox can do filtering either in the browser or in the server. * When MultiSelectComboBox has only a relatively small set of items, the * filtering will happen in the browser, allowing smooth user-experience. When * the size of the data set is larger than the {@code pageSize}, the * webcomponent doesn't necessarily have all the data available, and it will * make requests to the server to handle the filtering. Also, if you have * defined custom filtering logic, with eg. * {@link #setItems(ComboBox.ItemFilter, Collection)}, filtering will happen in * the server. To enable client-side filtering with larger data sets, you can * override the {@code pageSize} to be bigger than the size of your data set. * However, then the full data set will be sent to the client immediately, and * you will lose the benefits of lazy loading. *

Validation

*

* MultiSelectComboBox comes with a built-in validation mechanism that verifies * that the field is not empty when {@link #setRequiredIndicatorVisible(boolean) * required} is enabled. *

* Validation is triggered whenever the user initiates a value change, for * example by selection from the dropdown or manual entry followed by Enter or * blur. Programmatic value changes trigger validation as well. If validation * fails, the component is marked as invalid and an error message is displayed * below the input. *

* The required error message can be configured using either * {@link MultiSelectComboBoxI18n#setRequiredErrorMessage(String)} or * {@link #setErrorMessage(String)}. *

* For more advanced validation that requires custom rules, you can use * {@link Binder}. Please note that Binder provides its own API for the required * validation, see {@link Binder.BindingBuilder#asRequired(String) * asRequired()}. *

* However, if Binder doesn't fit your needs and you want to implement fully * custom validation logic, you can disable the built-in validation by setting * {@link #setManualValidation(boolean)} to true. This will allow you to control * the invalid state and the error message manually using * {@link #setInvalid(boolean)} and {@link #setErrorMessage(String)} API. * * @param * the type of the items to be selectable from the combo box * @author Vaadin Ltd */ @Tag("vaadin-multi-select-combo-box") @NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.5.0") @JsModule("@vaadin/polymer-legacy-adapter/style-modules.js") @NpmPackage(value = "@vaadin/multi-select-combo-box", version = "24.5.0") @JsModule("@vaadin/multi-select-combo-box/src/vaadin-multi-select-combo-box.js") @JsModule("./flow-component-renderer.js") @JsModule("./comboBoxConnector.js") public class MultiSelectComboBox extends ComboBoxBase, TItem, Set> implements MultiSelect, TItem>, HasThemeVariant { private final MultiSelectComboBoxSelectionModel selectionModel; private AutoExpandMode autoExpand; /** * Default constructor. Creates an empty combo box. */ public MultiSelectComboBox() { this(50); } /** * Creates an empty combo box with the defined page size for lazy loading. *

* The default page size is 50. *

* 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. * * @param pageSize * the amount of items to request at a time for lazy loading * @see #setPageSize(int) */ @SuppressWarnings({ "rawtypes", "unchecked" }) public MultiSelectComboBox(int pageSize) { super("selectedItems", new LinkedHashSet<>(), JsonArray.class, MultiSelectComboBox::presentationToModel, MultiSelectComboBox::modelToPresentation); // Create the selection model that manages the currently selected items. // The model ensures that items are compared based on their data // provider identify, and that the selection only changes if items // actually have a different identity in the data provider. selectionModel = new MultiSelectComboBoxSelectionModel<>( item -> getDataProvider().getId(item)); addValueChangeListener(e -> { // Synchronize selection if value is updated from client if (e.isFromClient()) { selectionModel.setSelectedItems(e.getValue()); } }); // Pass identifier provider to selection model when it is changed // through a data view ComponentEventListener> listener = e -> selectionModel .setIdentityProvider(e.getIdentifierProvider()); ComponentUtil.addListener(this, IdentifierProviderChangeEvent.class, (ComponentEventListener) listener); // Initialize page size and data provider setPageSize(pageSize); setItems(new DataCommunicator.EmptyDataProvider<>()); setAutoExpand(AutoExpandMode.NONE); } /** * Creates an empty combo box with the defined label. * * @param label * the label describing the combo box * @see #setLabel(String) */ public MultiSelectComboBox(String label) { this(); setLabel(label); } /** * Creates a combo box with the defined label and populated with the items * in the collection. * * @param label * the label describing the combo box * @param items * the items to be shown in the list of the combo box * @see #setLabel(String) * @see #setItems(Collection) */ public MultiSelectComboBox(String label, Collection items) { this(); setLabel(label); setItems(items); } /** * Creates a combo box with the defined label and populated with the items * in the array. * * @param label * the label describing the combo box * @param items * the items to be shown in the list of the combo box * @see #setLabel(String) * @see #setItems(Object...) */ @SafeVarargs public MultiSelectComboBox(String label, TItem... items) { this(); setLabel(label); setItems(items); } /** * Constructs a combo box with a value change listener. * * @param listener * the value change listener to add * @see #addValueChangeListener(ValueChangeListener) */ public MultiSelectComboBox( ValueChangeListener, Set>> listener) { this(); addValueChangeListener(listener); } /** * Constructs a combo box with the defined label and a value change * listener. * * @param label * the label describing the combo box * @param listener * the value change listener to add * @see #setLabel(String) * @see #addValueChangeListener(ValueChangeListener) */ public MultiSelectComboBox(String label, ValueChangeListener, Set>> listener) { this(label); addValueChangeListener(listener); } /** * Constructs a combo box with the defined label, a value change listener * and populated with the items in the array. * * @param label * the label describing the combo box * @param listener * the value change listener to add * @param items * the items to be shown in the list of the combo box * @see #setLabel(String) * @see #addValueChangeListener(ValueChangeListener) * @see #setItems(Object...) */ @SafeVarargs public MultiSelectComboBox(String label, ValueChangeListener, Set>> listener, TItem... items) { this(label, listener); setItems(items); } @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); if (getI18n() != null) { updateI18n(); } } private static Set presentationToModel( MultiSelectComboBox multiSelectComboBox, JsonArray presentation) { DataKeyMapper keyMapper = multiSelectComboBox.getKeyMapper(); if (presentation == null || keyMapper == null) { return multiSelectComboBox.getEmptyValue(); } Set set = new LinkedHashSet<>(); for (int i = 0; i < presentation.length(); i++) { String key = presentation.getObject(i).getString("key"); set.add(keyMapper.get(key)); } return set; } private static JsonArray modelToPresentation( MultiSelectComboBox multiSelectComboBox, Set model) { JsonArray array = Json.createArray(); if (model == null || model.isEmpty()) { return array; } model.stream().map(multiSelectComboBox::generateJson) .forEach(jsonObject -> array.set(array.length(), jsonObject)); return array; } private JsonObject generateJson(TItem item) { JsonObject jsonObject = Json.createObject(); jsonObject.put("key", getKeyMapper().key(item)); getDataGenerator().generateData(item, jsonObject); return jsonObject; } /** * 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 * @see MultiSelectComboBoxI18n#setRequiredErrorMessage(String) */ @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(); } /** * Gets the value of the component, which is a set of selected items. *

* The returned set is immutable and can not be modified. Use * {@link #select(Object[])} or {@link #deselect(Object[])} to add or remove * individual items. * * @return an unmodifiable set of selected items */ @Override public Set getValue() { return Collections.unmodifiableSet(super.getValue()); } /** * Sets the value of the component, which is a set of selected items. *

* Note that it is allowed to pass {@code null} as value to clear the * selection, but that an empty set will be stored as value instead. * * @param value * the new value */ @Override public void setValue(Set value) { if (value == null) { value = Collections.emptySet(); } // Update selection first, which returns a boolean indicating whether // the selection (=value) has actually changed boolean hasValueChanged = selectionModel.setSelectedItems(value); if (hasValueChanged) { // Only update field value and generate change event if value has // actually changed super.setValue(value); } } /** * Sets the value of the component, which is a set of selected items. As * each item can only be selected once, duplicates in the provided items * will be removed. Passing no items will result in an empty selection. * * @param items * the new value */ @SafeVarargs public final void setValue(TItem... items) { Set value = new LinkedHashSet<>(List.of(items)); setValue(value); } /** * Sets the value of the component, which is a set of selected items. As * each item can only be selected once, duplicates in the provided items * will be removed. Passing no items will result in an empty selection. * * @param items * the new value */ public void setValue(Collection items) { Set value = new LinkedHashSet<>(items); setValue(value); } @Override protected void refreshValue() { Set value = getValue(); if (value == null || value.isEmpty()) { return; } JsonArray selectedItems = modelToPresentation(this, value); getElement().setPropertyJson("selectedItems", selectedItems); } @Override public boolean isSelected(TItem item) { Objects.requireNonNull(item, "Null can not be selected"); // Override the default MultiSelect.isSelected implementation to use the // selection model, which compares items using the identity provider of // the data provider return selectionModel.isSelected(item); } @Override public Set getSelectedItems() { return Collections.unmodifiableSet(selectionModel.getSelectedItems()); } @Override public Registration addSelectionListener( MultiSelectionListener, TItem> listener) { // Selection is equivalent to the value, so we can reuse the value // change listener here return addValueChangeListener(event -> listener .selectionChange(new MultiSelectionEvent<>(this, this, event.getOldValue(), event.isFromClient()))); } @Override public void updateSelection(Set addedItems, Set removedItems) { // Update the selection, which returns a boolean indicating whether // the selection (=value) has actually changed boolean hasValueChanged = selectionModel.updateSelection(addedItems, removedItems); if (hasValueChanged) { // Only update field value and generate change event if value has // actually changed super.setValue(selectionModel.getSelectedItems()); } } /** * Defines possible behavior of the component when not all selected items * can be displayed as chips within the current field width. */ public enum AutoExpandMode { /** * Field expands vertically and chips wrap. */ VERTICAL(false, true), /** * Field expands horizontally until max-width is reached, then collapses * to overflow chip. */ HORIZONTAL(true, false), /** * Field expands horizontally until max-width is reached, then expands * vertically and chips wrap. */ BOTH(true, true), /** * Field does not expand and collapses to overflow chip. */ NONE(false, false); private final boolean expandHorizontally; private final boolean expandVertically; AutoExpandMode(boolean expandHorizontally, boolean expandVertically) { this.expandHorizontally = expandHorizontally; this.expandVertically = expandVertically; } /** * Gets whether to expand horizontally. * * @return Whether to expand horizontally */ public boolean getExpandHorizontally() { return expandHorizontally; } /** * Gets whether to expand vertically. * * @return Whether to expand vertically */ public boolean getExpandVertically() { return expandVertically; } } /** * Gets the behavior of the component when not all selected items can be * displayed as chips within the current field width. * * @since 24.3 * @return The current {@link AutoExpandMode} */ public AutoExpandMode getAutoExpand() { return autoExpand; } /** * Sets the behavior of the component when not all selected items can be * displayed as chips within the current field width. * * Expansion only works with undefined size in the desired direction (i.e. * setting `max-width` limits the component's width). * * @param {AutoExpandMode} * autoExpandMode * @since 24.3 */ public void setAutoExpand(AutoExpandMode autoExpandMode) { Objects.requireNonNull(autoExpandMode, "The mode to be set cannot be null"); autoExpand = autoExpandMode; getElement().setProperty("autoExpandHorizontally", autoExpandMode.getExpandHorizontally()); getElement().setProperty("autoExpandVertically", autoExpandMode.getExpandVertically()); } /** * Gets whether selected items are grouped at the top of the overlay. * * @since 24.3 * @return {@code true} if enabled, {@code false} otherwise */ public boolean isSelectedItemsOnTop() { return getElement().getProperty("selectedItemsOnTop", false); } /** * Enables or disables grouping of the selected items at the top of the * overlay. * * @since 24.3 * @param selectedItemsOnTop * {@code true} to group selected items at the top */ public void setSelectedItemsOnTop(boolean selectedItemsOnTop) { getElement().setProperty("selectedItemsOnTop", selectedItemsOnTop); } /** * Gets whether the filter is kept after selecting items. {@code false} by * default. * * @since 24.4 * @return {@code true} if enabled, {@code false} otherwise */ public boolean isKeepFilter() { return getElement().getProperty("keepFilter", false); } /** * Enables or disables keeping the filter after selecting items. By default, * the filter is cleared after selecting an item and the overlay shows the * unfiltered list of items again. Enabling this option will keep the * filter, which allows to select multiple filtered items in succession. * * @param keepFilter * whether to keep the filter after selecting an item */ public void setKeepFilter(boolean keepFilter) { getElement().setProperty("keepFilter", keepFilter); } /** * 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(MultiSelectComboBoxI18n)} * * @return the i18n object or {@code null} if no i18n object has been set */ public MultiSelectComboBoxI18n getI18n() { return (MultiSelectComboBoxI18n) super.getI18n(); } /** * Sets the internationalization object for this component. * * @param i18n * the i18n object, not {@code null} */ public void setI18n(MultiSelectComboBoxI18n i18n) { super.setI18n(i18n); updateI18n(); } /** * Update I18N settings in the web component. Merges the * {@link MultiSelectComboBoxI18n} settings with the current / default * settings of the web component. */ private void updateI18n() { JsonObject i18nJson = (JsonObject) JsonSerializer.toJson(getI18n()); // Remove null values so that we don't overwrite existing WC // translations with empty ones removeNullValuesFromJsonObject(i18nJson); // Remove the error message properties because they aren't used on // the client-side. i18nJson.remove("requiredErrorMessage"); // Assign new I18N object to WC, by merging the existing // WC I18N, and the values from the new I18n instance, // into an empty object getElement().executeJs("this.i18n = Object.assign({}, this.i18n, $0);", i18nJson); } private void removeNullValuesFromJsonObject(JsonObject jsonObject) { for (String key : jsonObject.keys()) { if (jsonObject.get(key).getType() == JsonType.NULL) { jsonObject.remove(key); } } } /** * Sets the dropdown overlay width. * * @param width * the new dropdown width. Pass in null to set the dropdown width * back to the default value. */ public void setOverlayWidth(String width) { getStyle().set("--vaadin-multi-select-combo-box-overlay-width", width); } /** * Sets the dropdown overlay width. Negative number implies unspecified size * (the dropdown width is reverted back to the default value). * * @param width * the width of the dropdown. * @param unit * the unit used for the dropdown. */ public void setOverlayWidth(float width, Unit unit) { Objects.requireNonNull(unit, "Unit can not be null"); setOverlayWidth(HasSize.getCssSize(width, unit)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy