How-to: Add a custom form with a file upload

This how-to will cover the basics of creating an additional form view.

A short summary about the topics discussed:

  • Create a file upload with validation

  • Create your own form view on a entity

  • Register a new property on a form view that is rendered by EntityModule

Create our file dto class

First we have to create a class that can act as a dto object for our uploaded file. We can also use this to set our validation annotations if desired.

FileRequest.java - Example of dto class for our to be uploaded file
@Setter
@Getter
public class FileRequest {
    @NotNull
    private MultipartFile[] files;
}

Register a new form view on an entity

To create a form view, all we have to do is register modify the configuration of our entity in an EntityConfigurer.

DummyEntityConfiguration.java - Entity configuration for our dummy entity
@Configuration
class DummyEntityConfiguration implements EntityConfigurer {

    @Override
    public void configure(EntitiesConfigurationBuilder entities) {
        entities.withType(DummyEntity.class)
            .formView("upload", fvb -> fvb
                .properties(props -> props.property("files") (1)
                    .displayName("Csv file") (2)
                    .viewElementBuilder( (3)
                        ViewElementMode.CONTROL, BootstrapUiBuilders.file()
                            .controlName("extensions[upload].files")
                    )
                    .writable(true) (4)
                )
                .showProperties("files")
                .removeViewProcessor(SaveEntityViewProcessor.class.getName()) (5)
            );
    }
 }
1 A new property files is registered which we will use to represent the file upload.
2 By configuring a display name we modify the label that will be rendered.
3 A ViewElementBuilder is provided to override how the control should be rendered. Since we’re creating a file upload form we’ll override the builder to ensure that our rendered ViewElement is a file input. To bind our form control to the FileRequest#files property, we’ll also provide a control name. In this case we’ll be mapping to an extension which will be configured in the view processor later on.
4 The end user should be able to select a file, so we set the property to writable.
5 When a form is submitted, we expect our data to be saved, which is done by the SaveEntityViewProcessor. In this case, we merely want to receive the uploaded files and then process them instead of saving the attached entity, so we’ll remove this specific processor.
Take a look at Configuring entity types for more in depth knowledge on configuring entities.

Access the view

Now that our custom form view has been registered we can access it by navigating to the following path on our application: admin/entities/dummyEntity?view=upload. This path is the base path to the DummyEntity type, for which we have just configured an additional form view with the name upload. By adding the additional view parameter we can specify which view should be rendered.

You can find out more about how entity views work here.

Handling our form submit.

Thus far we’ve created a custom form view which renders the required controls. The next step is to handle the uploaded file, so we’ll create a view processor that extends the ExtensionViewProcessorAdapter. An ExtensionViewProcessorAdapter is a specific EntityViewProcessorAdapter which makes it easier to work with extensions.

Extensions are a map of additional properties on the EntityViewCommand and can be used to render/receive additional data. Extensions are automatically bound to the EntityViewCommand when submitted, during which base validation is applied as well. To learn more about extensions, take a look at EntityViewCommand.

We’ll take care of handling the form data in four steps:

  • Ensuring that the data is bound to the aforementioned DTO (FileRequest) when the form is submitted.

  • Registering the view processor on the configuration.

  • Ensuring that our form is configured for multi-part data.

  • Processing the received data.

First off we’ll create the processor.

FileUploadViewProcessor.java
@Component
public class FileUploadViewProcessor extends ExtensionViewProcessorAdapter<FileRequest> {

    @Override (1)
    protected String extensionName() {
        return "upload";
    }

    @Override (2)
    protected FileRequest createExtension(EntityViewRequest entityViewRequest, EntityViewCommand command, WebDataBinder dataBinder) {
        return new FileRequest();
    }

}
1 We provide the name of the extension that is registered in this processor. This will make sure that extension[upload] will be mapped to the entity defined in this processor.
2 In this case, we want to map the submitted form data to a FileRequest object, so we’ll register a new instance to which the form data can be bound.

Now that we have our view processor we can add it to our configuration class.

DummyEntityConfiguration.java - Abbreviated configuration
@Configuration
@RequiredArgsConstructor
class DummyEntityConfiguration implements EntityConfigurer {

    private final FileUploadViewProcessor fileUploadViewProcessor; (1)

    @Override
    public void configure(EntitiesConfigurationBuilder entities) {
        entities.withType(DummyEntity.class)
            .formView("upload", fvb -> fvb.viewProcessor(fileUploadViewProcessor) (1)
                .properties(props -> props.property("files")
                   // ...
                )
                // ...
            );
    }
 }
1 The view processor is wired and added to the upload form view.

Next up, we’ll ensure that our form is configured as a multipart form, by setting the encryption type on the form that is currently rendered.

FileUploadViewProcessor.java - Set the encryption type of the form
@Override
protected void postRender(EntityViewRequest entityViewRequest, EntityView entityView, ContainerViewElement container, ViewElementBuilderContext builderContext) {
    container.find("entityForm", FormViewElement.class)
        .ifPresent(form -> form.setEncType(FormViewElement.ENCTYPE_MULTIPART));
}

Finally we’ll handle our form submission.

FileUploadViewProcessor.java - Handle form submission
@Override
protected void doPost(FileRequest extension, BindingResult bindingResult, EntityView entityView, EntityViewRequest entityViewRequest) {
    if (!bindingResult.hasErrors()) { (1)
        MultipartFile[] files = file.getFiles(); (2)

        // ... Handle the submitted files.
    }
}
1 Only handle data if no validation errors have occurred.
2 Retrieve the upload files so they can be handled.

It’s also possible to quickly add a feedback message and/or set a redirect url after handling the data.

Example setting a feedback message
// The EntityViewPageHelper is a bean that can be directly wired into the processor.
entityViewPageHelper.addGlobalFeedbackAfterRedirect(entityViewRequest, Style.SUCCESS, "feedback.entityUpdated");
Example setting a redirect url
entityView.setRedirectUrl("/some-url");

You can also use the EntityLinkBuilder to generate a URL to one of the configured entity views.