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
-
See the Property model reference manual for detailed documentation about the
Property
model. -
See the Datastore API reference manual for detailed documentation about the
Datastore
API. -
See the Vaadin Flow UI reference manual for detailed documentation about the Holon Platform Vaadin Flow module.
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.6</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 theproducts
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 theID
property is declared as identifier property. -
A
DataTarget
namedproducts
to be used with theDatastore
API to refer to theproducts
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 theHome
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 theManage
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
:
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 notnull
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
:
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.