com.vaadin.flow.component.textfield.BigDecimalField Maven / Gradle / Ivy
/**
* Copyright 2000-2024 Vaadin Ltd.
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See {@literal } for the full
* license.
*/
package com.vaadin.flow.component.textfield;
import java.math.BigDecimal;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.CompositionNotifier;
import com.vaadin.flow.component.HasHelper;
import com.vaadin.flow.component.HasLabel;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasValidation;
import com.vaadin.flow.component.InputNotifier;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.shared.ClientValidationUtil;
import com.vaadin.flow.component.shared.HasClientValidation;
import com.vaadin.flow.component.shared.ValidationUtil;
import com.vaadin.flow.data.binder.HasValidator;
import com.vaadin.flow.data.binder.ValidationResult;
import com.vaadin.flow.data.binder.ValidationStatusChangeEvent;
import com.vaadin.flow.data.binder.ValidationStatusChangeListener;
import com.vaadin.flow.data.binder.Validator;
import com.vaadin.flow.data.value.HasValueChangeMode;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.function.SerializableBiFunction;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.shared.Registration;
/**
* Server-side component for the {@code vaadin-big-decimal-field} element. This
* field uses {@link BigDecimal} as the server-side value type, which allows
* handling decimal numbers with high precision. The component also prevents
* users from entering characters which can't be used in a decimal number, such
* as alphabets.
*
* When setting values from the server-side, the {@code scale} of the provided
* {@link BigDecimal} is preserved in the presentation format shown to the user,
* as described in {@link #setValue(BigDecimal)}.
*
* @author Vaadin Ltd.
*/
@Tag("vaadin-big-decimal-field")
@JavaScript("frontend://vaadin-big-decimal-field.js")
@JsModule("./vaadin-big-decimal-field.js")
public class BigDecimalField
extends GeneratedVaadinTextField
implements HasSize, HasValidation, HasValueChangeMode,
HasPrefixAndSuffix, InputNotifier, KeyNotifier, CompositionNotifier,
HasAutocomplete, HasAutocapitalize, HasAutocorrect, HasHelper, HasLabel,
HasValidator, HasClientValidation {
private ValueChangeMode currentMode;
private boolean isConnectorAttached;
private int valueChangeTimeout = DEFAULT_CHANGE_TIMEOUT;
private boolean required;
private Locale locale;
private static final SerializableBiFunction PARSER = (
field, valueFromClient) -> {
if (valueFromClient == null || valueFromClient.isEmpty()) {
return null;
}
try {
return new BigDecimal(
valueFromClient.replace(field.getDecimalSeparator(), '.'));
} catch (NumberFormatException e) {
return null;
}
};
private static final SerializableBiFunction FORMATTER = (
field, valueFromModel) -> valueFromModel == null ? ""
: valueFromModel.toPlainString().replace('.',
field.getDecimalSeparator());
/**
* Constructs an empty {@code BigDecimalField}.
*/
public BigDecimalField() {
super(null, null, String.class, PARSER, FORMATTER);
setLocale(Optional.ofNullable(UI.getCurrent()).map(UI::getLocale)
.orElse(Locale.ROOT));
// workaround for https://github.com/vaadin/flow/issues/3496
setInvalid(false);
setValueChangeMode(ValueChangeMode.ON_CHANGE);
addValueChangeListener(e -> validate());
if (isEnforcedFieldValidationEnabled()) {
addClientValidatedEventListener(e -> validate());
}
}
/**
* Constructs an empty {@code BigDecimalField} with the given label.
*
* @param label
* the text to set as the label
*/
public BigDecimalField(String label) {
this();
setLabel(label);
}
/**
* Constructs an empty {@code BigDecimalField} with the given label and
* placeholder text.
*
* @param label
* the text to set as the label
* @param placeholder
* the placeholder text to set
*/
public BigDecimalField(String label, String placeholder) {
this(label);
setPlaceholder(placeholder);
}
/**
* Constructs a {@code BigDecimalField} with the given label, an initial
* value and placeholder text.
*
* @param label
* the text to set as the label
* @param initialValue
* the initial value
* @param placeholder
* the placeholder text to set
* @see #setValue(Object)
* @see #setPlaceholder(String)
*/
public BigDecimalField(String label, BigDecimal initialValue,
String placeholder) {
this(label);
setValue(initialValue);
setPlaceholder(placeholder);
}
/**
* Constructs an empty {@code BigDecimalField} with a value change listener.
*
* @param listener
* the value change listener
* @see #addValueChangeListener(com.vaadin.flow.component.HasValue.ValueChangeListener)
*/
public BigDecimalField(
ValueChangeListener> listener) {
this();
addValueChangeListener(listener);
}
/**
* Constructs an empty {@code BigDecimalField} with a label and a value
* change listener.
*
* @param label
* the text to set as the label
* @param listener
* the value change listener
* @see #setLabel(String)
* @see #addValueChangeListener(com.vaadin.flow.component.HasValue.ValueChangeListener)
*/
public BigDecimalField(String label,
ValueChangeListener> listener) {
this(label);
addValueChangeListener(listener);
}
/**
* Constructs an empty {@code BigDecimalField} with a label,a value change
* listener and an initial value.
*
* @param label
* the text to set as the label
* @param initialValue
* the initial value
* @param listener
* the value change listener
* @see #setLabel(String)
* @see #setValue(Object)
* @see #addValueChangeListener(com.vaadin.flow.component.HasValue.ValueChangeListener)
*/
public BigDecimalField(String label, BigDecimal initialValue,
ValueChangeListener> listener) {
this(label);
setValue(initialValue);
addValueChangeListener(listener);
}
/**
* {@inheritDoc}
*
* The default value is {@link ValueChangeMode#ON_CHANGE}.
*/
@Override
public ValueChangeMode getValueChangeMode() {
return currentMode;
}
@Override
public void setValueChangeMode(ValueChangeMode valueChangeMode) {
currentMode = valueChangeMode;
setSynchronizedEvent(
ValueChangeMode.eventForMode(valueChangeMode, "value-changed"));
applyChangeTimeout();
}
@Override
public void setValueChangeTimeout(int valueChangeTimeout) {
this.valueChangeTimeout = valueChangeTimeout;
applyChangeTimeout();
}
@Override
public int getValueChangeTimeout() {
return valueChangeTimeout;
}
private void applyChangeTimeout() {
ValueChangeMode.applyChangeTimeout(getValueChangeMode(),
getValueChangeTimeout(), getSynchronizationRegistration());
}
@Override
public String getErrorMessage() {
return super.getErrorMessageString();
}
@Override
public void setErrorMessage(String errorMessage) {
super.setErrorMessage(errorMessage);
}
@Override
public boolean isInvalid() {
return isInvalidBoolean();
}
@Override
public void setInvalid(boolean invalid) {
super.setInvalid(invalid);
}
/**
* Sets the label for this component.
*
* @param label
* value for the {@code label} property in the webcomponent
*/
@Override
public void setLabel(String label) {
super.setLabel(label);
}
/**
* String used for the label element.
*
* @return the {@code label} property from the webcomponent
*/
@Override
public String getLabel() {
return getLabelString();
}
@Override
public void setPlaceholder(String placeholder) {
super.setPlaceholder(placeholder);
}
/**
* A hint to the user of what can be entered in the component.
*
* @return the {@code placeholder} property from the webcomponent
*/
public String getPlaceholder() {
return getPlaceholderString();
}
/**
* Specifies if the field value gets automatically selected when the field
* gains focus.
*
* @return true
if autoselect is active, false
* otherwise
*/
public boolean isAutoselect() {
return super.isAutoselectBoolean();
}
/**
* Set to true
to always have the field value automatically
* selected when the field gains focus, false
otherwise.
*
* @param autoselect
* true
to set auto select on, false
* otherwise
*/
@Override
public void setAutoselect(boolean autoselect) {
super.setAutoselect(autoselect);
}
/**
* Gets the visibility state of the button which clears the field.
*
* @return true
if the button is visible, false
* otherwise
*/
public boolean isClearButtonVisible() {
return isClearButtonVisibleBoolean();
}
/**
* Set to false
to hide the clear button which clears the text
* field.
*
* @param clearButtonVisible
* true
to set the button visible,
* false
otherwise
*/
@Override
public void setClearButtonVisible(boolean clearButtonVisible) {
super.setClearButtonVisible(clearButtonVisible);
}
@Override
public void setAutofocus(boolean autofocus) {
super.setAutofocus(autofocus);
}
/**
* Specify that this control should have input focus when the page loads.
*
* @return the {@code autofocus} property from the webcomponent
*/
public boolean isAutofocus() {
return isAutofocusBoolean();
}
/**
* The text usually displayed in a tooltip popup when the mouse is over the
* field.
*
* @return the {@code title} property from the webcomponent
*/
public String getTitle() {
return super.getTitleString();
}
@Override
public void setTitle(String title) {
super.setTitle(title);
}
@Override
public BigDecimal getEmptyValue() {
return null;
}
/**
* Sets the value of this field. If the new value is not equal to
* {@code getValue()}, fires a value change event.
*
* You can adjust how the value is presented in the field with the APIs
* provided by the value type {@link BigDecimal}. For example, you can
* change the number of decimal places with
* {@link BigDecimal#setScale(int)}. This doesn't however restrict the user
* from entering values with different number of decimals. Note that
* BigDecimals are immutable, so their methods will return new instances
* instead of editing the existing ones. Scientific notation (such as 1e9)
* is turned into plain number format for the presentation.
*
* @param value
* the new value
*/
@Override
public void setValue(BigDecimal value) {
BigDecimal oldValue = getValue();
super.setValue(value);
if (Objects.equals(oldValue, getEmptyValue())
&& Objects.equals(value, getEmptyValue())
&& isInputValuePresent()) {
// Clear the input element from possible bad input.
getElement().executeJs("this._inputElementValue = ''");
getElement().setProperty("_hasInputValue", false);
fireEvent(new ClientValidatedEvent(this, false));
} else {
// Restore the input element's value in case it was cleared
// in the above branch. That can happen when setValue(null)
// and setValue(...) are subsequently called within one round-trip
// and there was bad input.
getElement().executeJs("this._inputElementValue = this.value");
}
}
/**
* Returns the current value of the field. By default, the empty
* BigDecimalField will return {@code null}.
*
* @return the current value.
*/
@Override
public BigDecimal getValue() {
return super.getValue();
}
/**
* Performs server-side validation of the current value. This is needed
* because it is possible to circumvent the client-side validation
* constraints using browser development tools.
*/
@Override
protected void validate() {
BigDecimal value = getValue();
boolean isRequired = isRequiredIndicatorVisible();
ValidationResult requiredValidation = ValidationUtil
.checkRequired(isRequired, value, getEmptyValue());
setInvalid(
requiredValidation.isError() || checkValidity(value).isError());
}
private ValidationResult checkValidity(BigDecimal value) {
boolean hasNonParsableValue = Objects.equals(value, getEmptyValue())
&& isInputValuePresent();
if (hasNonParsableValue) {
return ValidationResult.error("");
}
return ValidationResult.ok();
}
@Override
public Validator getDefaultValidator() {
return (value, context) -> checkValidity(value);
}
@Override
public Registration addValidationStatusChangeListener(
ValidationStatusChangeListener listener) {
if (isEnforcedFieldValidationEnabled()) {
return addClientValidatedEventListener(
event -> listener.validationStatusChanged(
new ValidationStatusChangeEvent(this,
!isInvalid())));
}
return null;
}
/**
* Returns whether the input element has a value or not.
*
* @return true
if the input element's value is populated,
* false
otherwise
*/
@Synchronize(property = "_hasInputValue", value = "has-input-value-changed")
private boolean isInputValuePresent() {
return getElement().getProperty("_hasInputValue", false);
}
/**
* Sets the locale for this BigDecimalField. It is used to determine which
* decimal separator (the radix point) should be used.
*
* @param locale
* the locale to set, not {@code null}
*/
public void setLocale(Locale locale) {
Objects.requireNonNull(locale, "Locale to set can't be null.");
this.locale = locale;
setDecimalSeparator(
new DecimalFormatSymbols(locale).getDecimalSeparator());
}
/**
* Gets the locale used by this BigDecimalField. It is used to determine
* which decimal separator (the radix point) should be used.
*
* @return the locale of this field, never {@code null}
*/
@Override
public Locale getLocale() {
return locale;
}
/**
* Updates two things at client-side: changes the decimal separator in the
* current input value, and updates the invalid input prevention to accept
* the new decimal separator.
*/
private void setDecimalSeparator(char decimalSeparator) {
getElement().setProperty("_decimalSeparator", decimalSeparator + "");
}
private char getDecimalSeparator() {
return getElement().getProperty("_decimalSeparator").charAt(0);
}
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
if (isEnforcedFieldValidationEnabled()) {
ClientValidationUtil
.preventWebComponentFromModifyingInvalidState(this);
} else {
FieldValidationUtil.disableClientValidation(this);
}
}
/**
* Whether the full experience validation is enforced for the component.
*
* Exposed with protected visibility to support mocking
*
* The method requires the {@code VaadinSession} instance to obtain the
* application configuration properties, otherwise, the feature is
* considered disabled.
*
* @return {@code true} if enabled, {@code false} otherwise.
*/
protected boolean isEnforcedFieldValidationEnabled() {
VaadinSession session = VaadinSession.getCurrent();
if (session == null) {
return false;
}
DeploymentConfiguration configuration = session.getConfiguration();
if (configuration == null) {
return false;
}
return configuration.isEnforcedFieldValidationEnabled();
}
}