How-to: Creating a custom form view

In this how-to we’ll show you how to create an entity view with a form backed by a custom model object.

What we’ll do:

  • create a form model object representing our form data

  • creating an EntityViewProcessor that fetches, validates, saves and renders our model

  • add a custom form view and have it show up as a separate tab item

Some background

Creating an additional form view for an entity is a very common action. Often you want to manage data that does not actually represent entity type properties. You can attach this data to an existing (or separate) form by registering an extension on the EntityViewCommand.

Using extensions has the advantage that data binding and default (annotation based) validation will happen automatically.

If you add an extension to the default entity form (having the SaveEntityViewProcessor) and the extension has errors upon validation, that will count as global validation and will block saving the entity. This makes for a very easy way to extend a default form with custom behaviour.

Create the extension class

The extension class is the model for our form and will be validated when the form is submitted.

Extension class
@Data
static class MyExtension
{
    @NotBlank
    private String url;

    @Min(1980)
    @Max(2000)
    private int creationYear;
}

Create an EntityViewProcessor for the extension

Render the extension on the form and manage the form data. Annotation based validation will be done on form POST.

ExtensionViewProcessorAdapter implementation
/**
 * Custom EntityViewProcessor for MyExtension form.
 * Does not specify an extensionName() but uses the default one (class name),
 * as all processing will be done inside this implementation and there will
 * never be multiple instances of this processor for a single view.
 *
 * Builds a simple form manually.
 * Validation is actually done transparantly using annotation validation.
 * Form elements will show errors because the control names match the extension properties.
 */
static class MyExtensionViewProcessor extends ExtensionViewProcessorAdapter<MyExtension>
{
    @Autowired
    private CustomEntityValidator customEntityValidator;

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

    @Override
    protected void doPost( MyExtension extension,
                           BindingResult bindingResult,
                           EntityView entityView,
                           EntityViewRequest entityViewRequest ) {
        if ( !bindingResult.hasErrors() ) {
            // Put a dummy feedback message
            entityViewRequest.getPageContentStructure()
                             .addToFeedback(
                                     BootstrapUiBuilders.alert()
                                        .success()
                                        .dismissible()
                                        .text( "Updated url with: " + extension.getUrl() )
                                        .build()
                             );
        }
    }

	@Override
	protected void validateExtension( MyExtension extension, Errors errors, HttpMethod httpMethod, EntityViewRequest entityViewRequest ) {
        customEntityValidator.validate( extension, errors );
    }

    @Override
    protected void render( MyExtension extension,
                           EntityViewRequest entityViewRequest,
                           EntityView entityView,
                           ContainerViewElementBuilderSupport<?, ?> containerBuilder,
                           ViewElementBuilderMap builderMap,
                           ViewElementBuilderContext builderContext ) {
        builderMap.get( SingleEntityFormViewProcessor.LEFT_COLUMN, ColumnViewElementBuilder.class )
                  .add(
                          formGroup()
                                  .label( "URL" )
                                  .control( textbox()
                                               .controlName( controlPrefix() + ".url" )
                                               .text( extension.url ) )
                  )
                  .add(
                          formGroup()
                                  .label( "Creation year" )
                                  .control( textbox()
                                               .controlName( controlPrefix() + ".creationYear" )
                                               .text( "" + extension.creationYear ) )
                  );
    }

    @Override
    protected void postRender( MyExtension extension,
                               EntityViewRequest entityViewRequest,
                               EntityView entityView,
                               ContainerViewElement container,
                               ViewElementBuilderContext builderContext ) {
        EntityViewContext entityViewContext = entityViewRequest.getEntityViewContext();

        // Manually change the cancel button url
        container.find( "btn-cancel", ButtonViewElement.class )
                 .ifPresent( button -> button.setUrl(
                   entityViewContext.getLinkBuilder().update( entityViewContext.getEntity() )
                 ) );
    }
}

Register the view with our processor

The view itself can be registered under any name on the entity configuration. The view name will be used in the message code resolving.

When registering the view, some of the EntityViewCustomizers are used to specify an admin menu item (tab) should be rendered for this view.

Register the view
// Use a configuration template for a simple extension form
// Configure the view to create a menu item under the advanced options
entities.withType( ... )
        .formView(
                "extension",
                EntityViewCustomizers.basicSettings()
                    .adminMenu( "/advanced-options/extension" )
                    .andThen( EntityViewCustomizers.formSettings().forExtension( true ) )
                    .andThen( builder -> builder.viewProcessor( new MyExtensionViewProcessor() ) )
        );

Translate the menu item title

Set the right message code for the specific view menu item.

# Default value for every entity with that view
EntityModule.entities.adminMenu.views[extension]=My extension

# Specific title for the menu item on myEntity page
MyModule.entities.myEntity.adminMenu.views[extension]=Extra Fields