Broadleaf Microservices

Views

Overview

View components are the top-level component rendered for a route. Views are registered within the ComponentRegistry using a unique ID, e.g. catalog:products:list. The view metadata is retrieved for a route whenever that route is matched within a client.

Add a View

Views are primarily configured through Spring auto-configuration using a ComponentSource bean. Here is a simple example that creates and registers a single browse view:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Browse View

The primary view used for representing a top-level collection of entities.

Simple Grid

Browse view supports at least one single grid that interacts with a collection API:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Multi Grid

Browse view supports more than just a single grid, and can be configured with additional grids if desired:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .label("Brands")
                      .readUrl("/brands")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url")))
              .grid("specialBrands", specialBrandsGrid -> specialBrandsGrid
                      .label("Special Brands")
                      .readUrl("/special-brands")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url")));
    }
}

Create

Browse view supports configuring an action to initiate the creation flow of a new collection member:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .createAction(createAction -> createAction
                            .label("Create Brand")
                            // links by component ID, must have a component registered with ID of "brands:create"
                            .linkById("brands:create"))
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Filterable

Browse view grids support filtering the data in two different ways, using a text query, or a field-level query builder. Using #filterByTextQuery, the grid will render a search input that, when used, will result in a query parameter, q, being sent as part of the next fetch request, e.g. /brands?q=Heat Clinic. Using #filterByQueryBuilder, the grid will render a query build action that, when used, will result in an RSQL query being sent as the cq parameter, e.g. /brands?cq=name=='Heat Clinic'.

Note

Both of these filter actions require the backend API to support the parameters they send.

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .filterByTextQuery()
                      .filterByQueryBuilder()
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Both of these actions can be configured with a different query parameter to fit different API conventions:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .filterByTextQuery("query")
                      .filterByQueryBuilder("rsql")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

In addition to user-driven filters, grids may support implicit filters as well. Implicit filters are always applied and provided as part of the fetch request:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .filterByQueryBuilder()
                      // implicitly injects this filter into "cq" filter, e.g. /brands?cq=type=eq='PRIMARY' or /brands?cq=name=='My Brand';type=='PRIMARY'
                      .implicitFilter("cq", "type=eq='PRIMARY'")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Sortable

Browse view grids support sorting the data by any of the column headers. Using #sortable, the grid will be enabled with sortable support and begin sending a sort parameter on the next fetch request, e.g. `/brands?sort=name desc:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .sortable()
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

By default, all columns within a sortable grid will be marked as sortable. This can be customized by marking certain columns as #notSortable:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .sortable()
                      .addColumn("name", Columns.string().label("Name").sortable())
                      .addColumn("url", Columns.string().label("Url").notSortable())
              );
    }
}

Sortable grids can be configured to use a custom name for the sort parameter in order to support different API conventions:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .sortable("mySort") // e.g. /brands?mySort=name desc
                      .addColumn("name", Columns.string().label("Name").sortable())
                      .addColumn("url", Columns.string().label("Url").notSortable())
              );
    }
}

Sortable grids can be configured with a default sort that the grid uses for its initial sorting state:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .readUrl("/brands")
                      .sortableWithDefault(Sort.desc("name")) // e.g. /brands?sort=name desc
                      .addColumn("name", Columns.string().label("Name").sortable())
                      .addColumn("url", Columns.string().label("Url").notSortable())
              );
    }
}

Sandbox Trackable Grid

Browse view supports interacting with sandbox-trackable APIs including a sandbox ribbon with the ability to execute promotion flows:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .sandboxTrackable("BRAND")
                      .readUrl("/brands")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Catalog Trackable Grid

Browse view supports interacting with catalog-trackable APIS by including a catalog ribbon with the ability to switch catalogs:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
            .add("brands:browse", getBrandsBrowse());
    }

    private EntityBrowseView<?> getBrandsBrowse() {
        return Views.entityBrowseView()
              .label("Brands")
              .defaultGrid(brandsGrid -> brandsGrid
                      .catalogTrackable()
                      .readUrl("/brands")
                      .addColumn("name", Columns.string().label("Name"))
                      .addColumn("url", Columns.string().label("Url"))
              );
    }
}

Create View

The primary view used for representing a top-level creation of an entity.

Simple Form

Create view supports a simple metadata-driven form that interacts with a form submission API:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:create", getBrandCreate());
    }

    private CreateEntityView<?> getBrandCreate() {
        return Views.entityViewCreate()
                .label("Create Brand")
                // where the "back" link leads
                .backLinkById("brands:browse")
                // configure the submit action and give it a label
                .submitAction(submitAction -> submitAction
                        .label("Create"))
                // configure the submit URL, defaults to POST
                .submitUrl("/brands")
                .generalForm(form -> form
                        // adds a required field for the "name" property
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        // adds a required field for the "url" property
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));

    }
}

Post-Submit Redirection

Create view by default will redirect to the update view after a successful form submission. The link to the update view can be controlled on the create view:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:create", getBrandCreate());
    }

    private CreateEntityView<?> getBrandCreate() {
        return Views.entityViewCreate()
                .label("Create Brand")
                .backLinkById("brands:browse")
                // redirect to "brands:update" on submit
                .updateLinkById("brands:update")
                .submitAction(submitAction -> submitAction
                        .label("Create"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));
    }
}

Alternatively, in scenarios where there may not be an update view, one can configure the post-submit redirection to the back link instead:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:create", getBrandCreate());
    }

    private CreateEntityView<?> getBrandCreate() {
        return Views.entityViewCreate()
                .label("Create Brand")
                .backLinkById("brands:browse")
                // redirect to the back link on create
                .redirectToBackOnCreate()
                .submitAction(submitAction -> submitAction
                        .label("Create"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));
    }
}

Sandbox Trackable Create

Create view supports creation for a sandbox-trackable API including a sandbox ribbon:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:create", getBrandCreate());
    }

    private CreateEntityView<?> getBrandCreate() {
        return Views.entityViewCreate()
                .label("Create Brand")
                .backLinkById("brands:browse")
                .sandboxTrackable("BRAND")
                .submitAction(submitAction -> submitAction
                        .label("Create"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));
    }
}

Catalog Trackable Create

Create view supports creation for a catalog-trackable API:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:create", getBrandCreate());
    }

    private CreateEntityView<?> getBrandCreate() {
        return Views.entityViewCreate()
                .label("Create Brand")
                .backLinkById("brands:browse")
                .catalogTrackable()
                .submitAction(submitAction -> submitAction
                        .label("Create"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));
    }
}

Update View

The primary view used for representing a top-level update of an entity.

Simple Form

Update view supports a simple metadata-driven form that interacts with a form submission API:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:update", getBrandUpdate());
    }

    private UpdateEntityView<?> getBrandUpdate() {
        return Views.entityViewUpdate()
                // where the "back" link leads
                .backLinkById("brands:browse")
                // configure the read URL, defaults to GET
                .readUrl("/brands/${id}")
                // configure the delete action and give it a label
                .deleteAction(deleteAction -> deleteAction
                        .label("Delete"))
                // configure the delete URL, defaults to DELETE
                .deleteUrl("/brands/${id}")
                // configure the submit action and give it a label
                .submitAction(submitAction -> submitAction
                        .label("Save"))
                // configure the submit URL, defaults to PUT
                .submitUrl("/brands")
                // configure the general metadata-driven form
                .generalForm(form -> form
                        // adds a required field for the "name" property
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        // adds a required field for the "url" property
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));

    }
}

Multi Form

Update view supports multiple metadata-driven forms with each additional form showing up under a tab:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:update", getBrandUpdate());
    }

    private UpdateEntityView<?> getBrandUpdate() {
        return Views.entityViewUpdate()
                .backLinkById("brands:browse")
                .readUrl("/brands/${id}")
                .deleteAction(deleteAction -> deleteAction
                        .label("Delete"))
                .deleteUrl("/brands/${id}")
                .submitAction(submitAction -> submitAction
                        .label("Save"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()))
                // add an additional form tab for "advanced" properties
                .form("advancedForm", form -> form
                        .label("Advanced")
                        .addField("advanced", Fields.bool()
                                .label("Is Advanced?")));
    }
}

Sandbox Trackable Update

Update view supports creation for a sandbox-trackable API including a sandbox ribbon:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:update", getBrandUpdate());
    }

    private UpdateEntityView<?> getBrandUpdate() {
        return Views.entityViewUpdate()
                .backLinkById("brands:browse")
                .sandboxTrackable("BRAND")
                .readUrl("/brands/${id}")
                .deleteAction(deleteAction -> deleteAction
                        .label("Delete"))
                .deleteUrl("/brands/${id}")
                .submitAction(submitAction -> submitAction
                        .label("Save"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));
    }
}

Catalog Trackable Update

Update view supports updates for a catalog-trackable API:

@Configuration
public class BrandMetadataAutoConfiguration {
    @Bean
    public ComponentSource brandMetadataComponents() {
        return registry -> registry
                .add("brands:update", getBrandUpdate());
    }

    private UpdateEntityView<?> getBrandUpdate() {
        return Views.entityViewUpdate()
                .backLinkById("brands:browse")
                .catalogTrackable()
                .readUrl("/brands/${id}")
                .deleteAction(deleteAction -> deleteAction
                        .label("Delete"))
                .deleteUrl("/brands/${id}")
                .submitAction(submitAction -> submitAction
                        .label("Save"))
                .submitUrl("/brands")
                .generalForm(form -> form
                        .addField("name", Fields.string()
                                .label("Name")
                                .required())
                        .addField("url", Fields.string()
                                .label("URL")
                                .required()));
    }
}