org.dominokit.domino.ui.forms.NumberBox Maven / Gradle / Ivy
/*
* Copyright © 2019 Dominokit
*
* 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 org.dominokit.domino.ui.forms;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.dominokit.domino.ui.utils.Domino.*;
import static org.dominokit.domino.ui.utils.Domino.div;
import static org.dominokit.domino.ui.utils.Domino.input;
import elemental2.dom.ClipboardEvent;
import elemental2.dom.Event;
import elemental2.dom.HTMLInputElement;
import elemental2.dom.KeyboardEvent;
import java.util.Objects;
import java.util.function.Function;
import jsinterop.base.Js;
import org.dominokit.domino.ui.elements.DivElement;
import org.dominokit.domino.ui.events.EventType;
import org.dominokit.domino.ui.forms.validations.InputAutoValidator;
import org.dominokit.domino.ui.forms.validations.ValidationResult;
import org.dominokit.domino.ui.utils.*;
import org.gwtproject.i18n.client.NumberFormat;
import org.gwtproject.i18n.shared.cldr.LocaleInfo;
import org.gwtproject.i18n.shared.cldr.NumberConstants;
/**
* An abstract representation of a number input field with various customizations such as min/max
* values, prefix/postfix elements, and input validation.
*
* Usage example:
*
*
* NumberBox ageBox = new NumberBox<>("Age");
* ageBox.setMinValue(0.0);
* ageBox.setMaxValue(150.0);
*
*
* @param Concrete type of the NumberBox
* @param Value type (Number subclass) that this NumberBox supports
*/
public abstract class NumberBox, V extends Number>
extends InputFormField
implements HasMinMaxValue, HasStep, HasPostfix, HasPrefix, HasPlaceHolder {
protected final LazyChild prefixElement;
protected final LazyChild postfixElement;
private final ChangeListener formatValueChangeListener =
(oldValue, newValue) -> formatValue(newValue);
private Function valueParser = defaultValueParser();
private final NumberFormatSupplier defaultFormatSupplier =
formatPattern -> {
if (nonNull(getPattern())) {
return NumberFormat.getFormat(getPattern());
} else {
return NumberFormat.getDecimalFormat();
}
};
private NumberFormatSupplier numberFormatSupplier = defaultFormatSupplier;
private String maxValueErrorMessage;
private String minValueErrorMessage;
private String invalidFormatMessage;
private boolean formattingEnabled;
private String pattern = null;
/** Initializes the number box with default configurations. */
public NumberBox() {
super();
prefixElement = LazyChild.of(div().addCss(dui_field_prefix), wrapperElement);
postfixElement = LazyChild.of(div().addCss(dui_field_postfix), wrapperElement);
addValidator(this::validateInputString);
addValidator(this::validateMaxValue);
addValidator(this::validateMinValue);
setAutoValidation(true);
enableFormatting();
getInputElement().addEventListener(EventType.keypress, this::onKeyPress);
getInputElement().addEventListener(EventType.paste, this::onPaste);
}
/**
* Initializes the number box with a given label.
*
* @param label The label for the number box.
*/
public NumberBox(String label) {
this();
setLabel(label);
}
/**
* Returns the type of the input field. In this case, a telephone input type is returned.
*
* @return String representation of the input type.
*/
@Override
public String getType() {
return "tel";
}
/**
* {@inheritDoc}
*
* Creates the HTML input element for the number box.
*
* @param type Type of the input element.
* @return A domino element wrapping the created input element.
*/
@Override
protected DominoElement createInputElement(String type) {
return input(type).addCss(dui_field_input).toDominoElement();
}
/**
* Validates the provided input against standard number formatting.
*
* @param target The number box being validated.
* @return A ValidationResult indicating the success or failure of the validation.
*/
private ValidationResult validateInputString(NumberBox target) {
try {
tryGetValue();
} catch (NumberFormatException e) {
return ValidationResult.invalid(getInvalidFormatMessage());
}
return ValidationResult.valid();
}
/**
* Validates that the number box value does not exceed the defined maximum value.
*
* @param target The number box being validated.
* @return A ValidationResult indicating the success or failure of the validation.
*/
private ValidationResult validateMaxValue(NumberBox target) {
V value = getValue();
if (nonNull(value) && isExceedMaxValue(value)) {
return ValidationResult.invalid(getMaxValueErrorMessage());
}
return ValidationResult.valid();
}
/**
* Validates that the number box value is not less than the defined minimum value.
*
* @param target The number box being validated.
* @return A ValidationResult indicating the success or failure of the validation.
*/
private ValidationResult validateMinValue(NumberBox target) {
V value = getValue();
if (nonNull(value) && isLowerThanMinValue(value)) {
return ValidationResult.invalid(getMinValueErrorMessage());
}
return ValidationResult.valid();
}
/**
* Determines whether the number box input has a decimal separator.
*
* @return True if a decimal separator is present; otherwise, false.
*/
protected boolean hasDecimalSeparator() {
return false;
}
/**
* Constructs a regular expression pattern string to match allowed characters for the number box
* based on the locale and the custom pattern (if defined). The pattern will include numbers,
* minus sign, decimal separator, and any additional characters from the custom pattern.
*
* @return A string representing the regex pattern.
*/
protected String createKeyMatch() {
StringBuilder sB = new StringBuilder();
sB.append("[0-9");
NumberConstants numberConstants = LocaleInfo.getCurrentLocale().getNumberConstants();
sB.append(numberConstants.minusSign());
if (hasDecimalSeparator()) sB.append(numberConstants.decimalSeparator());
// If pattern is defined, except predefined digits, decimal separator and minus sign, append all
// other characters
if (pattern != null) sB.append(pattern.replaceAll("[0#.-]", ""));
sB.append(']');
return sB.toString();
}
/**
* Handles the "keypress" event for the number box. Only allows key presses that match the pattern
* created by the {@link #createKeyMatch()} method.
*
* @param event The keyboard event associated with the key press.
*/
protected void onKeyPress(Event event) {
KeyboardEvent keyboardEvent = Js.uncheckedCast(event);
if (!keyboardEvent.key.matches(createKeyMatch())) event.preventDefault();
}
/**
* Handles the "paste" event for the number box. It ensures that pasted value is of a valid
* format. If the pasted value is not a valid number, the event is prevented.
*
* @param event The clipboard event associated with the paste action.
*/
protected void onPaste(Event event) {
ClipboardEvent clipboardEvent = Js.uncheckedCast(event);
try {
parseValue(clipboardEvent.clipboardData.getData("text"));
} catch (NumberFormatException e) {
event.preventDefault();
}
}
/**
* Formats the provided value into a string representation and sets it as the value of the input
* element. If the provided value is null, the input element's value is set to an empty string.
*
* @param value The value to be formatted and set in the input element.
*/
private void formatValue(V value) {
getInputElement().element().value = nonNull(value) ? getNumberFormat().format(value) : "";
}
/**
* Attempts to retrieve and format the current value of the input element. If the current value is
* not a valid number format, no action is taken.
*/
private void formatValue() {
try {
formatValue(tryGetValue());
} catch (NumberFormatException e) {
// nothing to format, so we do nothing!
}
}
/**
* Overrides the parent class method to set the value for this number box. Depending on whether
* formatting is enabled or not, it either formats the number or simply converts it to a string.
*
* @param value The numeric value to be set in this box.
*/
@Override
protected void doSetValue(V value) {
if (nonNull(value)) {
if (this.formattingEnabled) {
getInputElement().element().value = getNumberFormat().format(value);
} else {
getInputElement().element().value = String.valueOf(value);
}
} else {
getInputElement().element().value = "";
}
}
/**
* Attempts to retrieve the current value of the input element and parse it into a numeric value.
* Returns null if the input element's value is empty.
*
* @return The parsed numeric value or null if the input value is empty.
*/
private V tryGetValue() {
String value = getStringValue();
if (value.isEmpty()) {
return null;
}
return parseValue(value);
}
/**
* Retrieves the current value of this number box. If the value cannot be parsed into a valid
* number format, the field is invalidated with an appropriate error message.
*
* @return The current numeric value or null if the value is not a valid number.
*/
@Override
public V getValue() {
try {
return tryGetValue();
} catch (NumberFormatException e) {
invalidate(
getStringValue().startsWith("-") ? getMinValueErrorMessage() : getMaxValueErrorMessage());
return null;
}
}
/**
* Determines if the current value of the input element is empty.
*
* @return true if the value is empty, false otherwise.
*/
@Override
public boolean isEmpty() {
String value = getInputElement().element().value;
return value.isEmpty();
}
/**
* Determines if the current value of the input element is empty or just contains white spaces.
*
* @return true if the value is empty or contains only white spaces, false otherwise.
*/
@Override
public boolean isEmptyIgnoreSpaces() {
String value = getInputElement().element().value;
return isEmpty() || value.trim().isEmpty();
}
/**
* Retrieves the current value of this number box as a string.
*
* @return The current string value of the input element.
*/
@Override
public String getStringValue() {
return getInputElement().element().value;
}
/**
* Sets the minimum allowed value for this number box. The provided value will be formatted and
* set as the min attribute of the input element.
*
* @param minValue The numeric value to set as the minimum allowable value.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setMinValue(V minValue) {
getInputElement().element().min = nonNull(minValue) ? getNumberFormat().format(minValue) : null;
return (T) this;
}
/**
* Sets the maximum allowed value for this number box. The provided value will be formatted and
* set as the max attribute of the input element.
*
* @param maxValue The numeric value to set as the maximum allowable value.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setMaxValue(V maxValue) {
getInputElement().element().max = nonNull(maxValue) ? getNumberFormat().format(maxValue) : null;
return (T) this;
}
/**
* Sets the step value, which determines the legal number intervals, for this number box. The
* provided value will be formatted and set as the step attribute of the input element.
*
* @param step The numeric value to set as the step value.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setStep(V step) {
getInputElement().element().step = nonNull(step) ? getNumberFormat().format(step) : null;
return (T) this;
}
/**
* Retrieves the maximum value allowed for this number box.
*
* @return The current maximum numeric value, or the default maximum value if not set.
*/
@Override
public V getMaxValue() {
String maxValue = getInputElement().element().max;
if (isNull(maxValue) || maxValue.isEmpty()) {
return defaultMaxValue();
}
return parseValue(maxValue);
}
/**
* Retrieves the minimum value allowed for this number box.
*
* @return The current minimum numeric value, or the default minimum value if not set.
*/
@Override
public V getMinValue() {
String minValue = getInputElement().element().min;
if (isNull(minValue) || minValue.isEmpty()) {
return defaultMinValue();
}
return parseValue(minValue);
}
/**
* Retrieves the current step value of this number box, which determines the legal number
* intervals.
*
* @return The current step numeric value or null if not set.
*/
@Override
public V getStep() {
String step = getInputElement().element().step;
if (isNull(step) || step.isEmpty()) {
return null;
}
return parseValue(step);
}
/**
* Sets a custom error message to be displayed when the entered number exceeds the maximum allowed
* value.
*
* @param maxValueErrorMessage The custom error message.
* @return This instance, to facilitate method chaining.
*/
public T setMaxValueErrorMessage(String maxValueErrorMessage) {
this.maxValueErrorMessage = maxValueErrorMessage;
return (T) this;
}
/**
* Sets a custom error message to be displayed when the entered number is below the minimum
* allowed value.
*
* @param minValueErrorMessage The custom error message.
* @return This instance, to facilitate method chaining.
*/
public T setMinValueErrorMessage(String minValueErrorMessage) {
this.minValueErrorMessage = minValueErrorMessage;
return (T) this;
}
/**
* Sets a custom error message to be displayed when the entered number format is invalid.
*
* @param invalidFormatMessage The custom error message.
* @return This instance, to facilitate method chaining.
*/
public T setInvalidFormatMessage(String invalidFormatMessage) {
this.invalidFormatMessage = invalidFormatMessage;
return (T) this;
}
/**
* Retrieves the custom error message set for exceeding the maximum value or provides a default
* message.
*
* @return The custom or default error message.
*/
public String getMaxValueErrorMessage() {
return isNull(maxValueErrorMessage)
? "Maximum allowed value is [" + getMaxValue() + "]"
: maxValueErrorMessage;
}
/**
* Retrieves the custom error message set for being below the minimum value or provides a default
* message.
*
* @return The custom or default error message.
*/
public String getMinValueErrorMessage() {
return isNull(minValueErrorMessage)
? "Minimum allowed value is [" + getMinValue() + "]"
: minValueErrorMessage;
}
/**
* Retrieves the custom error message set for invalid number format or provides a default message.
*
* @return The custom or default error message.
*/
public String getInvalidFormatMessage() {
return isNull(invalidFormatMessage) ? "Invalid number format" : invalidFormatMessage;
}
/**
* Checks if the provided value exceeds the maximum allowed value.
*
* @param value The number to check.
* @return True if the number exceeds the maximum, otherwise false.
*/
private boolean isExceedMaxValue(V value) {
V maxValue = getMaxValue();
if (isNull(maxValue)) return false;
return isExceedMaxValue(maxValue, value);
}
/**
* Checks if the provided value is below the minimum allowed value.
*
* @param value The number to check.
* @return True if the number is below the minimum, otherwise false.
*/
private boolean isLowerThanMinValue(V value) {
V minValue = getMinValue();
if (isNull(minValue)) return false;
return isLowerThanMinValue(minValue, value);
}
/**
* Enables the formatting of numbers displayed in the NumberBox.
*
* @return This instance, to facilitate method chaining.
*/
public T enableFormatting() {
return setFormattingEnabled(true);
}
/**
* Disables the formatting of numbers displayed in the NumberBox.
*
* @return This instance, to facilitate method chaining.
*/
public T disableFormatting() {
return setFormattingEnabled(false);
}
/**
* Sets whether the formatting of numbers should be enabled or disabled in the NumberBox.
*
* @param formattingEnabled A boolean indicating whether formatting should be enabled.
* @return This instance, to facilitate method chaining.
*/
private T setFormattingEnabled(boolean formattingEnabled) {
this.formattingEnabled = formattingEnabled;
if (formattingEnabled) {
if (!hasChangeListener(formatValueChangeListener)) {
addChangeListener(formatValueChangeListener);
}
} else {
removeChangeListener(formatValueChangeListener);
}
return (T) this;
}
/**
* Retrieves the current number format used by this NumberBox. If a custom pattern has been set,
* that format will be returned; otherwise, the default decimal format will be used.
*
* @return The NumberFormat instance corresponding to the current format.
*/
protected NumberFormat getNumberFormat() {
return numberFormatSupplier.get(getPattern());
}
/**
* Sets a supplier to use to get a custom number format to be used by this NumberBox. if null is
* provided, then use default supplier.
*
* @return same component.
*/
public T setNumberFormat(NumberFormatSupplier numberFormatSupplier) {
if (nonNull(numberFormatSupplier)) {
this.numberFormatSupplier = numberFormatSupplier;
} else {
this.numberFormatSupplier = defaultFormatSupplier;
}
return (T) this;
}
/**
* Retrieves the custom pattern used for number formatting in this NumberBox, if any.
*
* @return The custom pattern string, or null if none has been set.
*/
public String getPattern() {
return pattern;
}
/**
* Sets a custom pattern for number formatting in this NumberBox. After setting the pattern, the
* current value will be reformatted according to the new pattern.
*
* @param pattern The custom pattern string.
* @return This instance, to facilitate method chaining.
*/
public T setPattern(String pattern) {
if (!Objects.equals(this.pattern, pattern)) {
// It is important get the current numeric value based on old pattern
V value = getValue();
// Update the pattern now and format value with new pattern
this.pattern = pattern;
formatValue(value);
}
return (T) this;
}
/**
* Attempts to parse a given string into a double value, using the current number format. If
* parsing fails with the current format and a custom pattern is set, it will try to parse using
* the default decimal format.
*
* @param value The string to parse.
* @return The parsed double value.
* @throws NumberFormatException if the string cannot be parsed into a double.
*/
public double parseDouble(String value) {
try {
return getNumberFormat().parse(value);
} catch (NumberFormatException e) {
if (nonNull(getPattern())) {
return NumberFormat.getDecimalFormat().parse(value);
}
throw e;
}
}
/**
* Retrieves the placeholder text displayed in the NumberBox when it is empty.
*
* @return The placeholder text.
*/
@Override
public String getPlaceholder() {
return getInputElement().element().placeholder;
}
/**
* Sets the placeholder text to be displayed in the NumberBox when it is empty.
*
* @param placeholder The desired placeholder text.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setPlaceholder(String placeholder) {
getInputElement().element().placeholder = placeholder;
return (T) this;
}
/**
* Parses a given string value into the numeric type (V) of this NumberBox using the current value
* parser.
*
* @param value The string to be parsed.
* @return The parsed numeric value of type V.
*/
protected V parseValue(String value) {
return valueParser.apply(value);
}
/**
* Provides the default function used to parse string values into the numeric type (V) of this
* NumberBox.
*
* @return The default value parsing function.
*/
protected abstract java.util.function.Function defaultValueParser();
/**
* Provides the default maximum allowed value for this NumberBox.
*
* @return The default maximum value.
*/
protected abstract V defaultMaxValue();
/**
* Provides the default minimum allowed value for this NumberBox.
*
* @return The default minimum value.
*/
protected abstract V defaultMinValue();
/**
* Sets a custom parser function to parse string values into the numeric type (V) of this
* NumberBox. If the provided parser is null, the current parser remains unchanged.
*
* @param valueParser The custom value parsing function.
* @return This instance, to facilitate method chaining.
*/
public T setValueParser(java.util.function.Function valueParser) {
if (nonNull(valueParser)) {
this.valueParser = valueParser;
}
return (T) this;
}
/**
* Determines if a given value exceeds the specified maximum value.
*
* @param maxValue The maximum value to compare against.
* @param value The value to be checked.
* @return True if the value exceeds the maximum value; otherwise, false.
*/
protected abstract boolean isExceedMaxValue(V maxValue, V value);
/**
* Determines if a given value is lower than the specified minimum value.
*
* @param minValue The minimum value to compare against.
* @param value The value to be checked.
* @return True if the value is lower than the minimum value; otherwise, false.
*/
protected abstract boolean isLowerThanMinValue(V minValue, V value);
/**
* Creates an automatic validator for the NumberBox input.
*
* @param autoValidate A function to be called when automatic validation is triggered.
* @return A new instance of the InputAutoValidator for this NumberBox.
*/
@Override
public AutoValidator createAutoValidator(ApplyFunction autoValidate) {
return new InputAutoValidator(autoValidate, getInputElement());
}
/**
* Sets the text content for the postfix element.
*
* @param postfix The desired postfix string.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setPostfix(String postfix) {
postfixElement.get().setTextContent(postfix);
return (T) this;
}
/**
* Retrieves the text content of the postfix element.
*
* @return The postfix text.
*/
@Override
public String getPostfix() {
if (postfixElement.isInitialized()) {
return postfixElement.get().getTextContent();
}
return "";
}
/**
* Sets the text content for the prefix element.
*
* @param prefix The desired prefix string.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setPrefix(String prefix) {
prefixElement.get().setTextContent(prefix);
return (T) this;
}
/**
* Retrieves the text content of the prefix element.
*
* @return The prefix text.
*/
@Override
public String getPrefix() {
if (prefixElement.isInitialized()) {
return prefixElement.get().getTextContent();
}
return "";
}
/**
* Retrieves the prefix element itself.
*
* @return The DivElement representing the prefix.
*/
public DivElement getPrefixElement() {
return prefixElement.get();
}
/**
* Retrieves the postfix element itself.
*
* @return The DivElement representing the postfix.
*/
public DivElement getPostfixElement() {
return postfixElement.get();
}
/**
* Ensures the prefix element is initialized.
*
* @return This instance, to facilitate method chaining.
*/
public T withPrefixElement() {
prefixElement.get();
return (T) this;
}
/**
* Initializes and applies a handler to the prefix element.
*
* @param handler A function taking the current NumberBox instance and the prefix DivElement, to
* apply custom behavior.
* @return This instance, to facilitate method chaining.
*/
public T withPrefixElement(ChildHandler handler) {
handler.apply((T) this, prefixElement.get());
return (T) this;
}
/**
* Ensures the postfix element is initialized.
*
* @return This instance, to facilitate method chaining.
*/
public T withPostfixElement() {
postfixElement.get();
return (T) this;
}
/**
* Initializes and applies a handler to the postfix element.
*
* @param handler A function taking the current NumberBox instance and the postfix DivElement, to
* apply custom behavior.
* @return This instance, to facilitate method chaining.
*/
public T withPostfixElement(ChildHandler handler) {
handler.apply((T) this, postfixElement.get());
return (T) this;
}
/**
* Retrieves the name attribute of the input element.
*
* The name attribute specifies the name for an `` element. The name attribute is used
* to reference elements in a JavaScript, or to reference form data after a form is submitted.
*
* @return The name attribute value.
*/
@Override
public String getName() {
return getInputElement().element().name;
}
/**
* Sets the name attribute for the input element.
*
*
The name attribute specifies the name for an `` element. This can be beneficial when
* sending data to the server or referencing it in scripts.
*
* @param name The desired name attribute value.
* @return This instance, to facilitate method chaining.
*/
@Override
public T setName(String name) {
getInputElement().element().name = name;
return (T) this;
}
@FunctionalInterface
public interface NumberFormatSupplier {
NumberFormat get(String pattern);
}
}