-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
1,084 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
You many need a custom implementation of api:views.fields.FormElement[]. If you do, create a bean of type api:views.fields.formelementresolvers.FormElementResolver[] to resolve your custom `FormElement` class for a certain bean property's type. | ||
|
||
Providing a bean of type api:views.fields.formelementresolvers.FormElementResolver[] allows the custom form element to be part of the api:views.fields.FieldsetGenerator[] API. |
2 changes: 1 addition & 1 deletion
2
test-suite-thymeleaf-fieldset/src/test/resources/views/fieldset/fieldset.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<th:block th:each="field : ${el.fields()}" th:fragment="fieldset(el)" xmlns:th="http://www.thymeleaf.org"><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'hidden'}"><input th:replace="~{fieldset/inputhidden :: inputhidden(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'password'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('password',${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'textarea'}"><div class="mb-3" th:insert="~{fieldset/textarea :: textarea(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'number'}"><div class="mb-3" th:insert="~{fieldset/inputnumber :: inputnumber(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'submit'}"><div th:replace="~{fieldset/inputsubmit :: inputsubmit(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'datetime-local'}"><div class="mb-3" th:insert="~{fieldset/inputdatetimelocal :: inputdatetimelocal(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'date'}"><div class="mb-3" th:insert="~{fieldset/inputdate :: inputdate(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'time'}"><div class="mb-3" th:insert="~{fieldset/inputtime :: inputtime(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'text'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('text', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'email'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('email', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'tel'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('tel', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'url'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('url', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'radio'}"><div class="mb-3" th:insert="~{fieldset/inputradios :: inputradios(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'checkbox'}"><div class="mb-3" th:insert="~{fieldset/inputcheckbox :: inputcheckbox(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'select'}"><div class="mb-3" th:insert="~{fieldset/select :: select(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'trix-editor'}"><div class="mb-3" th:if="${el.hasErrors()}" th:insert="~{fieldset/errors :: errors(${el.errors()})}"></div><div class="mb-3" th:insert="~{fieldset/trixeditor :: trixeditor(${field})}"></div></th:block></th:block> | ||
<th:block th:each="field : ${el.fields()}" th:fragment="fieldset(el)" xmlns:th="http://www.thymeleaf.org"><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'file'}"><input th:replace="~{fieldset/inputfile :: inputfile(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'hidden'}"><input th:replace="~{fieldset/inputhidden :: inputhidden(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'password'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('password',${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'textarea'}"><div class="mb-3" th:insert="~{fieldset/textarea :: textarea(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'number'}"><div class="mb-3" th:insert="~{fieldset/inputnumber :: inputnumber(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'submit'}"><div th:replace="~{fieldset/inputsubmit :: inputsubmit(${field})}"/></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'datetime-local'}"><div class="mb-3" th:insert="~{fieldset/inputdatetimelocal :: inputdatetimelocal(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'date'}"><div class="mb-3" th:insert="~{fieldset/inputdate :: inputdate(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'time'}"><div class="mb-3" th:insert="~{fieldset/inputtime :: inputtime(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'text'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('text', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'email'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('email', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'tel'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('tel', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'url'}"><div class="mb-3" th:insert="~{fieldset/inputstring :: inputstring('url', ${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'radio'}"><div class="mb-3" th:insert="~{fieldset/inputradios :: inputradios(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'input' && field.type.toString() == 'checkbox'}"><div class="mb-3" th:insert="~{fieldset/inputcheckbox :: inputcheckbox(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'select'}"><div class="mb-3" th:insert="~{fieldset/select :: select(${field})}"></div></th:block><th:block th:if="${field.tag.toString() == 'trix-editor'}"><div class="mb-3" th:if="${el.hasErrors()}" th:insert="~{fieldset/errors :: errors(${el.errors()})}"></div><div class="mb-3" th:insert="~{fieldset/trixeditor :: trixeditor(${field})}"></div></th:block></th:block> |
1 change: 1 addition & 0 deletions
1
test-suite-thymeleaf-fieldset/src/test/resources/views/fieldset/inputfile.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<th:block th:fragment="inputfile(el)" xmlns:th="http://www.thymeleaf.org"><label th:replace="~{fieldset/label :: label(${el.id()}, ${el.label()})}"></label><input th:type="file" th:name="${el.name()}" th:id="${el.id()}" th:accept="${el.accept()}" th:required="${el.required()}" class="form-control" th:classappend="${el.hasErrors()} ? is-invalid" th:attr="aria-describedby=${el.hasErrors}?|${el.name}ValidationServerFeedback|"/><div th:if="${el.hasErrors()}" th:replace="~{fieldset/errors :: errors(${el.name()}, ${el.errors()})}"></div></th:block> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
...et-tck/src/main/java/io/micronaut/views/fields/tck/FormCompletedFileUploadRenderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/* | ||
* 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.tck; | ||
|
||
import io.micronaut.context.annotation.Property; | ||
import io.micronaut.context.annotation.Requires; | ||
import io.micronaut.core.annotation.Introspected; | ||
import io.micronaut.core.beans.BeanIntrospection; | ||
import io.micronaut.http.multipart.CompletedFileUpload; | ||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest; | ||
import io.micronaut.views.ViewsRenderer; | ||
import io.micronaut.views.fields.Form; | ||
import io.micronaut.views.fields.FormElement; | ||
import io.micronaut.views.fields.FormGenerator; | ||
import io.micronaut.views.fields.annotations.InputHidden; | ||
import io.micronaut.views.fields.elements.InputSubmitFormElement; | ||
import io.micronaut.views.fields.messages.Message; | ||
import jakarta.inject.Singleton; | ||
import jakarta.validation.ConstraintViolationException; | ||
import jakarta.validation.Valid; | ||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.NotNull; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.io.IOException; | ||
import java.util.Map; | ||
import java.util.function.BiConsumer; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
||
@SuppressWarnings({"java:S5960"}) // Assertions are fine, these are tests | ||
@Property(name = "spec.name", value = "FormCompletedFileUploadRenderTest") | ||
@MicronautTest(startApplication = false) | ||
class FormCompletedFileUploadRenderTest { | ||
|
||
@Test | ||
void render(ViewsRenderer<Map<String, Object>, ?> viewsRenderer, | ||
FormGenerator formGenerator, | ||
EventImageSaveValidator validator) throws IOException { | ||
String viewName = "fieldset/form.html"; | ||
BiConsumer<String, BeanIntrospection.Builder<? extends FormElement>> 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 action="/foo/bar" method="post" enctype="multipart/form-data">\ | ||
<input type="hidden" name="id" value=""/>\ | ||
<div class="mb-3"><label for="alt" class="form-label">Alternative description of the image</label><input type="text" name="alt" value="" id="alt" class="form-control" required="required"/></div>\ | ||
<label for="file" class="form-label">File</label><input type="file" name="file" id="file" accept="image/png, image/jpeg" class="form-control" required="required"/>\ | ||
<input type="submit" value="Submit" class="btn btn-primary"/>\ | ||
</form>"""; | ||
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 action="/foo/bar" method="post" enctype="multipart/form-data">\ | ||
<input type="hidden" name="id" value="xxx"/>\ | ||
<div class="mb-3"><label for="alt" class="form-label">Alternative description of the image</label>\ | ||
<input type="text" name="alt" value="" id="alt" class="form-control is-invalid" aria-describedby="altValidationServerFeedback" required="required"/>\ | ||
<div id="altValidationServerFeedback" class="invalid-feedback">must not be blank</div>\ | ||
</div>\ | ||
<label for="file" class="form-label">File</label>\ | ||
<input type="file" name="file" id="file" accept="image/png, image/jpeg" class="form-control is-invalid" aria-describedby="fileValidationServerFeedback" required="required"/>\ | ||
<div id="fileValidationServerFeedback" class="invalid-feedback">must not be null</div>\ | ||
<input type="submit" value="Submit" class="btn btn-primary"/>\ | ||
</form>"""; | ||
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 action="/foo/bar" method="post" enctype="multipart/form-data">\ | ||
<input type="hidden" name="id" value="xxx"/>\ | ||
<div class="mb-3"><label for="alt" class="form-label">Alternative description of the image</label><input type="text" name="alt" value="Micronaut Logo" id="alt" class="form-control" required="required"/></div>\ | ||
<label for="file" class="form-label">File</label><input type="file" name="file" id="file" accept="image/png, image/jpeg" class="form-control" required="required"/>\ | ||
<input type="submit" value="Submit" class="btn btn-primary"/>\ | ||
</form>"""; | ||
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) { | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.