Introduction
This tutorial shows how to use the Holon Platform Property model, the Datastore API and the Vaadin UI Module 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
-
How to use the Holon Platform
ViewNavigator
to setup and manage the application views (or virtual pages)
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 UI reference manual for detailed documentation about the Holon Platform Vaadin 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 {
public static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Product ID"); (1)
public static final StringProperty SKU = StringProperty.create("sku").message("SKU");
public static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description");
public static final StringProperty CATEGORY = StringProperty.create("category").message("Category");
public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price").message("Price")
// not negative value validator
.withValidator(Validator.notNegative()); (2)
public 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
public static final PropertySet<?> PRODUCT = PropertySet
.builderOf(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE, WITHDRAWN).identifier(ID).build();
// "products" DataTarget
public static final DataTarget<?> TARGET = DataTarget.named("products");
}
1 | To each property is assigned a caption 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: UI configuration
Now it’s time to configure the Vaadin application UI using the Holon Platform Vaadin 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</artifactId>
</dependency>
Next you want to create the Vaddin application entry point, i.e. the UI
class:
@SpringUI (1)
@SpringViewDisplay (2)
@Theme("valo") (3)
public class UI extends com.vaadin.ui.UI {
@Override
protected void init(VaadinRequest request) {
}
}
1 | Declare the UI class to be automatically detected and configured by Spring |
2 | Use the UI itself as view display, i.e. as the View content container |
3 | Set valo as theme |
Then create a simple Spring Boot 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 views
After the configuration phase has ended, it’s time to create the application views, i.e. the virtual pages which represent the application functions and contents.
Using Spring Boot, the Holon Platform ViewNavigator
is automatically configured and setted up. This will allow us to use some of the ViewNavigator
features, such as the @DefaultView
configuration, the @OnShow
view lifecycle hook, the @ViewParameter
handling and the ability to automatically open a view in a application window.
See the Holon Vaadin UI reference manual for detailed documentation about the Holon Platform Vaadin module and the ViewNavigator API.
|
We’re going to create three views:
-
Home
: the home view, listing all the available products and allowing to display the product data by clicking on a row -
View
: displays a product in an application window, providing the edit and delete operations -
Manage
: manages the product data, providing an input form to create or update a product
Each view is enabled as a Spring bean, so dependency injection and bean scope configuration are available.
The Home
view
The Home
view is the application default view, i.e. the view displayed when a navigation path is not specified, for example at first application display, for this reason it’s annotated with @DefaultView
.
The @UIScope
annotation on the Home
view class is used to declare the Spring scope of the View, which in this case is the ui
scope, so that the View instance lifecycle will match the application UI lifecyle.
By default, the View bean lifecycle matches the navigation step range, i.e. a new instance of the View is created when the view in displayed and destroyed when hidden (the navigation proceeds to another View).
This view lists the available products using a Holon Platform PropertyListing
component. The PRODUCT
PropertySet
is configured as the listing property set, so the listing columns will be generated for all the properties that are part of the PRODUCT
property set.
The product property captions (configured through the message(…)
property builder method in the Product
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.
An item click listener is configured so that when to user clicks a listing row, the application View named view
is opened, providing the id
parameter value which represents the ID
property value of the product to display. To navigate to such view, the Holon Platform ViewNavigator
is used, which is statically obtained from the current platform context.
Furthermore, a "Add new" button is added at the top of the View content, to allow the user to create a new product. When the user clicks the button, the manage
So you want to create a Home
class this way:
@DefaultView
@UIScope
@SpringView(name = "home") (1)
public class Home extends VerticalLayout implements View {
private static final long serialVersionUID = 1L;
@Autowired
private Datastore datastore;
@PostConstruct
public void init() {
Components.configure(this)
// set full to view content
.fullSize().spacing().add(Components.button().caption("Add new").styleName(ValoTheme.BUTTON_PRIMARY)
// navigate to "manage" view
.onClick(e -> ViewNavigator.require().toView("manage").navigate()).build())
// build and add listing
.addAndExpandFull(Components.listing.properties(PRODUCT)
// setup data source using Datastore with 'products' table name target and product ID as pk
.dataSource(datastore, TARGET, ID)
// froze the ID column
.frozenColumns(1)
// set the ID column width and style
.width(ID, 120).style(ID, "id-column")
// when user clicks on a row, open the 'view' named View providing product id parameter
.withItemClickListener((i, p, x, e) -> ViewNavigator.require().toView("view")
.withParameter("id", i.getValue(ID)).navigate())
// set full size and build
.fullSize().build());
}
}
1 | The @SpringView annotation is used to declare the view as a Spring bean, enable the view in the ViewNavigator and assign the view name |
The View
view
The view
application View is used to display the product data, using the Holon Platform PropertyViewForm
, which is able to display a set of Property
values. The PropertyViewForm
is configured with the PRODUCT
property set and built using the fluent component builder this way:
Components.view.form().fullSize().properties(PRODUCT).build()
Then we add two buttons at the bottom of the view:
-
Edit: To edit the product data. When the user clicks the button, the
ViewNavigator
is used to navigate to themanage
view, providing anid
parameter with the value of the displayed productID
property -
Delete: To delete the displayed product. When the user clicks the button, a question dialog is opened to request confirmation. If the user clicks yes on the question dialog, the current product is deleted using the
Datastore
API with thePropertyBox
provided by thePropertyViewForm
. Then thenavigateBack()
method of theViewNavigator
is used to navigate back to the previous View.
A view parameter is declared using the @ViewParameter
annotation on a View field. The field is named id
and it’s of type Long
. The Holon Platform ViewNavigator
will automatically bind the view parameter value named id
, provided with the navigation request, into that field.
The id
parameter is used to obtain the id of the product to display. The method annotated with @OnShow
is invoked when the View is displayed and loads in the PropertyViewForm
the PropertyBox
of the product with such id using the Datastore
API.
Finally, the view is annotated with @WindowView
, instructing the ViewNavigator
to display the View contents in an application (modal) Window. The window can be configured using the annotation and the @Caption
annotation can be used to customize the Window header message.
@WindowView(windowWidth = "500px", windowHeigth = "400px")
@Caption("View product")
@SpringView(name = "view")
public class View extends VerticalLayout implements com.vaadin.navigator.View {
private static final long serialVersionUID = 1L;
@ViewParameter
private long id;
@Autowired
private Datastore datastore;
private final PropertyViewForm viewForm;
public View() {
super();
Components.configure(this)
// set margins and size full to view content
.margin().fullSize()
// add view form using Product property set
.addAndExpandFull(viewForm = Components.view.form().fullSize().properties(PRODUCT).build()).add(
// horizontal layout as bottom toolbar
Components.hl().fullWidth().spacing().styleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR)
// EDIT action
.add(Components.button().caption("Edit").fullWidth().styleName(ValoTheme.BUTTON_PRIMARY)
// navigate to "manage" view providing product id parameter
.onClick(e -> ViewNavigator.require().toView("manage").withParameter("id", id)
.navigate())
.build())
// DELETE action
.add(Components.button().caption("Delete").fullWidth()
.styleName(ValoTheme.BUTTON_DANGER)
// ask confirmation before delete
.onClick(e -> Components.questionDialog().message("Are you sure?")
.callback(answeredYes -> {
// if confirmed, delete the current form product PropertyBox
datastore.delete(TARGET, viewForm.getValue());
// navigate back
ViewNavigator.require().navigateBack();
}).open())
.build())
.build());
}
@OnShow
public void onShow() {
// set the view form product value
viewForm.setValue(
// load product using id parameter
datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT)
// throw an exception if not found
.orElseThrow(() -> new DataAccessException("Product not found: " + id)));
}
}
The Manage
view
The last view is the manage
view, who will take care of product data input, allowing to create a new product or update an existing one.
A 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 created and displayed on the UI.
Some additional configuration is provided to the input form:
-
The product
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 indicator symbol will be displayed next to the input field -
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 form 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.
A Save action 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 home view, using the navigateToDefault()
method of the ViewNavigator
.
Just like the View
class, the product id to use when the product data has to be updated is provided using a @ViewParameter
field named id
of type Long
, automatically setted by the ViewNavigator
according to the navigation request, if available.
At view display, the @OnShow
annotated method checks if the id
parameter value is available, and if so retrieve the product data which corresponds to given id using the Datastore
API and loads it into the PropertyInputForm
.
The manage
view is annotated with @VolatileView
, excluding it form the navigation history, so that it won’t be recalled when navigating back in history.
@VolatileView
@SpringView(name = "manage")
public class Manage extends VerticalLayout implements com.vaadin.navigator.View {
private static final long serialVersionUID = 1L;
@ViewParameter
private Long id;
@Autowired
private Datastore datastore;
private PropertyInputForm form;
private Button clearButton;
@PostConstruct
public void init() {
Components.configure(this)
// set margins and size full to view content
.margin().fullSize().addAndExpandFull(
// add a form using Product property set
form = Components.input.form().fullSize().properties(PRODUCT)
// set id as read-only
.readOnly(ID)
// set SKU as required
.required(SKU)
// add a validator to check DESCRIPTION with minimum 3 characters
.withValidator(DESCRIPTION, Validator.min(3))
// build the form
.build())
.add(Components.hl().margin().spacing()
// SAVE action
.add(Components.button().caption("Save").styleName(ValoTheme.BUTTON_PRIMARY)
.onClick(e -> save()).build())
// CLEAR action
.add(clearButton = Components.button().caption("Clear")
// clear the form
.onClick(e -> form.clear()).build())
.build());
}
@OnShow
public void load() {
// if id parameter is not null, we are in edit mode
if (id != null) {
// 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("Data not found: " + id)));
}
// enable the Clear button if not in edit mode
clearButton.setVisible(id == null);
}
private void save() {
// check valid and get PropertyBox value
form.getValueIfValid().ifPresent(value -> {
// save and notify
datastore.save(TARGET, value, DefaultWriteOption.BRING_BACK_GENERATED_IDS);
// notify the saved id
Notification.show("Saved [" + ((id != null) ? id : value.getValue(ID)) + "]");
// go back home
ViewNavigator.require().navigateToDefault();
});
}
}
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 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.
See also
The source code of this tutorial is available on GitHub.
See the Vaadin Flow web application tutorial to learn how to use Vaadin Flow to build and run a simple web application.
See the Holon Platform tutorials to explore other available tutorials.