Skip to content

Commit

Permalink
feat: add i18n error message support for Select (#6468)
Browse files Browse the repository at this point in the history
  • Loading branch information
vursen authored Jul 30, 2024
1 parent f1a6736 commit c8d1afe
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
public class BasicValidationPage
extends AbstractValidationPage<Select<String>> {
public static final String REQUIRED_BUTTON = "required-button";
public static final String REQUIRED_ERROR_MESSAGE = "Field is required";

public BasicValidationPage() {
testField.setI18n(new Select.SelectI18n()
.setRequiredErrorMessage(REQUIRED_ERROR_MESSAGE));

add(createButton(REQUIRED_BUTTON, "Enable required", event -> {
testField.setRequiredIndicatorVisible(true);
}));
Expand All @@ -28,7 +32,6 @@ protected void validate() {
};
select.setItems(List.of("foo", "bar", "baz"));
select.setEmptySelectionAllowed(true);

return select;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.openqa.selenium.Keys;

import static com.vaadin.flow.component.select.tests.validation.BasicValidationPage.REQUIRED_BUTTON;
import static com.vaadin.flow.component.select.tests.validation.BasicValidationPage.REQUIRED_ERROR_MESSAGE;

@TestPath("vaadin-select/validation/basic")
public class BasicValidationIT extends AbstractValidationIT<SelectElement> {
Expand All @@ -15,6 +16,7 @@ public class BasicValidationIT extends AbstractValidationIT<SelectElement> {
public void fieldIsInitiallyValid() {
assertClientValid();
assertServerValid();
assertErrorMessage(null);
}

@Test
Expand All @@ -23,6 +25,7 @@ public void triggerBlur_assertValidity() {
assertValidationCount(0);
assertServerValid();
assertClientValid();
assertErrorMessage(null);
}

@Test
Expand All @@ -33,6 +36,7 @@ public void required_triggerBlur_assertValidity() {
assertValidationCount(0);
assertServerValid();
assertClientValid();
assertErrorMessage(null);
}

@Test
Expand All @@ -43,11 +47,13 @@ public void required_changeValue_assertValidity() {
assertValidationCount(1);
assertServerValid();
assertClientValid();
assertErrorMessage("");

testField.selectByText("");
assertValidationCount(1);
assertServerInvalid();
assertClientInvalid();
assertErrorMessage(REQUIRED_ERROR_MESSAGE);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
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.ValidationResult;
import com.vaadin.flow.data.provider.DataChangeEvent;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.DataProviderWrapper;
Expand All @@ -65,11 +66,13 @@
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.shared.Registration;

import java.io.Serializable;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Stream;

/**
Expand Down Expand Up @@ -128,8 +131,13 @@ public class Select<T> extends AbstractSinglePropertyField<Select<T>, T>

private SerializableConsumer<UI> sizeRequest;

private SelectI18n i18n;

private boolean manualValidationEnabled = false;

private String customErrorMessage;
private String constraintErrorMessage;

/**
* Constructs a select.
*/
Expand Down Expand Up @@ -208,6 +216,44 @@ public Select(String label,
setItems(items);
}

/**
* Sets an error message to display for all constraint violations.
* <p>
* This error message takes priority over i18n error messages when both are
* set.
*
* @param errorMessage
* the error message to set, or {@code null} to clear
*/
@Override
public void setErrorMessage(String errorMessage) {
customErrorMessage = errorMessage;
updateErrorMessage();
}

/**
* Gets the error message displayed for all constraint violations.
*
* @return the error message
*/
@Override
public String getErrorMessage() {
return customErrorMessage;
}

private void setConstraintErrorMessage(String errorMessage) {
constraintErrorMessage = errorMessage;
updateErrorMessage();
}

private void updateErrorMessage() {
String errorMessage = constraintErrorMessage;
if (customErrorMessage != null && !customErrorMessage.isEmpty()) {
errorMessage = customErrorMessage;
}
getElement().setProperty("errorMessage", errorMessage);
}

private static <T> T presentationToModel(Select<T> select,
String presentation) {
if (select.keyMapper == null) {
Expand Down Expand Up @@ -1072,13 +1118,29 @@ public void setManualValidation(boolean enabled) {
this.manualValidationEnabled = enabled;
}

/**
* Validates the current value against the constraints and sets the
* {@code invalid} property and the {@code errorMessage} property based on
* the result. If a custom error message is provided with
* {@link #setErrorMessage(String)}, it is used. Otherwise, the error
* message defined in the i18n object is used.
* <p>
* The method does nothing if the manual validation mode is enabled.
*/
protected void validate() {
if (!this.manualValidationEnabled) {
boolean isRequired = this.isRequiredIndicatorVisible();
boolean isInvalid = ValidationUtil.validateRequiredConstraint("",
isRequired, getValue(), getEmptyValue()).isError();
if (this.manualValidationEnabled) {
return;
}

setInvalid(isInvalid);
ValidationResult result = ValidationUtil.validateRequiredConstraint(
getI18nErrorMessage(SelectI18n::getRequiredErrorMessage),
isRequiredIndicatorVisible(), getValue(), getEmptyValue());
if (result.isError()) {
setInvalid(true);
setConstraintErrorMessage(result.getErrorMessage());
} else {
setInvalid(false);
setConstraintErrorMessage("");
}
}

Expand Down Expand Up @@ -1141,4 +1203,69 @@ protected Registration addInvalidChangeListener(
return addListener(InvalidChangeEvent.class, listener);
}

/**
* Gets the internationalization object previously set for this component.
* <p>
* NOTE: Updating the instance that is returned from this method will not
* update the component if not set again using {@link #setI18n(SelectI18n)}
*
* @return the i18n object or {@code null} if no i18n object has been set
*/
public SelectI18n getI18n() {
return i18n;
}

/**
* Sets the internationalization object for this component.
*
* @param i18n
* the i18n object, not {@code null}
*/
public void setI18n(SelectI18n i18n) {
this.i18n = Objects.requireNonNull(i18n,
"The i18n properties object should not be null");
}

private String getI18nErrorMessage(Function<SelectI18n, String> getter) {
return Optional.ofNullable(i18n).map(getter).orElse("");
}

/**
* The internationalization properties for {@link Select}.
*/
public static class SelectI18n implements Serializable {

private String requiredErrorMessage;

/**
* Gets the error message displayed when the field is required but
* empty.
*
* @return the error message or {@code null} if not set
* @see Select#isRequiredIndicatorVisible()
* @see Select#setRequiredIndicatorVisible(boolean)
*/
public String getRequiredErrorMessage() {
return requiredErrorMessage;
}

/**
* Sets the error message to display when the field is required but
* empty.
* <p>
* Note, custom error messages set with
* {@link Select#setErrorMessage(String)} take priority over i18n error
* messages.
*
* @param errorMessage
* the error message or {@code null} to clear it
* @return this instance for method chaining
* @see Select#isRequiredIndicatorVisible()
* @see Select#setRequiredIndicatorVisible(boolean)
*/
public SelectI18n setRequiredErrorMessage(String errorMessage) {
requiredErrorMessage = errorMessage;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,63 @@
*/
package com.vaadin.flow.component.select.validation;

import org.junit.Assert;
import org.junit.Test;

import com.vaadin.flow.component.select.Select;
import com.vaadin.tests.validation.AbstractBasicValidationTest;

public class BasicValidationTest
extends AbstractBasicValidationTest<Select<String>, String> {
@Test
public void required_validate_emptyErrorMessageDisplayed() {
testField.setRequiredIndicatorVisible(true);
testField.setValue("foo");
testField.setValue(null);
Assert.assertEquals("", getErrorMessageProperty());
}

@Test
public void required_setI18nErrorMessage_validate_i18nErrorMessageDisplayed() {
testField.setRequiredIndicatorVisible(true);
testField.setI18n(new Select.SelectI18n()
.setRequiredErrorMessage("Field is required"));
testField.setValue("foo");
testField.setValue(null);
Assert.assertEquals("Field is required", getErrorMessageProperty());
}

@Test
public void setI18nAndCustomErrorMessage_validate_customErrorMessageDisplayed() {
testField.setRequiredIndicatorVisible(true);
testField.setI18n(new Select.SelectI18n()
.setRequiredErrorMessage("Field is required"));
testField.setErrorMessage("Custom error message");
testField.setValue("foo");
testField.setValue(null);
Assert.assertEquals("Custom error message", getErrorMessageProperty());
}

@Test
public void setI18nAndCustomErrorMessage_validate_removeCustomErrorMessage_i18nErrorMessageDisplayed() {
testField.setRequiredIndicatorVisible(true);
testField.setI18n(new Select.SelectI18n()
.setRequiredErrorMessage("Field is required"));
testField.setErrorMessage("Custom error message");
testField.setValue("foo");
testField.setValue(null);
testField.setErrorMessage("");
Assert.assertEquals("Field is required", getErrorMessageProperty());
}

@Override
protected Select<String> createTestField() {
return new Select<String>();
Select<String> select = new Select<>();
select.setItems("foo");
return select;
}

private String getErrorMessageProperty() {
return testField.getElement().getProperty("errorMessage");
}
}

0 comments on commit c8d1afe

Please sign in to comment.