Introduction

This tutorial shows how to use the Holon Platform Property model, the Datastore API and the Vaadin Flow UI module to build a simple web application, leveraging on Spring Boot to auto-configure and run the application using an embedded servlet container.

The web application purpose is to manage a simple product entity, persisted in a RDBMS, allowing the user to list the available products, display product data, create a new product, update an existing product and delete a product.

What You’ll learn

  • How to configure and build a web UI using the Holon Platform Vaadin module and provided fluent component builders.

  • How to use the @QueryParameter annotation to automatically inject query parameters in routing targets.

  • How to use the Navigator API to handle the application routing with query parameters support.

What You’ll need

  • About 30 minutes

  • A favorite text editor or IDE

  • JDK 1.8 or later

Reference documentation and source code

The source code for this tutorial is available on GitHub in the Holon Platform examples repository:

Project setup

We’ll use Maven to create and setup the example project of this tutorial. So, create a standard Maven project and use the following decalrations in your pom.xml to obtain the required Holon Platform artifacts:

2. Use a holon.platform.version property to declare the Holon Platform version number in the properties section, for example:

<properties>
  <holon.platform.version>5.2.1</holon.platform.version>
</properties>
See Getting started to know about the last Holon Platform release version.

2. Import the Holon Platform BOM in the dependencyManagement section, using the last platform version:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.holon-platform</groupId>
      <artifactId>bom</artifactId>
      <version>${holon.platform.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Step #1: Datastore configuration

We’ll use the JDBC Datastore implementation to manage data persistence, auto-configured and enabled using the Holon Platform Spring Boot starter that includes the HikariCP DataSource.

So you want to add the following depencency to your project pom:

<dependencies>

  <dependency>
    <groupId>com.holon-platform.jdbc</groupId>
    <artifactId>holon-starter-jdbc-datastore-hikaricp</artifactId>
  </dependency>

</dependencies>

We’ll use H2 as in-memory database engine, so you want to add the following dependency:

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.4.193</version>
</dependency>

Next you want to create a schema.sql script (in the default Maven src/main/resources resources folder) to create the products database table:

create table products (
  id bigint primary key auto_increment,
  sku varchar(100) not null,
  description varchar(500),
  category varchar(10),
  price double,
  withdrawn integer default 0
)

We also provide a data.sql script to store some example product data in the products table:

INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-1', 'Product 1', 'C1', 19.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-2', 'Product 2', 'C2', 29.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-3', 'Product 3', 'C1', 15.00, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-4', 'Product 4', 'C2', 75.50, 1);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-5', 'Product 5', 'C3', 19.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-6', 'Product 6', 'C1', 39.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-7', 'Product 7', 'C4', 44.20, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-8', 'Product 8', 'C1', 77.00, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-9', 'Product 9', 'C3', 23.70, 1);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-10', 'Product 10', 'C2', 92.20, 0);

Finally, we configure a DataSource using Spring Boot auto-configuration through the following configuration properties in the application.yml file:

spring:
  datasource:
    url: "jdbc:h2:mem:test"
    username: "sa"

Step #2: The property model

Now you want to create the product entity Property model using an interface named Product, which will contain:

  • The product entity properties, declared through a suitable PathProperty type, where each property path name correspond to the products database table column name. A property caption message is specified for each property and later used by the UI components.

  • The PropertySet which represents the product entity, where the the ID property is declared as identifier property.

  • A DataTarget named products to be used with the Datastore API to refer to the products database table.

public interface Product {

  static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Product ID"); (1)

  static final StringProperty SKU = StringProperty.create("sku").message("SKU");

  static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description");

  static final StringProperty CATEGORY = StringProperty.create("category").message("Category");

  static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price").message("Price")
      // not negative value validator
      .withValidator(Validator.notNegative()); (2)

  static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn").message("Withdrawn")
      // set a property value converter from Integer model type to Boolean
      .converter(PropertyValueConverter.numericBoolean(Integer.class)); (3)

  // Product property set
  static final PropertySet<?> PRODUCT = PropertySet.builderOf(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE, WITHDRAWN) //
      .identifier(ID) // Set the ID property as identifier
      .build();

  // "products" DataTarget
  static final DataTarget<?> TARGET = DataTarget.named("products");

}
1 A caption is assigned to each property through the message(…​) builder method
2 Add a property value validator to check the price is not negative: this validator will be inherited from input type UI components
3 Set a property value converter from Integer model type (the database column type) to Boolean property type
See the property model tutorial to learn the basics about the Holon Platform property model.

Step #3: Application configuration

Now it’s time to configure the Vaadin Flow application UI using the Holon Platform Vaadin Flow module and the Spring Boot auto-configuration support.

First of all, you wan to add the Spring Boot starter dependency to the project pom:

<dependency>
  <groupId>com.holon-platform.vaadin</groupId>
  <artifactId>holon-starter-vaadin-flow</artifactId>
</dependency>

Next you want to create the Spring Boot entry point Application class, which will be used to enable the auto-configuration features and run the application using an embedded servlet container:

@SpringBootApplication
public class Application {

  public static void main(String[] args) throws Exception {
    SpringApplication.run(Application.class, args);
  }

}

Step #4: Create the application routes

After the configuration phase has ended, it’s time to create the application routes, i.e. the components which represents the application views (which can be intended as the application pages).

The @QueryParameter annotation is used on routing target components to provide automatic URL query parameters value injection, with a consistent Java type conversion support.

See the Vaadin Flow module documentation for detailed information about the query parameters injection and type handling.

Using Spring Boot, the Holon Platform Navigator API is automatically configured and setted up for the current UI, and will be used to perform the application routing, i.e. the navigation between the application routes, with a simple and consistent support to provide and convert the query parameter values.

See the Vaadin Flow module documentation for detailed information about the Navigator API.

We’re going to create two routes:

  • The default route, mapped to the empty "" route path and represented by the Home UI component. The home route will list all the available products and provide the new, edit and delete actions.

  • The manage route, mapped to the "manage" route path and represented by the Manage UI component. The manage route will provide an input form to create or update a product.

Using the @Route annotation, each route class is automatically enabled as Spring bean, allowing dependency injection (for example using the @Autowired annotation) and bean lifecycle hooks (for example using the @PostConstruct annotation).

The Home route

The Home route is the application default view, i.e. the view displayed when a navigation path is not specified, since it is mapped to the empty "" route path.

It lists the available products using a Holon Platform PropertyListing component. The PRODUCT PropertySet is configured as listing property set, so the listing columns will be automatically generated for all the properties that are part of such property set.

The product property captions (configured through the message(…​) property builder method in the Product data model) will be used by default as column headers.

A Datastore is obtained by injection and used as the listing data source, providing the product TARGET as query target. Since the PRODUCT property set declares the ID property as identifier, its values will be used as item identifiers in the listing.

A item click listener is configured so that when to user clicks a listing row, the edit action is perfomed: the Navigator API is used to navigate towards the manage route path, providing the product id as id query parameter value.

Finally, a delete action is provided by adding a virtual listing column using the withComponentColumn builder method: for each row a Button is generated with a click event handler, which uses the Datastore API to delete the product which corresponds to the listing row and refreshes the list at the end.

Furthermore, a "Add new" button is added at the top of the content layout, to allow the user to create a new product. When the user clicks the button, a navigation towards the manage route path is performed using the Navigator API, obtained statically for the current UI using the get() method.

So you want to create a Home class this way:

@Route("") (1)
@PageTitle("Products")
public class Home extends VerticalLayout {

  private static final long serialVersionUID = 1L;

  @Autowired
  private Datastore datastore; (2)

  private PropertyListing listing;

  @PostConstruct
  public void init() {
    // configure the layout
    Components.configure(this).fullSize()
        // add new button
        .add(Components.button().text("Add new") (3)
            // on click, navigate to "manage" view
            .onClick(e -> Navigator.get().navigateTo("manage")).build())
        // build and add listing, setting the flex grow ratio to 1
        .addAndExpand(listing = Components.listing.properties(PRODUCT).fullWidth() (4)
            // setup listing data source using the Datastore with "products" as data target
            .dataSource(datastore, TARGET) (5)
            // froze the Actions and ID columns
            .frozenColumns(2)
            // when user clicks on a row, route to Manage, providing the product "id" parameter
            .withItemClickListener(event -> { (6)
              Navigator.get().navigation(Manage.class)
                  .withQueryParameter("id", event.getItem().getValue(ID)).navigate();
            })
            // add a Actions column as first with a "Delete" Button
            .withComponentColumn(item -> Components.button().text("Delete").icon(VaadinIcon.TRASH)
                .onClick(event -> delete(item.getValue(ID))).build())
            .header("Actions").displayAsFirst().add() (7)
            // build
            .build(), 1); (8)
  }

  private void delete(Long productId) {
    // ask confirmation
    Components.dialog.question(answeredYes -> { (9)
      if (answeredYes) {
        // if confirmed, delete the product
        datastore.bulkDelete(TARGET).filter(ID.eq(productId)).execute(); (10)
        // refresh list
        listing.refresh(); (11)
      }
    }).text("Are you sure?").open();
  }

}
1 Declare the class as route and map it to the default path
2 Inject the JDBC Datastore instance, automatically configured using Spring Boot
3 Add a Button to add a new product
4 Build a PropertyListing component using the PRODUCT property set
5 Use the injected Datastore and the TARGET data target as listing data source
6 Add a item click listener to edit a product, by navigating to the manage route path and providing the product id through the id query parameter value
7 Add virtual column to provide a delete button, configuring the column to be displayed as first column and with the "Actions" column header
8 Build and add the PropertyListing component, setting the flew grow ratio to 1 in order to give all the available space to the component in the layout
9 When the user clickc the delete button, open a question dialog to ask the user confirmation
10 Use the Datastore API to delete the product with the given id
11 Refresh the listing item set

Example of the Home route, displayed usig the URL http://localhost:8080:

webapp1

The Manage route

The Manage route is used to provide the product data input, allowing to create a new product or update an existing one.

A URL query parameter named id is used to provide the id of the product to edit and its value is automatically injected in the id field using the @QueryParameter annotation, with automatic value conversion from the String type URL query parameter value to the required Long Java type.

The Holon Platform PropertyInputForm component is used to allow the user to input the product property values and it is configured using the PRODUCT property set. This way, an input component for each property of the set is automatically created and displayed on the UI.

Some additional configuration is provided to the input form:

  • The ID property is setted as read-only, so that it’s value cannot be modified by the user, since the product id value is auto-generated by the database.

  • The SKU property is configured as required: a validator to check the value is not null will be automatically added and the required state will be displayed for to the input field.

  • A default value is provided for the CATEGORY property.

  • A validator is configured for the DESCRIPTION property, to check the value is minimum three characters long.

Any Property specific validator is inherited by the input form, so, for example, the not negative price validator configured for the UNIT_PRICE property in the Product model class will be inherited by the corresponding input component.

The product property captions (configured through the message(…​) property builder method in the Product model) will be used by default as the form input component captions.

The id query parameter is used to discern if the form is in edit or insert mode. When the id query parameter is null, we assume the form to be in insert mode, otherwise the product with the corresponding id value is loaded form the backend using the Datastore API and setted as input form value, causing the form to load all the product property values and setting it as input components value.

Finally, a Save button is provided to allow the user to save a new product or confirm product data update. When the user clicks the button, the Datastore API is used to save the product data, obtained as a PropertyBox form the input form. Then the user is redirected to the application default route, using the navigateToDefault() method of the Navigator API.

So you want to create a Manage class this way:

@Route("manage")
@PageTitle("Edit product")
public class Manage extends VerticalLayout {

  private static final long serialVersionUID = 1L;

  @QueryParameter
  private Long id; (1)

  @Autowired
  private Datastore datastore;

  private PropertyInputForm form;

  @PostConstruct
  public void init() {
    Components.configure(this)
        // add a PropertyInputForm using the Product property set
        .add(form = Components.input.form(PRODUCT) (2)
            // set ID as read-only
            .readOnly(ID)
            // set SKU as required
            .required(SKU)
            // set "DFT" as CATEGORY default value
            .defaultValue(CATEGORY, () -> "DFT")
            // add a validator to check that DESCRIPTION has minimum 3 characters
            .withValidator(DESCRIPTION, Validator.min(3))
            // build the form
            .build())
        .add(Components.button().text("Save").withThemeVariants(ButtonVariant.LUMO_PRIMARY).onClick(e -> save())
            .build());
  }

  @OnShow
  public void load() {
    // if the "id" parameter is not null, we are in edit mode
    if (id != null) { (3)
      // load the product data
      form.setValue(datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT)
          // throw an exception if a product with given id was not found
          .orElseThrow(() -> new DataAccessException("Product not found: " + id)));
    }
  }

  private void save() {
    // check valid and get PropertyBox value
    form.getValueIfValid().ifPresent(value -> {
      // save the product data
      datastore.save(TARGET, value, DefaultWriteOption.BRING_BACK_GENERATED_IDS); (4)
      // notify the saved product id
      Notification.show("Product saved [" + ((id != null) ? id : value.getValue(ID)) + "]");
      // go back home
      Navigator.get().navigateToDefault();
    });

  }

}
1 Query parameter declaration for automatic value injection in the id field. Since value() annotation attribute is not specified, the query parameter name is assumed to be the field name (id).
2 Create a PropertyInputForm using the PRODUCT property set
3 The id query parameter value is used to check if the form is in insert or edit mode. When available, the Datastore API is used to retrieve the product data and the form setValue method is used to load the product data into the form input components
4 The BRING_BACK_GENERATED_IDS default write option is used to reflect the auto-generated ID property value into the PropertyBox instance when in insert mode

Example of the Manage route, displayed usig the URL http://localhost:8080/manage?id=1:

webapp2

Step #5: Run the application

Since we used Spring Boot to configure the web application, just run the Application class to start the embedded servlet container and deploy the application. Next, open the following URL in your web browser to display the application:

Summary

You’ve learned how to use the Holon Platform Vaadin Flow module to create and run a web application which allows to manage a simple data entity, leveraging on the Property model and the Datastore API to configure the UI components and manage the data persistence in a RDBMS. The @QueryParameter annotation and the Navigator API have been used to handle the application routes navigation and the URL query parameter values.

See also

The source code of this tutorial is available on GitHub.

See the Holon Platform tutorials to explore other available tutorials.