com.vaadin.flow.component.checkbox.CheckboxGroup Maven / Gradle / Ivy
/*
* Copyright 2000-2023 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.checkbox;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.AbstractSinglePropertyField;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.HasAriaLabel;
import com.vaadin.flow.component.HasHelper;
import com.vaadin.flow.component.ItemLabelGenerator;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.checkbox.dataview.CheckboxGroupDataView;
import com.vaadin.flow.component.checkbox.dataview.CheckboxGroupListDataView;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.ClientValidationUtil;
import com.vaadin.flow.component.shared.HasClientValidation;
import com.vaadin.flow.component.shared.HasThemeVariant;
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.data.binder.HasItemComponents;
import com.vaadin.flow.data.binder.HasValidator;
import com.vaadin.flow.data.binder.ValidationStatusChangeEvent;
import com.vaadin.flow.data.binder.ValidationStatusChangeListener;
import com.vaadin.flow.data.provider.DataChangeEvent;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.DataProviderWrapper;
import com.vaadin.flow.data.provider.DataViewUtils;
import com.vaadin.flow.data.provider.HasDataView;
import com.vaadin.flow.data.provider.HasListDataView;
import com.vaadin.flow.data.provider.IdentifierProvider;
import com.vaadin.flow.data.provider.InMemoryDataProvider;
import com.vaadin.flow.data.provider.ItemCountChangeEvent;
import com.vaadin.flow.data.provider.KeyMapper;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.data.renderer.ComponentRenderer;
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.function.SerializableConsumer;
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.shared.Registration;
import elemental.json.Json;
import elemental.json.JsonArray;
/**
* CheckBoxGroup is a multi-selection component where items are displayed as
* check boxes.
*
* Use CheckBoxGroup to group related items. Individual checkboxes should be
* used for options that are not related to each other in any way.
*
* @author Vaadin Ltd
*/
@Tag("vaadin-checkbox-group")
@NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.2.5")
@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js")
@NpmPackage(value = "@vaadin/checkbox-group", version = "24.2.5")
@JsModule("@vaadin/checkbox-group/src/vaadin-checkbox-group.js")
public class CheckboxGroup
extends AbstractSinglePropertyField, Set>
implements HasAriaLabel, HasClientValidation,
HasDataView>, HasHelper,
HasItemComponents,
InputField, Set>, Set>,
HasListDataView>,
HasThemeVariant, HasValidationProperties,
HasValidator>, MultiSelect, T> {
private static final String VALUE = "value";
private final KeyMapper keyMapper = new KeyMapper<>(this::getItemId);
private final AtomicReference> dataProvider = new AtomicReference<>(
DataProvider.ofItems());
private boolean isReadOnly;
private SerializablePredicate itemEnabledProvider = item -> isEnabled();
private ItemLabelGenerator itemLabelGenerator = String::valueOf;
private ComponentRenderer extends Component, T> itemRenderer;
private Registration dataProviderListenerRegistration;
private int lastNotifiedDataSize = -1;
private volatile int lastFetchedDataSize = -1;
private SerializableConsumer sizeRequest;
private boolean manualValidationEnabled = false;
/**
* Creates an empty checkbox group
*/
public CheckboxGroup() {
super("value", Collections.emptySet(), JsonArray.class,
CheckboxGroup::presentationToModel,
CheckboxGroup::modelToPresentation);
addValueChangeListener(e -> validate());
addClientValidatedEventListener(e -> validate());
}
/**
* Creates an empty checkbox group with the defined label.
*
* @param label
* the label describing the checkbox group
* @see #setLabel(String)
*/
public CheckboxGroup(String label) {
this();
setLabel(label);
}
/**
* Creates a checkbox group with the defined label and populated with the
* items in the collection.
*
* @param label
* the label describing the checkbox group
* @param items
* the items to be shown in the list of the checkbox group
* @see #setLabel(String)
* @see #setItems(Collection)
*/
public CheckboxGroup(String label, Collection items) {
this();
setLabel(label);
setItems(items);
}
/**
* Creates a checkbox group with the defined label and populated with the
* items in the array.
*
* @param label
* the label describing the checkbox group
* @param items
* the items to be shown in the list of the checkbox group
* @see #setLabel(String)
* @see #setItems(Object...)
*/
@SafeVarargs
public CheckboxGroup(String label, T... items) {
this();
setLabel(label);
setItems(items);
}
/**
* Constructs a checkbox group with a value change listener.
*
* @param listener
* the value change listener to add
* @see #addValueChangeListener(ValueChangeListener)
*/
public CheckboxGroup(
ValueChangeListener, Set>> listener) {
this();
addValueChangeListener(listener);
}
/**
* Constructs a checkbox group with the defined label and a value change
* listener.
*
* @param label
* the label describing the checkbox group
* @param listener
* the value change listener to add
* @see #setLabel(String)
* @see #addValueChangeListener(ValueChangeListener)
*/
public CheckboxGroup(String label,
ValueChangeListener, Set>> listener) {
this(label);
addValueChangeListener(listener);
}
/**
* Constructs a checkbox group with the defined label, a value change
* listener and populated with the items in the array.
*
* @param label
* the label describing the checkbox group
* @param listener
* the value change listener to add
* @param items
* the items to be shown in the list of the checkbox group
* @see #setLabel(String)
* @see #addValueChangeListener(ValueChangeListener)
* @see #setItems(Object...)
*/
@SafeVarargs
public CheckboxGroup(String label,
ValueChangeListener, Set>> listener,
T... items) {
this(label, listener);
setItems(items);
}
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
ClientValidationUtil.preventWebComponentFromModifyingInvalidState(this);
}
@Override
public CheckboxGroupDataView setItems(
DataProvider dataProvider) {
setDataProvider(dataProvider);
return getGenericDataView();
}
@Override
public CheckboxGroupDataView setItems(
InMemoryDataProvider inMemoryDataProvider) {
// We don't use DataProvider.withConvertedFilter() here because it's
// implementation does not apply the filter converter if Query has a
// null filter
DataProvider convertedDataProvider = new DataProviderWrapper>(
inMemoryDataProvider) {
@Override
protected SerializablePredicate getFilter(Query query) {
// Just ignore the query filter (Void) and apply the
// predicate only
return Optional.ofNullable(inMemoryDataProvider.getFilter())
.orElse(item -> true);
}
};
return setItems(convertedDataProvider);
}
@Override
public CheckboxGroupListDataView setItems(
ListDataProvider dataProvider) {
setDataProvider(dataProvider);
return getListDataView();
}
/**
* Gets the list data view for the checkbox group. This data view should
* only be used when the items are in-memory and set with:
*
* - {@link #setItems(Collection)}
* - {@link #setItems(Object[])}
* - {@link #setItems(ListDataProvider)}
*
* If the items are not in-memory an exception is thrown.
*
* @return the list data view that provides access to the data bound to the
* checkbox group
*/
@Override
public CheckboxGroupListDataView getListDataView() {
return new CheckboxGroupListDataView<>(this::getDataProvider, this,
this::identifierProviderChanged, (filter, sorting) -> reset());
}
/**
* Gets the generic data view for the checkbox group. This data view should
* only be used when {@link #getListDataView()} is not applicable for the
* underlying data provider.
*
* @return the generic DataView instance implementing
* {@link CheckboxGroupDataView}
*/
@Override
public CheckboxGroupDataView getGenericDataView() {
return new CheckboxGroupDataView<>(this::getDataProvider, this,
this::identifierProviderChanged);
}
private static class CheckBoxItem extends Checkbox
implements HasItemComponents.ItemComponent {
private final T item;
private CheckBoxItem(String id, T item) {
this.item = item;
getElement().setProperty(VALUE, id);
}
@Override
public T getItem() {
return item;
}
}
/**
* Sets a generic data provider for the CheckboxGroup to use.
*
* 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}.
*
* @param dataProvider
* DataProvider instance to use, not null
*/
public void setDataProvider(DataProvider dataProvider) {
this.dataProvider.set(dataProvider);
DataViewUtils.removeComponentFilterAndSortComparator(this);
reset();
if (dataProviderListenerRegistration != null) {
dataProviderListenerRegistration.remove();
}
dataProviderListenerRegistration = dataProvider
.addDataProviderListener(event -> {
if (event instanceof DataChangeEvent.DataRefreshEvent) {
T otherItem = ((DataChangeEvent.DataRefreshEvent) event)
.getItem();
this.getCheckboxItems()
.filter(item -> Objects.equals(
getItemId(item.item),
getItemId(otherItem)))
.findFirst().ifPresent(this::updateCheckbox);
} else {
reset();
}
});
}
@Override
public void updateSelection(Set addedItems, Set removedItems) {
Set value = new HashSet<>(getValue());
value.addAll(addedItems);
value.removeAll(removedItems);
setValue(value);
}
/**
* Sets the value of this component. If the new value is not equal to the
* previous value, fires a value change event.
*
* The component doesn't accept {@code null} values. The value of a checkbox
* group without any selected items is an empty set. You can use the
* {@link #clear()} method to set the empty value.
*
* @param value
* the new value to set, not {@code null}
* @throws NullPointerException
* if value is {@code null}
*/
@Override
public void setValue(Set value) {
Objects.requireNonNull(value,
"Cannot set a null value to checkbox group. "
+ "Use the clear-method to reset the component's value to an empty set.");
super.setValue(value);
refreshCheckboxes();
}
@Override
public Set getSelectedItems() {
return getValue();
}
@Override
public Registration addSelectionListener(
MultiSelectionListener, T> listener) {
return addValueChangeListener(event -> listener
.selectionChange(new MultiSelectionEvent<>(this, this,
event.getOldValue(), event.isFromClient())));
}
/**
* Gets the data provider used by this CheckboxGroup.
*
*
* To get information and control over the items in the CheckboxGroup, use
* either {@link #getListDataView()} or {@link #getGenericDataView()}
* instead.
*
* @return the data provider used by this CheckboxGroup
*/
public DataProvider getDataProvider() {
// dataProvider reference won't have been initialized before
// calling from CheckboxGroup constructor
return Optional.ofNullable(dataProvider).map(AtomicReference::get)
.orElse(null);
}
@Override
public void onEnabledStateChanged(boolean enabled) {
if (isReadOnly()) {
setDisabled(true);
} else {
setDisabled(!enabled);
}
getCheckboxItems().forEach(this::updateEnabled);
}
@Override
public void setReadOnly(boolean readOnly) {
isReadOnly = readOnly;
if (isEnabled()) {
setDisabled(readOnly);
refreshCheckboxes();
}
}
@Override
public boolean isReadOnly() {
return isReadOnly;
}
/**
* Returns the item enabled predicate.
*
* @return the item enabled predicate
* @see #setItemEnabledProvider
*/
public SerializablePredicate getItemEnabledProvider() {
return itemEnabledProvider;
}
/**
* Sets the item enabled predicate for this checkbox group. 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. The default predicate
* always returns true (all the items are enabled).
*
* @param itemEnabledProvider
* the item enable predicate, not {@code null}
*/
public void setItemEnabledProvider(
SerializablePredicate itemEnabledProvider) {
this.itemEnabledProvider = Objects.requireNonNull(itemEnabledProvider);
refreshCheckboxes();
}
/**
* Sets the item label generator that is used to produce the strings shown
* in the checkbox group for each item. By default,
* {@link String#valueOf(Object)} is used.
*
* Setting an item label generator removes any previously set item renderer.
*
* @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;
this.itemRenderer = null;
refreshCheckboxes();
}
/**
* Gets the item label generator that is used to produce the strings shown
* in the checkbox group for each item.
*
* @return the item label generator used, not null
*/
public ItemLabelGenerator getItemLabelGenerator() {
return itemLabelGenerator;
}
/**
* Sets the label for the checkbox group.
*
* @param label
* value for the {@code label} property in the checkbox group
*/
public void setLabel(String label) {
getElement().setProperty("label", label == null ? "" : label);
}
/**
* Gets the label of the checkbox group.
*
* @return the {@code label} property of the checkbox group
*/
public String getLabel() {
return getElement().getProperty("label");
}
@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"));
}
/**
* Specifies that the user must fill in a value.
*
* @param required
* the boolean value to set
*/
public void setRequired(boolean required) {
getElement().setProperty("required", required);
}
/**
* Determines whether the checkbox group is marked as input required.
*
* This property is not synchronized automatically from the client side, so
* the returned value may not be the same as in client side.
*
* @return {@code true} if the input is required, {@code false} otherwise
*/
public boolean isRequired() {
return getElement().getProperty("required", false);
}
/**
* If true, the user cannot interact with this element.
*
* @param disabled
* the boolean value to set
*/
protected void setDisabled(boolean disabled) {
getElement().setProperty("disabled", disabled);
}
/**
* If true, the user cannot interact with this element.
*
* @return the {@code disabled} property from the webcomponent
*/
protected boolean isDisabledBoolean() {
return getElement().getProperty("disabled", false);
}
@Override
protected boolean valueEquals(Set value1, Set value2) {
assert value1 != null && value2 != null;
if (value1.size() != value2.size()) {
return false;
}
if (getDataProvider() == null) {
return super.valueEquals(value1, value2);
}
IdentifierProvider identifierProvider = getIdentifierProvider();
Set