com.vaadin.ui.AbstractSingleSelect Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.ui;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.jsoup.nodes.Element;
import com.vaadin.data.HasValue;
import com.vaadin.data.SelectionModel.Single;
import com.vaadin.data.provider.DataCommunicator;
import com.vaadin.data.provider.DataGenerator;
import com.vaadin.event.selection.SingleSelectionEvent;
import com.vaadin.event.selection.SingleSelectionListener;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.selection.SelectionServerRpc;
import com.vaadin.shared.ui.AbstractSingleSelectState;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import elemental.json.Json;
import elemental.json.JsonObject;
/**
* An abstract base class for listing components that only support single
* selection and no lazy loading of data items.
*
* @author Vaadin Ltd.
*
* @param
* the item date type
*
* @see com.vaadin.data.SelectionModel.Single
*
* @since 8.0
*/
public abstract class AbstractSingleSelect extends AbstractListing
implements SingleSelect {
private T selectedItem = null;
/**
* Creates a new {@code AbstractListing} with a default data communicator.
*
*/
protected AbstractSingleSelect() {
init();
}
/**
* Creates a new {@code AbstractSingleSelect} with the given custom data
* communicator.
*
* Note: This method is for creating an
* {@code AbstractSingleSelect} with a custom communicator. In the common
* case {@link AbstractSingleSelect#AbstractSingleSelect()} should be used.
*
*
* @param dataCommunicator
* the data communicator to use, not null
*/
protected AbstractSingleSelect(DataCommunicator dataCommunicator) {
super(dataCommunicator);
init();
}
/**
* Adds a selection listener to this select. The listener is called when the
* selection is changed either by the user or programmatically.
*
* @param listener
* the selection listener, not null
* @return a registration for the listener
*/
public Registration addSelectionListener(
SingleSelectionListener listener) {
return addListener(SingleSelectionEvent.class, listener,
SingleSelectionListener.SELECTION_CHANGE_METHOD);
}
/**
* Returns the currently selected item, or an empty optional if no item is
* selected.
*
* @return an optional of the selected item if any, an empty optional
* otherwise
*/
public Optional getSelectedItem() {
return Optional.ofNullable(selectedItem);
}
/**
* Sets the current selection to the given item or clears selection if given
* {@code null}.
*
* @param item
* the item to select or {@code null} to clear selection
*/
public void setSelectedItem(T item) {
setSelectedItem(item, false);
}
/**
* Returns the current value of this object which is the currently selected
* item.
*
* The call is delegated to {@link #getSelectedItem()}
*
* @return the current selection, may be {@code null}
*
* @see #getSelectedItem()
* @see Single#getSelectedItem
*/
@Override
public T getValue() {
return getSelectedItem().orElse(null);
}
/**
* Sets the value of this object which is an item to select. If the new
* value is not equal to {@code getValue()}, fires a value change event. If
* value is {@code null} then it deselects currently selected item.
*
* The call is delegated to {@link #setSelectedItem(Object)}.
*
* @see #setSelectedItem(Object)
* @see Single#setSelectedItem(Object)
*
* @param value
* the item to select or {@code null} to clear selection
*/
@Override
public void setValue(T value) {
setSelectedItem(value);
}
@Override
public Registration addValueChangeListener(
HasValue.ValueChangeListener listener) {
return addSelectionListener(
event -> listener.valueChange(new ValueChangeEvent<>(this,
event.getOldValue(), event.isUserOriginated())));
}
@Override
protected AbstractSingleSelectState getState() {
return (AbstractSingleSelectState) super.getState();
}
@Override
protected AbstractSingleSelectState getState(boolean markAsDirty) {
return (AbstractSingleSelectState) super.getState(markAsDirty);
}
@Override
public void setRequiredIndicatorVisible(boolean visible) {
super.setRequiredIndicatorVisible(visible);
}
@Override
public boolean isRequiredIndicatorVisible() {
return super.isRequiredIndicatorVisible();
}
@Override
public void setReadOnly(boolean readOnly) {
super.setReadOnly(readOnly);
}
@Override
public boolean isReadOnly() {
return super.isReadOnly();
}
/**
* Returns the item that the given key is assigned to, or {@code null} if
* there is no such item.
*
* @param key
* the key whose item to return
* @return the associated item if any, {@code null} otherwise.
*/
protected T keyToItem(String key) {
return getDataCommunicator().getKeyMapper().get(key);
}
/**
* Returns whether the given item is currently selected.
*
* @param item
* the item to check, not null
* @return {@code true} if the item is selected, {@code false} otherwise
*/
public boolean isSelected(T item) {
if (Objects.equals(selectedItem, item)) {
return true;
}
if (item == null || selectedItem == null) {
return false;
}
return Objects.equals(getDataProvider().getId(selectedItem),
getDataProvider().getId(item));
}
@Override
protected Element writeItem(Element design, T item, DesignContext context) {
Element element = super.writeItem(design, item, context);
if (isSelected(item)) {
element.attr("selected", true);
}
return element;
}
@Override
protected void readItems(Element design, DesignContext context) {
Set selected = new HashSet<>();
List items = design.children().stream()
.map(child -> readItem(child, selected, context))
.collect(Collectors.toList());
if (!items.isEmpty()) {
setItems(items);
}
selected.forEach(this::setValue);
}
/**
* Reads an Item from a design and inserts it into the data source.
* Hierarchical select components should override this method to recursively
* recursively read any child items as well.
*
* @param child
* a child element representing the item
* @param selected
* A set accumulating selected items. If the item that is read is
* marked as selected, its item id should be added to this set.
* @param context
* the DesignContext instance used in parsing
* @return the item id of the new item
*
* @throws DesignException
* if the tag name of the {@code child} element is not
* {@code option}.
*/
protected T readItem(Element child, Set selected,
DesignContext context) {
T item = readItem(child, context);
if (child.hasAttr("selected")) {
selected.add(item);
}
return item;
}
@Override
protected Collection getCustomAttributes() {
Collection attributes = super.getCustomAttributes();
// "value" is not an attribute for the component. "selected" attribute
// is used in "option"'s tag to mark selection which implies value for
// single select component
attributes.add("value");
return attributes;
}
private void init() {
registerRpc(new SelectionServerRpc() {
@Override
public void select(String key) {
setSelectedItem(keyToItem(key), true);
}
@Override
public void deselect(String key) {
T item = keyToItem(key);
if (isSelected(item)) {
setSelectedItem(null, true);
}
}
});
addDataGenerator(new DataGenerator() {
@Override
public void generateData(T item, JsonObject jsonObject) {
if (isSelected(item)) {
// Deferred update of state.
updateSelectedItemState(item);
}
}
@Override
public void refreshData(T item) {
if (isSelected(item)) {
selectedItem = item;
// Invalidate old data
updateSelectedItemState(null);
}
}
});
}
/**
* This method updates the internal selection state of the server-side of
* {@code AbstractSingleSelect}.
*
* @param value
* the value that should be selected
* @param userOriginated
* {@code true} if selection was done by user, {@code false} if
* not
*
* @since 8.5
*/
protected void setSelectedItem(T value, boolean userOriginated) {
if (isSelected(value)) {
return;
}
// Update selection
T oldValue = selectedItem;
selectedItem = value;
// Re-generate selected item data
if (oldValue != null) {
getDataCommunicator().refresh(oldValue);
}
if (value != null) {
getDataCommunicator().refresh(value);
}
// Deselection can be handled immediately
updateSelectedItemState(value);
// Update diffstate to make sure null can be selected later.
updateDiffstate("selectedItemKey", Json.createObject());
fireEvent(new SingleSelectionEvent<>(AbstractSingleSelect.this,
oldValue, userOriginated));
}
/**
* This method updates the shared selection state of the
* {@code AbstractSingleSelect}.
*
* @param value
* the value that is selected; may be {@code null}
*
* @since 8.5
*/
protected void updateSelectedItemState(T value) {
// FIXME: If selecting a value that does not exist, this will leave and
// extra object in the key mapper that will not be dropped any time.
getState().selectedItemKey = value != null
? getDataCommunicator().getKeyMapper().key(value)
: null;
}
}