> builderConsumer = (propertyName, builder) -> {
+ if (propertyName.equals("file")) {
+ builder.with("accept", "image/png, image/jpeg");
+ }
+ if (propertyName.equals("alt")) {
+ builder.with("label", Message.of("Alternative description of the image"));
+ }
+ };
+ String expectedClass = """
+ """;
+ Form form = formGenerator.generate("/foo/bar", "post", EventImageSave.class, builderConsumer);
+ assertEquals(expectedClass, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", "post", EventImageSave.class, FormGenerator.SUBMIT, builderConsumer);
+ assertEquals(expectedClass, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", "post", EventImageSave.class, new InputSubmitFormElement(FormGenerator.SUBMIT), builderConsumer);
+ assertEquals(expectedClass, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", EventImageSave.class, builderConsumer);
+ assertEquals(expectedClass, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", EventImageSave.class, FormGenerator.SUBMIT, builderConsumer);
+ assertEquals(expectedClass, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", EventImageSave.class, new InputSubmitFormElement(FormGenerator.SUBMIT), builderConsumer);
+ assertEquals(expectedClass, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ EventImageSave invalid = new EventImageSave("xxx", "", null);
+ ConstraintViolationException ex = assertThrows(ConstraintViolationException.class, () -> validator.validate(invalid));
+ String expectedInvalid = """
+ """;
+ form = formGenerator.generate("/foo/bar", "post", invalid, ex, builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", "post", invalid, ex, FormGenerator.SUBMIT, builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", "post", invalid, ex, new InputSubmitFormElement(FormGenerator.SUBMIT), builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", invalid, ex, new InputSubmitFormElement(FormGenerator.SUBMIT), builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", invalid, ex, builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", invalid, ex, FormGenerator.SUBMIT, builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", invalid, ex, new InputSubmitFormElement(FormGenerator.SUBMIT), builderConsumer);
+ assertEquals(expectedInvalid, TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ EventImageSave valid = new EventImageSave("xxx", "Micronaut Logo", null);
+ String expectedValid = """
+ """;
+ form = formGenerator.generate("/foo/bar", "post", valid, builderConsumer);
+ assertEquals(expectedValid,
+ TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", valid, builderConsumer);
+ assertEquals(expectedValid,
+ TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", valid, FormGenerator.SUBMIT, builderConsumer);
+ assertEquals(expectedValid,
+ TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+
+ form = formGenerator.generate("/foo/bar", valid, new InputSubmitFormElement(FormGenerator.SUBMIT), builderConsumer);
+ assertEquals(expectedValid,
+ TestUtils.render(viewName, viewsRenderer, Map.of("form", form)));
+ }
+
+ @Introspected
+ record EventImageSave(@NotBlank @InputHidden String id,
+ @NotBlank String alt,
+ @NotNull CompletedFileUpload file) {
+ }
+
+ @Requires(property = "spec.name", value = "FormCompletedFileUploadRenderTest")
+ @Singleton
+ static class EventImageSaveValidator {
+ void validate(@Valid EventImageSave eventImageSave) {
+ }
+ }
+
+}
diff --git a/views-fieldset/build.gradle.kts b/views-fieldset/build.gradle.kts
index fa16e9d13..1a5373c37 100644
--- a/views-fieldset/build.gradle.kts
+++ b/views-fieldset/build.gradle.kts
@@ -5,13 +5,13 @@ plugins {
dependencies {
annotationProcessor(mnValidation.micronaut.validation.processor)
implementation(mnValidation.micronaut.validation)
+ compileOnly(mn.micronaut.http)
testAnnotationProcessor(mnValidation.micronaut.validation.processor)
testImplementation(mnValidation.micronaut.validation)
compileOnly(projects.micronautViewsCore)
-
testImplementation(mnData.micronaut.data.model)
-
+ testImplementation(mn.micronaut.http)
testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(libs.junit.jupiter.api)
testImplementation(mnTest.micronaut.test.junit5)
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFieldGenerator.java b/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFieldGenerator.java
index 1cb19bc7d..8dfbd3006 100644
--- a/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFieldGenerator.java
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFieldGenerator.java
@@ -25,17 +25,11 @@
import io.micronaut.core.beans.BeanWrapper;
import io.micronaut.core.util.StringUtils;
import io.micronaut.views.fields.annotations.InputCheckbox;
-import io.micronaut.views.fields.annotations.InputEmail;
-import io.micronaut.views.fields.annotations.InputHidden;
-import io.micronaut.views.fields.annotations.InputPassword;
import io.micronaut.views.fields.annotations.InputRadio;
-import io.micronaut.views.fields.annotations.InputTel;
-import io.micronaut.views.fields.annotations.InputUrl;
import io.micronaut.views.fields.annotations.Select;
-import io.micronaut.views.fields.annotations.Textarea;
-import io.micronaut.views.fields.annotations.TrixEditor;
import io.micronaut.views.fields.elements.*;
import io.micronaut.views.fields.fetchers.*;
+import io.micronaut.views.fields.formelementresolvers.FormElementResolver;
import io.micronaut.views.fields.messages.ConstraintViolationUtils;
import io.micronaut.views.fields.messages.Message;
import jakarta.annotation.Nonnull;
@@ -44,9 +38,6 @@
import jakarta.validation.constraints.*;
import java.lang.annotation.Annotation;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
@@ -75,23 +66,8 @@ public class DefaultFieldGenerator implements FieldsetGenerator {
private static final String BUILDER_METHOD_MAX = "max";
private static final String BUILDER_METHOD_MAX_LENGTH = "maxLength";
private static final String BUILDER_METHOD_MIN_LENGTH = "minLength";
-
- private static final Map, Class extends FormElement>> ANNOTATION_MAPPING = Map.ofEntries(
- Map.entry(InputHidden.class, InputHiddenFormElement.class),
- Map.entry(InputRadio.class, InputRadioFormElement.class),
- Map.entry(InputCheckbox.class, InputCheckboxFormElement.class),
- Map.entry(InputPassword.class, InputPasswordFormElement.class),
- Map.entry(InputEmail.class, InputEmailFormElement.class),
- Map.entry(Email.class, InputEmailFormElement.class),
- Map.entry(InputUrl.class, InputUrlFormElement.class),
- Map.entry(InputTel.class, InputTelFormElement.class),
- Map.entry(Select.class, SelectFormElement.class),
- Map.entry(Textarea.class, TextareaFormElement.class),
- Map.entry(TrixEditor.class, TrixEditorFormElement.class)
- );
private static final String MEMBER_MIN = "min";
private static final String MEMBER_MAX = "max";
- private static final String CLASS_IO_MICRONAUT_DATA_ANNOTATION_AUTO_POPULATED = "io.micronaut.data.annotation.AutoPopulated";
private final EnumOptionFetcher> enumOptionFetcher;
@@ -100,6 +76,8 @@ public class DefaultFieldGenerator implements FieldsetGenerator {
private final EnumCheckboxFetcher> enumCheckboxFetcher;
private final BeanContext beanContext;
+ private final FormElementResolver formElementResolver;
+
private final ConcurrentHashMap, OptionFetcher> optionFetcherCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap, RadioFetcher> radioFetcherCache = new ConcurrentHashMap<>();
@@ -110,15 +88,18 @@ public class DefaultFieldGenerator implements FieldsetGenerator {
* @param enumRadioFetcher Enum fetcher for {@link Radio}.
* @param enumCheckboxFetcher Enum fetcher for {@link Checkbox}.
* @param beanContext Bean Context
+ * @param formElementResolver Primary Form Element Resolver. {@link io.micronaut.views.fields.formelementresolvers.CompositeFormElementResolver}.
*/
public DefaultFieldGenerator(EnumOptionFetcher> enumOptionFetcher,
EnumRadioFetcher> enumRadioFetcher,
EnumCheckboxFetcher> enumCheckboxFetcher,
- BeanContext beanContext) {
+ BeanContext beanContext,
+ FormElementResolver formElementResolver) {
this.enumOptionFetcher = enumOptionFetcher;
this.enumRadioFetcher = enumRadioFetcher;
this.enumCheckboxFetcher = enumCheckboxFetcher;
this.beanContext = beanContext;
+ this.formElementResolver = formElementResolver;
}
@Override
@@ -173,7 +154,7 @@ private List extends FormElement> generateOfBeanWrapper(@NonNull BeanWrapp
@Nullable BiConsumer> builderConsumer) {
List result = new ArrayList<>(beanWrapper.getBeanProperties().size());
for (BeanProperty beanProperty : beanWrapper.getBeanProperties()) {
- formElementClassForBeanProperty(beanProperty).ifPresent(formElementClazz -> {
+ formElementResolver.resolve(beanProperty).ifPresent(formElementClazz -> {
BeanIntrospection.Builder extends FormElement> builder = formElementBuilderForBeanProperty(beanProperty, formElementClazz, beanWrapper, ex, builderConsumer);
result.add(builder.build());
});
@@ -181,41 +162,6 @@ private List extends FormElement> generateOfBeanWrapper(@NonNull BeanWrapp
return result;
}
- @NonNull
- private Optional> formElementClassForBeanProperty(@NonNull BeanProperty beanProperty) {
- if (beanProperty.hasStereotype(CLASS_IO_MICRONAUT_DATA_ANNOTATION_AUTO_POPULATED)) {
- return Optional.empty();
- }
- for (var mapping : ANNOTATION_MAPPING.entrySet()) {
- if (beanProperty.hasAnnotation(mapping.getKey())) {
- return Optional.of(mapping.getValue());
- }
- }
- if (beanProperty.getType() == LocalDate.class) {
- return Optional.of(InputDateFormElement.class);
- }
- if (beanProperty.getType() == LocalDateTime.class) {
- return Optional.of(InputDateTimeLocalFormElement.class);
- }
- if (beanProperty.getType() == LocalTime.class) {
- return Optional.of(InputTimeFormElement.class);
- }
- if (Number.class.isAssignableFrom(beanProperty.getType())) {
- return Optional.of(InputNumberFormElement.class);
- }
- if (beanProperty.getType() == boolean.class) {
- return Optional.of(InputCheckboxFormElement.class);
- }
- if (beanProperty.getType().isEnum()) {
- return Optional.of(SelectFormElement.class);
- }
- if (CharSequence.class.isAssignableFrom(beanProperty.getType())) {
- return Optional.of(InputTextFormElement.class);
- }
-
- return Optional.empty();
- }
-
@NonNull
private Optional radioFetcherForBeanProperty(@NonNull BeanProperty beanProperty) {
if (beanProperty.hasAnnotation(InputRadio.class)) {
@@ -409,7 +355,7 @@ private List extends FormElement> formElements(Collection formElementOfBeanProperty(@NonNull BeanProperty, ?> beanProperty, @Nullable BiConsumer> builderConsumer) {
- return formElementClassForBeanProperty(beanProperty)
+ return formElementResolver.resolve(beanProperty)
.map(formElementClass -> formElementBuilderForBeanProperty(beanProperty, formElementClass, null, null, builderConsumer).build());
}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFormGenerator.java b/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFormGenerator.java
index a92288f4f..073255909 100644
--- a/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFormGenerator.java
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/DefaultFormGenerator.java
@@ -17,6 +17,8 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.beans.BeanIntrospection;
+import io.micronaut.views.fields.elements.InputFileFormElement;
import io.micronaut.views.fields.elements.InputSubmitFormElement;
import jakarta.inject.Singleton;
import jakarta.validation.ConstraintViolationException;
@@ -25,6 +27,8 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
/**
* {@link io.micronaut.context.annotation.DefaultImplementation} of {@link FormGenerator}.
@@ -54,11 +58,24 @@ public Form generateWithFieldset(@NonNull String action,
}
@Override
- public Form generate(String action, String method, Object instance, InputSubmitFormElement inputSubmitFormElement) {
+ public Form generate(@NonNull @NotBlank String action,
+ @NonNull @NotBlank String method,
+ @NonNull Object instance,
+ @NonNull @NotNull InputSubmitFormElement inputSubmitFormElement) {
Fieldset fieldset = fieldsetGenerator.generate(instance);
return generate(action, method, fieldset, inputSubmitFormElement);
}
+ @Override
+ public Form generate(@NonNull @NotBlank String action,
+ @NonNull @NotBlank String method,
+ @NonNull Object instance,
+ @NonNull @NotNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer) {
+ Fieldset fieldset = fieldsetGenerator.generate(instance, builderConsumer);
+ return generate(action, method, fieldset, inputSubmitFormElement);
+ }
+
@Override
public Form generate(@NonNull String action,
@NonNull String method,
@@ -69,6 +86,17 @@ public Form generate(@NonNull String action,
return generate(action, method, fieldset, inputSubmitFormElement);
}
+ @Override
+ public Form generate(@NonNull @NotBlank String action,
+ @NonNull @NotBlank String method,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull @NotNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer) {
+ Fieldset fieldset = fieldsetGenerator.generate(instance, ex, builderConsumer);
+ return generate(action, method, fieldset, inputSubmitFormElement);
+ }
+
@Override
public Form generate(@NonNull @NotBlank String action,
@NonNull @NotBlank String method,
@@ -78,12 +106,31 @@ public Form generate(@NonNull @NotBlank String action,
return generate(action, method, fieldset, inputSubmitFormElement);
}
- private Form generate(@NonNull @NotBlank String action,
+ @Override
+ public Form generate(@NonNull @NotBlank String action,
@NonNull @NotBlank String method,
- @NonNull @NotNull Fieldset fieldset,
- @NonNull @NotNull InputSubmitFormElement inputSubmitFormElement) {
+ @NonNull @NotNull Class type,
+ @NonNull @NotNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer) {
+ Fieldset fieldset = fieldsetGenerator.generate(type, builderConsumer);
+ return generate(action, method, fieldset, inputSubmitFormElement);
+ }
+
+ private Form generate(@NonNull @NotBlank String action,
+ @NonNull @NotBlank String method,
+ @NonNull @NotNull Fieldset fieldset,
+ @NonNull @NotNull InputSubmitFormElement inputSubmitFormElement) {
List fields = new ArrayList<>(fieldset.fields());
fields.add(inputSubmitFormElement);
- return new Form(action, method, new Fieldset(fields, fieldset.errors()));
+ return enctype(fieldset)
+ .map(enctype -> new Form(action, method, new Fieldset(fields, fieldset.errors()), enctype))
+ .orElseGet(() -> new Form(action, method, new Fieldset(fields, fieldset.errors())));
+ }
+
+ @NonNull
+ private Optional enctype(@NonNull Fieldset fieldset) {
+ return fieldset.fields().stream().anyMatch(InputFileFormElement.class::isInstance)
+ ? Optional.of("multipart/form-data")
+ : Optional.empty();
}
}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/FormGenerator.java b/views-fieldset/src/main/java/io/micronaut/views/fields/FormGenerator.java
index e0bd023ff..ad11a433c 100644
--- a/views-fieldset/src/main/java/io/micronaut/views/fields/FormGenerator.java
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/FormGenerator.java
@@ -17,10 +17,13 @@
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.views.fields.elements.InputSubmitFormElement;
import io.micronaut.views.fields.messages.Message;
import jakarta.validation.ConstraintViolationException;
+import java.util.function.BiConsumer;
+
/**
* Generates a {@link Form} for a given type representing a form class.
* @author Sergio del Amo
@@ -358,4 +361,323 @@ default Form generate(@NonNull String action,
@NonNull InputSubmitFormElement inputSubmitFormElement) {
return generate(action, POST, type, inputSubmitFormElement);
}
+
+
+ /**
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Object instance,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, method, instance, SUBMIT, builderConsumer);
+ }
+
+ /**
+ * Generate FORM Post.
+ * @param action Form action attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Object instance,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, instance, SUBMIT, builderConsumer);
+ }
+
+ /**
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param submitValue input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Object instance,
+ @NonNull Message submitValue,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, method, instance, new InputSubmitFormElement(submitValue), builderConsumer);
+ }
+
+ /**
+ * Generate FORM Post.
+ * @param action Form action attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param submitValue input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Object instance,
+ @NonNull Message submitValue,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, instance, new InputSubmitFormElement(submitValue), builderConsumer);
+ }
+
+ /**
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param inputSubmitFormElement input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Object instance,
+ @NonNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer);
+
+ /**
+ * Generate FORM Post.
+ * @param action Form action attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param inputSubmitFormElement input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Object instance,
+ @NonNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, POST, instance, inputSubmitFormElement, builderConsumer);
+ }
+
+ /**
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param ex A Validation exception
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, method, instance, ex, SUBMIT, builderConsumer);
+ }
+
+ /**
+ * Generate POST form.
+ * @param action Form action attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param ex A Validation exception
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, instance, ex, SUBMIT, builderConsumer);
+ }
+
+ /**
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param ex A Validation exception
+ * @param submitValue input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull Message submitValue,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, method, instance, ex, new InputSubmitFormElement(submitValue), builderConsumer);
+ }
+
+ /**
+ * Generate POST form.
+ * @param action Form action attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param ex A Validation exception
+ * @param submitValue input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull Message submitValue,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, instance, ex, new InputSubmitFormElement(submitValue), builderConsumer);
+ }
+
+ /**
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param ex A Validation exception
+ * @param inputSubmitFormElement input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer);
+
+ /**
+ * Generate POST form.
+ * @param action Form action attribute
+ * @param instance The Object instance which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param ex A Validation exception
+ * @param inputSubmitFormElement input submit
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Object instance,
+ @NonNull ConstraintViolationException ex,
+ @NonNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, POST, instance, ex, inputSubmitFormElement, builderConsumer);
+ }
+
+ /**
+ *
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param type A class which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param type
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Class type,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, method, type, SUBMIT, builderConsumer);
+ }
+
+ /**
+ * Generate POST Form.
+ * @param action Form action attribute
+ * @param type A class which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param type
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Class type,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, type, SUBMIT, builderConsumer);
+ }
+
+ /**
+ *
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param type A class which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param submitValue input submit
+ * @param type
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Class type,
+ @NonNull Message submitValue,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, method, type, new InputSubmitFormElement(submitValue), builderConsumer);
+ }
+
+ /**
+ * Generate a POST Form.
+ * @param action Form action attribute
+ * @param type A class which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param submitValue input submit
+ * @param type
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Class type,
+ @NonNull Message submitValue,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, type, new InputSubmitFormElement(submitValue), builderConsumer);
+ }
+
+ /**
+ *
+ * @param action Form action attribute
+ * @param method Form method attribute
+ * @param type A class which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param inputSubmitFormElement input submit
+ * @param type
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ Form generate(@NonNull String action,
+ @NonNull String method,
+ @NonNull Class type,
+ @NonNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer);
+
+ /**
+ * Generate a POST form.
+ * @param action Form action attribute
+ * @param type A class which should be {@link io.micronaut.core.annotation.Introspected}.
+ * @param inputSubmitFormElement input submit
+ * @param type
+ * @param builderConsumer A BiConsumer with the property name and the builder. It allows to consume a form element builder while the form fieldset is being generated.
+ * @return A Form
+ * @since 5.1.0
+ */
+ @NonNull
+ default Form generate(@NonNull String action,
+ @NonNull Class type,
+ @NonNull InputSubmitFormElement inputSubmitFormElement,
+ @NonNull BiConsumer> builderConsumer) {
+ return generate(action, POST, type, inputSubmitFormElement, builderConsumer);
+ }
}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/InputType.java b/views-fieldset/src/main/java/io/micronaut/views/fields/InputType.java
index 6cea3fd62..454e42544 100644
--- a/views-fieldset/src/main/java/io/micronaut/views/fields/InputType.java
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/InputType.java
@@ -69,6 +69,11 @@ public enum InputType {
*/
EMAIL("email"),
+ /**
+ * HTML Input type file.
+ */
+ FILE("file"),
+
/**
* HTML Input type password.
*/
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/constraints/package-info.java b/views-fieldset/src/main/java/io/micronaut/views/fields/constraints/package-info.java
index d938ff311..5139c8eb8 100644
--- a/views-fieldset/src/main/java/io/micronaut/views/fields/constraints/package-info.java
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/constraints/package-info.java
@@ -18,4 +18,4 @@
* @author Sergio del Amo
* @since 5.1.0
*/
-package io.micronaut.views.fields.constraints;
\ No newline at end of file
+package io.micronaut.views.fields.constraints;
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/elements/InputFileFormElement.java b/views-fieldset/src/main/java/io/micronaut/views/fields/elements/InputFileFormElement.java
new file mode 100644
index 000000000..b3c6363ea
--- /dev/null
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/elements/InputFileFormElement.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * 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
+ *
+ * https://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 io.micronaut.views.fields.elements;
+
+import io.micronaut.core.annotation.Experimental;
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.views.fields.InputType;
+import io.micronaut.views.fields.messages.Message;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Input File.
+ * @see Input File
+ * @param name Name of the form control. Submitted with the form as part of a name/value pair
+ * @param id It defines an identifier (ID) which must be unique in the whole document
+ * @param accept It defines the file types the file input should accept.
+ * @param required If true indicates that the user must specify a value for the input before the owning form can be submitted.
+ * @param label the input label
+ * @param errors errors associated with this input
+ *
+ * @author Sergio del Amo
+ * @since 4.1.0
+ */
+@Experimental
+@Introspected(builder = @Introspected.IntrospectionBuilder(builderClass = InputFileFormElement.Builder.class))
+public record InputFileFormElement(@NonNull String name,
+ @Nullable String id,
+ @Nullable String accept,
+ boolean required,
+ @Nullable Message label,
+ @NonNull List errors) implements InputFormElement, GlobalAttributes, FormElementAttributes {
+ @Override
+ @NonNull
+ public InputType getType() {
+ return InputType.FILE;
+ }
+
+ /**
+ *
+ * @return Input Text builder
+ */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Input Text Builder.
+ */
+ public static final class Builder {
+ /**
+ *
+ * Name of the form control. Submitted with the form as part of a name/value pair.
+ */
+ private String name;
+
+ /**
+ *
+ * It defines an identifier (ID) which must be unique in the whole document.
+ */
+ private String id;
+
+ /**
+ *
+ * The accept attribute defines the file types the file input should accept.
+ */
+ private String accept;
+
+ /**
+ *
+ * If true indicates that the user must specify a value for the input before the owning form can be submitted.
+ */
+ private boolean required;
+
+ /**
+ *
+ * Input Errors.
+ */
+ private List errors;
+
+ /**
+ *
+ * message for an HTML Label element.
+ */
+ private Message label;
+
+ /**
+ *
+ * @param accept The accept attribute defines the file types the file input should accept.
+ * @return The Builder
+ */
+ @NonNull
+ public Builder accept(@NonNull String accept) {
+ this.accept = accept;
+ return this;
+ }
+
+ /**
+ *
+ * @param required If true indicates that the user must specify a value for the input before the owning form can be submitted.
+ * @return the Builder
+ */
+ @NonNull
+ public Builder required(boolean required) {
+ this.required = required;
+ return this;
+ }
+
+ /**
+ *
+ * @param name Name of the form control. Submitted with the form as part of a name/value pair
+ * @return the Builder
+ */
+ @NonNull
+ public Builder name(@NonNull String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ *
+ * @param id It defines an identifier (ID) which must be unique in the whole document
+ * @return The Builder
+ */
+ @NonNull
+ public Builder id(@NonNull String id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ *
+ * @param label represents a caption for an item in a user interface
+ * @return the Builder
+ */
+ @NonNull
+ public Builder label(Message label) {
+ this.label = label;
+ return this;
+ }
+
+ /**
+ *
+ * @param errors Form element validation Errors.
+ * @return The builder
+ */
+ @NonNull
+ public Builder errors(@NonNull List errors) {
+ this.errors = errors;
+ return this;
+ }
+
+ /**
+
+ /**
+ *
+ * @return Creates a {@link InputFileFormElement}.
+ */
+ @NonNull
+ public InputFileFormElement build() {
+ return new InputFileFormElement(name, id, accept, required, label, errors == null ? Collections.emptyList() : errors);
+ }
+ }
+}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/CompletedFileUploadFormElementResolver.java b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/CompletedFileUploadFormElementResolver.java
new file mode 100644
index 000000000..70899a40c
--- /dev/null
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/CompletedFileUploadFormElementResolver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * 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
+ *
+ * https://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 io.micronaut.views.fields.formelementresolvers;
+
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.core.beans.BeanProperty;
+import io.micronaut.http.multipart.CompletedFileUpload;
+import io.micronaut.views.fields.FormElement;
+import io.micronaut.views.fields.elements.InputFileFormElement;
+import jakarta.inject.Singleton;
+
+import java.util.Optional;
+
+/**
+ * Resolves a form element of type {@link InputFileFormElement} if the bean property is {@link CompletedFileUpload}.
+ * @author Sergio del Amo
+ * @since 5.1.0
+ */
+@Requires(classes = CompletedFileUpload.class)
+@Singleton
+public class CompletedFileUploadFormElementResolver implements FormElementResolver {
+ @Override
+ public Optional> resolve(BeanProperty beanProperty) {
+ return beanProperty.getType() == CompletedFileUpload.class
+ ? Optional.of(InputFileFormElement.class)
+ : Optional.empty();
+ }
+
+ @Override
+ public int getOrder() {
+ return DefaultFormElementResolver.ORDER + 10;
+ }
+}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/CompositeFormElementResolver.java b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/CompositeFormElementResolver.java
new file mode 100644
index 000000000..8c7a5f1a1
--- /dev/null
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/CompositeFormElementResolver.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * 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
+ *
+ * https://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 io.micronaut.views.fields.formelementresolvers;
+
+import io.micronaut.context.annotation.Primary;
+import io.micronaut.core.annotation.Experimental;
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.beans.BeanProperty;
+import io.micronaut.core.order.Ordered;
+import io.micronaut.views.fields.FormElement;
+import jakarta.inject.Singleton;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Composite Pattern implementaiton for the {@link FormElementResolver} API.
+ * This instance is injected when injecting a single bean of type {@link FormElementResolver} because of the {@link Primary} annotation.
+ * This implementation iterates through every bean of type {@link FormElementResolver} in order and returns the first resolved {@link FormElement} class if any.
+ * @author Sergio del Amo
+ * @since 5.1.0
+ */
+@Experimental
+@Primary
+@Singleton
+@Internal
+public class CompositeFormElementResolver implements FormElementResolver {
+
+ private final List formElementResolvers;
+
+ /**
+ *
+ * @param formElementResolvers Beans of type {@link FormElementResolver} in order.
+ */
+ public CompositeFormElementResolver(List formElementResolvers) {
+ this.formElementResolvers = formElementResolvers;
+ }
+
+ @Override
+ public Optional> resolve(BeanProperty beanProperty) {
+ return formElementResolvers.stream()
+ .map(resolver -> resolver.resolve(beanProperty))
+ .flatMap(Optional::stream)
+ .findFirst();
+ }
+
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/DefaultFormElementResolver.java b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/DefaultFormElementResolver.java
new file mode 100644
index 000000000..c2eebe732
--- /dev/null
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/DefaultFormElementResolver.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * 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
+ *
+ * https://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 io.micronaut.views.fields.formelementresolvers;
+
+import io.micronaut.core.beans.BeanProperty;
+import io.micronaut.views.fields.FormElement;
+import io.micronaut.views.fields.annotations.*;
+import io.micronaut.views.fields.elements.*;
+import jakarta.inject.Singleton;
+import jakarta.validation.constraints.Email;
+
+import java.lang.annotation.Annotation;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Default implementation of the {@link FormElementResolver} API.
+ * @author Sergio del Amo
+ * @since 5.1.0
+ */
+@Singleton
+public class DefaultFormElementResolver implements FormElementResolver {
+ public static final int ORDER = 0;
+ private static final String CLASS_IO_MICRONAUT_DATA_ANNOTATION_AUTO_POPULATED = "io.micronaut.data.annotation.AutoPopulated";
+ private static final Map, Class extends FormElement>> ANNOTATION_MAPPING = Map.ofEntries(
+ Map.entry(InputHidden.class, InputHiddenFormElement.class),
+ Map.entry(InputRadio.class, InputRadioFormElement.class),
+ Map.entry(InputCheckbox.class, InputCheckboxFormElement.class),
+ Map.entry(InputPassword.class, InputPasswordFormElement.class),
+ Map.entry(InputEmail.class, InputEmailFormElement.class),
+ Map.entry(Email.class, InputEmailFormElement.class),
+ Map.entry(InputUrl.class, InputUrlFormElement.class),
+ Map.entry(InputTel.class, InputTelFormElement.class),
+ Map.entry(Select.class, SelectFormElement.class),
+ Map.entry(Textarea.class, TextareaFormElement.class),
+ Map.entry(TrixEditor.class, TrixEditorFormElement.class)
+ );
+
+ @Override
+ public Optional> resolve(BeanProperty beanProperty) {
+ if (beanProperty.hasStereotype(CLASS_IO_MICRONAUT_DATA_ANNOTATION_AUTO_POPULATED)) {
+ return Optional.empty();
+ }
+ for (var mapping : ANNOTATION_MAPPING.entrySet()) {
+ if (beanProperty.hasAnnotation(mapping.getKey())) {
+ return Optional.of(mapping.getValue());
+ }
+ }
+ if (beanProperty.getType() == LocalDate.class) {
+ return Optional.of(InputDateFormElement.class);
+ }
+ if (beanProperty.getType() == LocalDateTime.class) {
+ return Optional.of(InputDateTimeLocalFormElement.class);
+ }
+ if (beanProperty.getType() == LocalTime.class) {
+ return Optional.of(InputTimeFormElement.class);
+ }
+ if (Number.class.isAssignableFrom(beanProperty.getType())) {
+ return Optional.of(InputNumberFormElement.class);
+ }
+ if (beanProperty.getType() == boolean.class) {
+ return Optional.of(InputCheckboxFormElement.class);
+ }
+ if (beanProperty.getType().isEnum()) {
+ return Optional.of(SelectFormElement.class);
+ }
+ if (CharSequence.class.isAssignableFrom(beanProperty.getType())) {
+ return Optional.of(InputTextFormElement.class);
+ }
+
+ return Optional.empty();
+ }
+
+ @Override
+ public int getOrder() {
+ return ORDER;
+ }
+}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/FormElementResolver.java b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/FormElementResolver.java
new file mode 100644
index 000000000..66a400d09
--- /dev/null
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/FormElementResolver.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * 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
+ *
+ * https://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 io.micronaut.views.fields.formelementresolvers;
+
+import io.micronaut.core.annotation.Indexed;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.beans.BeanProperty;
+import io.micronaut.core.order.Ordered;
+import io.micronaut.views.fields.FormElement;
+
+import java.util.Optional;
+
+/**
+ * Resolves given a bean property the {@link FormElement} class which should be used to be build the element.
+ * @author Sergio del Amo
+ * @since 5.1.0
+ */
+@FunctionalInterface
+@Indexed(FormElementResolver.class)
+public interface FormElementResolver extends Ordered {
+ /**
+ * Resolves given a bean property the {@link FormElement} class which should be used to be build the element.
+ *
+ * @param beanProperty Bean Property
+ * @return The best Form Element for the bean property.
+ * @param The bean type
+ * @param The bean property type
+ */
+ Optional> resolve(@NonNull BeanProperty beanProperty);
+}
diff --git a/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/package-info.java b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/package-info.java
new file mode 100644
index 000000000..4bca2479c
--- /dev/null
+++ b/views-fieldset/src/main/java/io/micronaut/views/fields/formelementresolvers/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+/**
+ * Defines an API to resolve a Form Element class given a bean property.
+ * @author Sergio del Amo
+ * @since 5.1.0
+ */
+package io.micronaut.views.fields.formelementresolvers;
diff --git a/views-fieldset/src/test/java/io/micronaut/views/fields/InputFileFormElementTest.java b/views-fieldset/src/test/java/io/micronaut/views/fields/InputFileFormElementTest.java
new file mode 100644
index 000000000..65b6af55d
--- /dev/null
+++ b/views-fieldset/src/test/java/io/micronaut/views/fields/InputFileFormElementTest.java
@@ -0,0 +1,50 @@
+package io.micronaut.views.fields;
+
+import io.micronaut.core.beans.BeanIntrospection;
+import io.micronaut.views.fields.elements.InputFileFormElement;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class InputFileFormElementTest {
+ private final String ID = "numid";
+ private static final String NAME = "num";
+ private static final boolean REQUIRED = true;
+ private static final String ACCEPT = ".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+
+ @Test
+ void testTagAndType() {
+ InputFileFormElement formElement = InputFileFormElement.builder().build();
+ assertEquals(HtmlTag.INPUT, formElement.getTag());
+ assertEquals(InputType.FILE, formElement.getType());
+ }
+
+ @Test
+ void builder() {
+ InputFileFormElement formElement = InputFileFormElement.builder()
+ .name(NAME)
+ .id(ID)
+ .accept(ACCEPT)
+ .required(REQUIRED)
+ .build();
+ assertFormElement(formElement);
+ BeanIntrospection introspection = BeanIntrospection.getIntrospection(InputFileFormElement.class);
+ BeanIntrospection.Builder builder = introspection.builder();
+ formElement = builder
+ .with("name", NAME)
+ .with("id", ID)
+ .with("accept", ACCEPT)
+ .with("required", REQUIRED)
+ .build();
+ assertFormElement(formElement);
+ assertFalse(formElement.hasErrors());
+ }
+
+ private void assertFormElement(InputFileFormElement formElement) {
+ assertNotNull(formElement);
+ assertEquals(ID, formElement.id());
+ assertEquals(NAME, formElement.name());
+ assertEquals(REQUIRED, formElement.required());
+ assertEquals(ACCEPT, formElement.accept());
+ }
+}
\ No newline at end of file
diff --git a/views-fieldset/src/test/java/io/micronaut/views/fields/formelementresolvers/FormElementResolverTest.java b/views-fieldset/src/test/java/io/micronaut/views/fields/formelementresolvers/FormElementResolverTest.java
new file mode 100644
index 000000000..4ed1afe61
--- /dev/null
+++ b/views-fieldset/src/test/java/io/micronaut/views/fields/formelementresolvers/FormElementResolverTest.java
@@ -0,0 +1,27 @@
+package io.micronaut.views.fields.formelementresolvers;
+
+import io.micronaut.context.BeanContext;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@MicronautTest(startApplication = false)
+class FormElementResolverTest {
+
+ @Inject
+ BeanContext beanContext;
+
+ @Test
+ void defaultFormElementResolverFirst() {
+ List resolvers = new ArrayList<>(beanContext.getBeansOfType(FormElementResolver.class));
+ assertEquals(3, resolvers.size());
+ assertTrue(resolvers.get(0) instanceof CompositeFormElementResolver);
+ assertTrue(resolvers.get(1) instanceof DefaultFormElementResolver);
+ assertTrue(resolvers.get(2) instanceof CompletedFileUploadFormElementResolver);
+ }
+}