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

com.vaadin.flow.component.select.Select Maven / Gradle / Ivy

There is a newer version: 24.6.0
Show newest version
/*
 * Copyright 2000-2017 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.select;

import java.io.Serializable;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasComponents;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasValidation;
import com.vaadin.flow.component.ItemLabelGenerator;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.select.generated.GeneratedVaadinSelect;
import com.vaadin.flow.data.binder.HasDataProvider;
import com.vaadin.flow.data.binder.HasItemsAndComponents;
import com.vaadin.flow.data.provider.DataChangeEvent;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.KeyMapper;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.renderer.TextRenderer;
import com.vaadin.flow.data.selection.SingleSelect;
import com.vaadin.flow.dom.PropertyChangeEvent;
import com.vaadin.flow.dom.PropertyChangeListener;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.shared.Registration;

/**
 * A customizable drop-down select component similar to a native browser select.
 * 

* This is a server side Java integration for {@code } web * component. *

* For usage examples, see * the demo * in vaadin.com. * * @param * the type of the items for the select * @author Vaadin Ltd. */ @JavaScript("frontend://selectConnector.js") public class Select extends GeneratedVaadinSelect, T> implements HasDataProvider, HasItemsAndComponents, HasSize, HasValidation, SingleSelect, T> { public static final String LABEL_ATTRIBUTE = "label"; private static T presentationToModel(Select select, String presentation) { if (!select.keyMapper.containsKey(presentation)) { return null; } return select.keyMapper.get(presentation); } private static String modelToPresentation(Select select, T model) { if (model == null) { return ""; } if (!select.keyMapper.has(model)) { return null; } return select.keyMapper.key(model); } private final KeyMapper keyMapper = new KeyMapper<>(); /* * Internal version of list box that is just used to delegate the child * components to. vaadin-select.html imports vaadin-list-box.html. * * Using this internally allows all events and updates to the children * (items, possible child components) to work even though the list box * element is moved on the client side in the renderer method from light-dom * to be a child of the select overlay. * * Not using the proper ListBox because all communication & updates are * going through the Select. Using ListBox would just duplicate things, and * cause e.g. unnecessary synchronizations and dependency to the Java * integration. * * The known side effect is that at the element level, the child components * are not the correct ones, e.g. the list box is the only child of select, * even though that is not visible from the component level. */ @Tag("vaadin-list-box") private class InternalListBox extends Component implements HasItemsAndComponents { @Override public void setItems(Collection collection) { // NOOP, never used directly, just need to have it here throw new UnsupportedOperationException( "The setItems method of the internal ListBox of the Select component should never be called."); } @Override public int getItemPosition(T item) { // null item is the empty selection item and that is always first if (item == null && isEmptySelectionAllowed()) { return 0; } else { return HasItemsAndComponents.super.getItemPosition(item); } } } private final InternalListBox listBox = new InternalListBox(); private DataProvider dataProvider = DataProvider.ofItems(); private ComponentRenderer itemRenderer; private SerializablePredicate itemEnabledProvider = null; private ItemLabelGenerator itemLabelGenerator = null; private final PropertyChangeListener validationListener = this::validateSelectionEnabledState; private Registration validationRegistration; private Registration dataProviderListenerRegistration; private boolean resetPending = true; private boolean emptySelectionAllowed; private String emptySelectionCaption; private VaadinItem emptySelectionItem; /** * Constructs a select. */ public Select() { super(null, null, String.class, Select::presentationToModel, Select::modelToPresentation); getElement().setProperty("invalid", false); getElement().setProperty("opened", false); getElement().appendChild(listBox.getElement()); registerValidation(); } /** * Constructs a select with the given items. * * @param items * the items for the select */ public Select(T... items) { this(); setItems(items); } /** * Returns the item component renderer. * * @return the item renderer or {@code null} if none set * @see #setRenderer(ComponentRenderer) */ public ComponentRenderer getItemRenderer() { return itemRenderer; } /** * Sets the item renderer for this select group. The renderer is applied to * each item to create a component which represents the item option in the * select's drop down. *

* Default is {@code null} which means that the item's {@link #toString()} * method is used and set as the text content of the vaadin item element. * * @param renderer * the item renderer, or {@code null} to clear */ public void setRenderer( ComponentRenderer renderer) { this.itemRenderer = renderer; refreshItems(); } /** * Convenience setter for creating a {@link TextRenderer} from the given * function that converts the item to a string. *

* NOTE: even though this accepts an {@link ItemLabelGenerator}, * this is not the same as * {@link #setItemLabelGenerator(ItemLabelGenerator)} which does a different * thing. * * @param itemLabelGenerator * the function that creates the text content from the item, not * {@code null} */ public void setTextRenderer(ItemLabelGenerator itemLabelGenerator) { Objects.requireNonNull(itemLabelGenerator); setRenderer(new TextRenderer<>(itemLabelGenerator)); } /** * Sets whether the user is allowed to select nothing. When set {@code true} * a special empty item is shown to the user. *

* Default is {@code false}. The empty selection item can be customized with * {@link #setEmptySelectionCaption(String)}. * * @param emptySelectionAllowed * {@code true} to allow not selecting anything, {@code false} to * require selection * @see #setEmptySelectionCaption(String) */ public void setEmptySelectionAllowed(boolean emptySelectionAllowed) { if (isEmptySelectionAllowed() == emptySelectionAllowed) { return; } if (isEmptySelectionAllowed()) { removeEmptySelectionItem(); } else { addEmptySelectionItem(); } this.emptySelectionAllowed = emptySelectionAllowed; } /** * Returns whether the user is allowed to select nothing. * * @return {@code true} if empty selection is allowed, {@code false} * otherwise */ public boolean isEmptySelectionAllowed() { return emptySelectionAllowed; } /** * Sets the empty selection caption when * {@link #setEmptySelectionAllowed(boolean)} has been enabled. The caption * is shown for the empty selection item in the drop down. *

* When the empty selection item is selected, the select shows the value * provided by {@link #setItemLabelGenerator(ItemLabelGenerator)} for the * {@code null} item, or the string set with {@link #setPlaceholder(String)} * or an empty string if not placeholder is set. *

* Default is an empty string "", which will show the place holder when * selected. * * @param emptySelectionCaption * the empty selection caption to set, not {@code null} * @see #setEmptySelectionAllowed(boolean) */ public void setEmptySelectionCaption(String emptySelectionCaption) { Objects.requireNonNull(emptySelectionCaption, "Empty selection caption must not be null"); this.emptySelectionCaption = emptySelectionCaption; if (emptySelectionItem != null) { updateItem(emptySelectionItem); } } public String getEmptySelectionCaption() { return emptySelectionCaption == null ? "" : emptySelectionCaption; } /** * Returns the item enabled predicate. * * @return the item enabled predicate or {@code null} if not set * @see #setItemEnabledProvider */ public SerializablePredicate getItemEnabledProvider() { return itemEnabledProvider; } /** * Sets the item enabled predicate for this select. The predicate is applied * to each item to determine whether the item should be enabled * ({@code true}) or disabled ({@code false}). Disabled items are displayed * as grayed out and the user cannot select them. *

* By default is {@code null} and all the items are enabled. * * @param itemEnabledProvider * the item enable predicate or {@code null} to clear */ public void setItemEnabledProvider( SerializablePredicate itemEnabledProvider) { this.itemEnabledProvider = itemEnabledProvider; refreshItems(); } /** * Gets the item label generator. It generates the text that is shown in the * input part for the item when it has been selected. *

* Default is {@code null}. * * @return the item label generator, {@code null} if not set */ public ItemLabelGenerator getItemLabelGenerator() { return itemLabelGenerator; } /** * Sets the item label generator. It generates the text that is shown in the * input part for the item when it has been selected. *

* Default is {@code null} and the text content generated for the item with * {@link #setRenderer(ComponentRenderer)} is used instead. * * @param itemLabelGenerator * the item label generator to set, or {@code null} to clear */ public void setItemLabelGenerator( ItemLabelGenerator itemLabelGenerator) { this.itemLabelGenerator = itemLabelGenerator; refreshItems(); } /** * Gets the placeholder hint set for the user. * * @return the placeholder or {@code null} if none set */ public String getPlaceholder() { return super.getPlaceholderString(); } /** * Sets the placeholder hint for the user. *

* The placeholder will be displayed in the case that there is no item * selected, or the selected item has an empty string label, or the selected * item has no label and it's DOM content is empty. *

* Default value is {@code null}. * * @param placeholder * the placeholder to set, or {@code null} to remove */ @Override public void setPlaceholder(String placeholder) { super.setPlaceholder(placeholder); } /** * Sets the string for the label element. *

* NOTE: the label must be set for the required indicator to be * visible. * * @param label * string or {@code null} to clear it */ @Override public void setLabel(String label) { super.setLabel(label); } /** * Gets the string for the label element. * * @return the label string, or {@code null} if not set */ public String getLabel() { return super.getLabelString(); } /** * Sets the select to have focus when the page loads. *

* Default is {@code false}. * * @param autofocus * the autofocus to set */ @Override public void setAutofocus(boolean autofocus) { super.setAutofocus(autofocus); } /** * Gets whether this select has been set to autofocus when the page loads. * * @return {@code true} if set to autofocus, {@code false} if not */ public boolean isAutofocus() { return super.isAutofocusBoolean(); } @Override public void setDataProvider(DataProvider dataProvider) { this.dataProvider = dataProvider; reset(); if (dataProviderListenerRegistration != null) { dataProviderListenerRegistration.remove(); } dataProviderListenerRegistration = dataProvider .addDataProviderListener(this::onDataChange); } /** * Gets the data provider. * * @return the data provider, not {@code null} */ public DataProvider getDataProvider() { return dataProvider; } @Override public void onEnabledStateChanged(boolean enabled) { setDisabled(!enabled); getItems().forEach(this::updateItemEnabled); } /** * {@inheritDoc} * * NOTE: The required indicator will not be visible, if the * {@link #setLabel(String)} property is not set for the select. */ @Override public void setRequiredIndicatorVisible(boolean requiredIndicatorVisible) { // this would be the same as setRequired(boolean) but we don't expose // both super.setRequiredIndicatorVisible(requiredIndicatorVisible); } /** * {@inheritDoc} * * NOTE: The required indicator will not be visible, if the * {@link #setLabel(String)} property is not set for the select. */ @Override public boolean isRequiredIndicatorVisible() { return super.isRequiredBoolean(); } /** * Sets the error message to show to the user on invalid selection. * * @param errorMessage * the error message or {@code null} to clear it */ @Override public void setErrorMessage(String errorMessage) { super.setErrorMessage(errorMessage); } /** * Gets the error message to show to the user on invalid selection * * @return the error message or {@code null} if not set */ @Override public String getErrorMessage() { return super.getErrorMessageString(); } /** * Sets the select to show as invalid state and display error message. * * @param invalid * {@code true} for invalid, {@code false} for valid */ @Override public void setInvalid(boolean invalid) { super.setInvalid(invalid); } /** * Gets whether the select is currently in invalid state. * * @return {@code true} for invalid, {@code false} for valid */ @Override public boolean isInvalid() { return super.isInvalidBoolean(); } /** * {@inheritDoc} *

* NOTE: If you add a component with the {@code slot} attribute * set, it will be placed in the light-dom of the {@code vaadin-select} * instead of the drop down, similar to {@link #addToPrefix(Component...)} */ @Override public void add(Component... components) { Objects.requireNonNull(components, "Components should not be null"); for (Component component : components) { if (component.getElement().hasAttribute("slot")) { HasItemsAndComponents.super.add(component); } else { listBox.add(component); } } } @Override public void addComponents(T afterItem, Component... components) { listBox.addComponents(afterItem, components); } @Override public void prependComponents(T beforeItem, Component... components) { listBox.prependComponents(beforeItem, components); } /** * {@inheritDoc} *

* NOTE: If you add a component with the {@code slot} attribute * set, it will be placed in the light-dom of the {@code vaadin-select} * instead of the drop down, similar to {@link #addToPrefix(Component...)} */ @Override public void addComponentAtIndex(int index, Component component) { Objects.requireNonNull(component, "Component should not be null"); if (component.getElement().hasAttribute("slot")) { HasItemsAndComponents.super.addComponentAtIndex(index, component); } else { listBox.addComponentAtIndex(index, component); } } /** * {@inheritDoc} *

* NOTE: If you add a component with the {@code slot} attribute * set, it will be placed in the light-dom of the {@code vaadin-select} * instead of the drop down, similar to {@link #addToPrefix(Component...)} */ @Override public void addComponentAsFirst(Component component) { Objects.requireNonNull(component, "Component should not be null"); if (component.getElement().hasAttribute("slot")) { HasItemsAndComponents.super.addComponentAsFirst(component); } else { listBox.addComponentAsFirst(component); } } @Override public void addToPrefix(Component... components) { super.addToPrefix(components); } @Override public Stream getChildren() { // do not provide access to items or list box as touching those will // hurt return Stream.concat( super.getChildren().filter(component -> component != listBox), listBox.getChildren().filter( component -> !(component instanceof VaadinItem))); } /** * Removes the given child components from this component. *

* NOTE: any component with the {@code slot} attribute will be * attempted to removed from the light dom of the vaadin-select, instead of * inside the options drop down. * * @param components * the components to remove * @throws IllegalArgumentException * if any of the components is not a child of this component */ @Override public void remove(Component... components) { Objects.requireNonNull(components, "Components should not be null"); for (Component component : components) { if (component.getElement().hasAttribute("slot")) { super.remove(component); } else { listBox.remove(components); } } } /** * Removes all child components that are not items. To remove all items, * reset the data provider or use {@link #setItems(Object[])}. *

* NOTE: this will remove all non-items from the drop down and any * slotted components from vaadin-select's light dom. * * @see HasComponents#removeAll() */ @Override public void removeAll() { // Only remove list box children that are not vaadin-item since it makes // no sense // to allow removing those, causing the component to be in flux state. // Also do not remove the list box but remove any slotted components // (see add()) getChildren().forEach(this::remove); } @Override protected boolean hasValidValue() { // this is not about whether the value is actually "valid", // this is about whether or not is something that should be committed to // the _value_ of this field. E.g, it might be a value that is // acceptable, // but the component status should still be _invalid_. String selectedKey = getElement().getProperty("value"); T item = keyMapper.get(selectedKey); if (item == null) { return isEmptySelectionAllowed() && isItemEnabled(item); } return isItemEnabled(item); } @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); initConnector(); } private void initConnector() { runBeforeClientResponse(ui -> { ui.getPage().executeJavaScript( "window.Vaadin.Flow.selectConnector.initLazy($0)", getElement()); // connector init will handle first data setting resetPending = false; }); } private boolean isItemEnabled(T item) { return itemEnabledProvider == null || itemEnabledProvider.test(item); } private Component createItem(T bean) { VaadinItem item = new VaadinItem<>(keyMapper.key(bean), bean); updateItem(item); return item; } private void updateItem(VaadinItem vaadinItem) { vaadinItem.removeAll(); T item = vaadinItem.getItem(); if (vaadinItem == emptySelectionItem) { vaadinItem.setText(emptySelectionCaption); } else if (getItemRenderer() == null) { vaadinItem.setText(item.toString()); } else { vaadinItem.add(getItemRenderer().createComponent(item)); } if (getItemLabelGenerator() != null) { vaadinItem.getElement().setAttribute(LABEL_ATTRIBUTE, getItemLabelGenerator().apply(item)); } else if (item == emptySelectionItem) { vaadinItem.getElement().setAttribute(LABEL_ATTRIBUTE, ""); } else { vaadinItem.getElement().removeAttribute(LABEL_ATTRIBUTE); } updateItemEnabled(vaadinItem); // currently the updates for the selected item are not reflected to the // input field automatically // https://github.com/vaadin/vaadin-select/issues/180 if (item == getValue() && !resetPending) { getElement().executeJavaScript("this._updateValueSlot()"); } } private void updateItemEnabled(VaadinItem item) { boolean itemEnabled = isItemEnabled(item.getItem()); boolean disabled = isDisabledBoolean() || !itemEnabled; // The disabled attribute should be set when the item is disabled, // but not if only the select is disabled, because setting disabled // attribute clears the selected value of an item. item.getElement().setEnabled(!disabled); item.getElement().setAttribute("disabled", !itemEnabled); } private void refreshItems() { getItems().forEach(this::updateItem); } @SuppressWarnings("unchecked") private Stream> getItems() { return listBox.getChildren() .filter(component -> component instanceof VaadinItem) .map(child -> (VaadinItem) child); } private void reset() { keyMapper.removeAll(); listBox.removeAll(); clear(); // reset added at this point to avoid unnecessary selected item update if (!resetPending) { resetPending = true; runBeforeClientResponse(ui -> { ui.getPage().executeJavaScript("$0.render();", getElement()); resetPending = false; }); } if (isEmptySelectionAllowed()) { addEmptySelectionItem(); } getDataProvider().fetch(new Query<>()).map(this::createItem) .forEach(this::add); } private void onDataChange(DataChangeEvent event) { if (event instanceof DataChangeEvent.DataRefreshEvent) { T updatedItem = ((DataChangeEvent.DataRefreshEvent) event) .getItem(); Object updatedItemId = getDataProvider().getId(updatedItem); getItems() .filter(vaadinItem -> updatedItemId.equals( getDataProvider().getId(vaadinItem.getItem()))) .findAny().ifPresent(this::updateItem); } else { reset(); } } private T getValue(Serializable key) { if (key == null || "".equals(key)) { return null; } return keyMapper.get(key.toString()); } private void addEmptySelectionItem() { if (emptySelectionItem == null) { emptySelectionItem = new VaadinItem<>("", null); } updateItem(emptySelectionItem); addComponentAsFirst(emptySelectionItem); if (getValue() == null) { setValue(null); } } private void removeEmptySelectionItem() { if (emptySelectionItem != null) { listBox.remove(emptySelectionItem); } emptySelectionItem = null; } private void validateSelectionEnabledState(PropertyChangeEvent event) { if (!event.isUserOriginated()) { return; } if (!hasValidValue() || isReadOnly()) { T oldValue = getValue(event.getOldValue()); // return the value back on the client side try { validationRegistration.remove(); getElement().setProperty("value", keyMapper.key(oldValue)); } finally { registerValidation(); } // Now make sure that the item is still in the correct state Optional> selectedItem = getItems().filter( item -> item.getItem() == getValue(event.getValue())) .findFirst(); selectedItem.ifPresent(this::updateItemEnabled); } } private void registerValidation() { if (validationRegistration != null) { validationRegistration.remove(); } validationRegistration = getElement().addPropertyChangeListener("value", validationListener); } private void runBeforeClientResponse(SerializableConsumer command) { getElement().getNode().runWhenAttached(ui -> ui .beforeClientResponse(this, context -> command.accept(ui))); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy