Building Modern Web Apps With Spring Boot and Vaadin
Building Modern Web Apps With Spring Boot and Vaadin
Building Modern Web Apps With Spring Boot and Vaadin
Why use Vaadin instead of Spring MVC and Thymeleaf, JSP or Freemarker? 3
Source code 4
Video tutorials 4
Setting up on Windows 6
Setting up on macOS 11
Setting up on Linux 13
Decoupling components 65
Setting up events 68
Defining routes 78
Testing Spring Boot apps with unit and integration tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Creating and running integration tests for more advanced UI logic 111
iii
Introduction: Building modern web apps with
Spring Boot and Vaadin
This guide is a practical introduction to web application development with Spring Boot
and Vaadin.
It covers the entire development process, from setup to deployment, following a step-by-
step approach. You can replicate each section at your own pace as you follow along.
The content is suitable for anyone familiar with Java who wants to build a web
application. To make sure your development experience is enjoyable and productive, we
start right from the beginning with setting up your development environment.
You can skip setting up a Java development environment and go ahead to chapter 4 if you
TIP
prefer to code using an online IDE.
• A responsive layout with side navigation that works on desktop and mobile.
• A dashboard view.
• Cloud deployment.
1
Tools and frameworks
The frameworks and tools used in the guide were chosen for two reasons: they are easy
to use, and they are suitable for both learning and production use.
On the back end, the application uses Spring Boot. This eliminates most of the hassle of
setting up and running a Spring-based app and lets you focus on your own code. The
main features you’ll use are:
Don’t worry if you don’t know what all of these are, we cover each individually as we go.
On the front end, the application uses Vaadin This is an open-source Java web app
framework that comes with:
2
• A large library of UI components. Each component has a Java API and you can
customize the look and feel.
• Vaadin is designed for building interactive single-page apps (SPA). Spring MVC and
templates are better suited for more static content.
• Vaadin offers a Java component API, allowing you to build the entire application in
Java.
• Vaadin handles the communication between the server and the browser
automatically, you don’t need to write any JavaScript to update content dynamically.
• Faster development: you do not need to develop the backend and frontend
separately.
• Vaadin handles the communication between the server and the browser
automatically, you don’t need to write any JavaScript to update content dynamically.
• More secure: the Vaadin app runs on the server and doesn’t expose application code
or extra endpoints to the browser.
3
Source code
You can find the full source code for this guide on GitHub. Each chapter is in a separate
branch. In addition, the individual chapters include links to download the code both
before and after the changes in the chapter, so you can easily get your project working
again, if something does go wrong. Each chapter has a link to the source code from the
previous chapter so you can start the tutorial at any chapter.
Video tutorials
You can find video tutorials of each chapter on the Vaadin YouTube Channel.
4
Setting up a Java development environment
Before you can start developing your Java app, you need to install the necessary
development tools and set up your development environment.
TIP You can skip to chapter 4 if you prefer to code using the online IDE.
• Java Development Kit (JDK): This is the foundation of your Java development
environment. It contains the various tools required to develop a working Java
application. Chief among them are the Java Runtime Environment (JRE), which allows
you to run a Java application and the Java compiler.
• Build/project management tool (Maven): We will use this to import our project into
the IDE and install any dependencies the project needs to function.
• Version control system (Git): Git lets you track and manage your web application’s
source code. For example, a correctly set-up Git repo would allow you to easily undo
any app breaking mistakes.
• Integrated development environment (IDE): The IDE is the tool you use to write and
run your code during development.
You can complete this guide using any IDE, but for the sake of simplicity, we use IntelliJ
Idea throughout. The IntelliJ Community Edition is free to use and a great choice if you
don’t already have an IDE setup.
If you have the necessary tools installed on your computer, or if you prefer to code using
the online IDE below, you can skip ahead to the Vaadin basics chapter.
5
When the Oracle JDK became a commercial product, many developers chose to switch
to alternative JDKs. There are many available drop-in replacements that are free to use
and come with long-term-support releases. We recommend you use either Amazon
Corretto or OpenJDK.
Setting up on Windows
To set up your development environment on Windows:
1. Install Java:
2. Install Maven:
6
c. Extract the archive to C:\Program Files.
d. In Windows, go to Control Panel > Systems and Security > System > Advanced
system settings.
f. In Environment Variables, select the Path systems variable (in the bottom box) and
then select Edit.
7
i. Select OK in all open dialogs to close them and save the environment variable.
3. Install Node:
b. Download and run the Windows Installer (.msi) for your system.
8
4. Install Git:
b. Download and run the Windows installer (.exe) for your system.
c. Follow the prompts in the wizard. If you are unsure about any option, use the
defaults.
9
5. Install IntelliJ:
10
d. Reboot your computer to finish the setup.
e. Start IntelliJ and set up your preferences. You can use the defaults, unless you have
reason not to.
Setting up on macOS
To set up your developer environment on macOS:
1. Install Java:
11
2. Install Homebrew:
Homebrew, is a package manager, and is the easiest way to install both Maven and
Node on macOS. To install Homebrew, paste the following into your terminal:
4. Install IntelliJ:
12
Setting up on Linux
This section contains instructions for Debian and RPM-based distros. Installation on
other distributions should be very similar and you can adapt these instructions, if
necessary. On Linux, it’s easiest to use OpenJDK, as it’s available in the package
repositories.
1. Install Node.js:
You need to install the latest Node.js LTS repository to your package manager. The
NOTE version available in most distributions is not sufficiently new for our purposes.
Nodesource offers packages for all major distros.
a. Debian-based systems:
ii. For Debian, or if you are not using sudo, run the following as root:
13
curl -sL https://deb.nodesource.com/setup_12.x | bash -
apt-get install -y openjdk-11-jdk maven git nodejs
• To ensure that you are running Java 11, run java -version.
If you are on a different distro, or aren’t comfortable with the automatic repo
NOTE setup script, you can find a full set of instructions on the NodeSource GitHub
repository.
2. Install IntelliJ:
The easiest way to install IntelliJ on Linux is to use the Snap package manager. If you
TIP
use Ubuntu or a derivative, it is already installed.
iv. Run the IDE (the start wizard gives you the option to create a desktop shortcut):
sh /opt/intellij/bin/idea.sh
14
You now have everything you need to start coding Java. The next tutorial will show you
how to import and run a Maven-based Java project in IntelliJ.
15
Importing, running, and debugging Maven
projects in IntelliJ IDEA
The first step in any project is to set up the project and get a base app running.
• How to set up your browser to automatically show updates as you build your
application.
TIP You can skip to chapter 4 if you prefer to code using the online IDE.
a. Go to https://vaadin.com/start.
e. Click Download.
16
2. Unzip the downloaded archive to a file location of your choice. TIP: Avoid unzipping to
the download folder, as you could unintentionally delete your project when clearing
out old downloads.
17
4. Find the extracted folder, and select the pom.xml file.
5. Select Open as Project. This imports a project based on the POM file.
18
6. IntelliJ imports the project and downloads all necessary dependencies. This can take
several minutes, depending on your internet connection speed.
When the import is complete, your project structure will be similar to this:
• Test files are in the src/test folder (we’ll refer to these later).
19
Running a Spring Boot project
Spring Boot makes it easier to run a Java web application, because it takes care of
starting and configuring the server.
To run your application, run the Application class that contains the main method that
starts Spring Boot. IntelliJ automatically detects that you have a class with a main
method and displays it in the run configurations dropdown.
• Open Application.java and click the play button next to the code line containing
the main method.
• After you have run the app once from the main method, it will show up in run
configurations dropdown in the main toolbar. On subsequent runs, you can run the
app from there.
The first time you start a Vaadin application, it downloads front-end dependencies and
builds a JavaScript bundle. This can take several minutes, depending on your computer
and internet speed.
20
You’ll know that your application has started when you see the following output in the
console:
You can now open localhost:8080 in your browser. You’ll see a Say hello button and Your
name field on the screen. Enter your name and click the button to see the notification
that displays.
1. If your application is still running from the previous step, click the red stop-button to
21
terminate it.
2. Start your application in debug mode, by clicking the bug icon next to the play
button. You can now insert a debug point. This tells the debugger to pause the app
whenever it gets to the line marked in the code.
You can now insert a debug point. This tells the debugger to pause the app whenever
it gets to the line marked in the code.
If you now open http://localhost:8080 in your browser, and click the Say hello button,
nothing happens. This is because the application stops on the line indicated in the
IDE.
4. In IntelliJ, have a look at the highlighted code line and the debug panel in the lower
part of the screen.
22
In the debug panel, you can see values for all variables. There are also controls that
allow you to run the app one step at a time, to better understand what’s happening.
The most important controls are:
• Step into: Drill into a method call (for instance, if youwanted to see what’s going on
inside service.greet()).
• Step out: Go back to the line of code that called the methodyou’re currently in.
23
Play around with the debugger to familiarize yourself with it. If you want to learn
more, JetBrains has an excellent resource on using the debugger.
Your code will now run normally and you’ll see the notification in your browser.
2. Install the plugin, reload your browser window, and click on the LiveReload icon in the
top bar of your browser. (Make sure your app is running when you do this.)
The middle of the icon should turn solid to indicate that LiveReload is working and
has connected to your app. If it doesn’t, try refreshing the page or reloading the
browser.
24
3. When LiveReload is running, verify that it works by making a change in the code:
a. Create a new H1 heading and add it as the first argument in the add() method on
the last line in MainView.
MainView.java
b. Click the build icon in IntelliJ (next to the run targets dropdown)
25
The first time you make a change with the debugger active, you’ll see a "Reload changed
classes now?" dialog. Select Do not ask again and click No. Spring Boot DevTools will take
care of the reload for us.
NOTE
1. If all goes well, you’ll see a notification that the build was successful, and your browser
will reload automatically to show the change. Magic.
26
You may sometimes see error messages like this in the browser after a reload.
or
NOTE
There was an exception while trying to navigate to '' with the exception
message 'Error creating bean with name 'com.vaadin.tutorial.crm.MainView':
Unsatisfied dependency expressed through constructor parameter 0
These errors are caused by a Spring DevTools reload timing issue. You may be able to
alleviate the issue by adding the following two properties to
src/main/resources/application.properties and adjusting the intervals to work with your
computer. Stop and restart the server after adding the properties.
application.properties
spring.devtools.restart.poll-interval=2s
spring.devtools.restart.quiet-period=1s
1. Open the Preferences/Settings window and navigate to Editor > General > Auto
Import.
27
Vaadin shares many class names (like Button) with Swing, AWT, and JavaFX.
3. If you don’t use Swing, AWT, or JavaFX in other projects, add the following packages
to the Exclude from import and completion list to help IntelliJ select the correct
classes automatically.
• com.sun
• java.awt
• javafx.scene
• javax.swing
• jdk.internal
• sun.plugin
Now that you have a working development environment, we can start building a web
app.
You can find the completed source code for this tutorial on GitHub.
28
Vaadin basics: building UIs with components and
layouts
In the previous two tutorials, you learned to set up a Java development environment and
to import and run a Maven-based Spring Boot project.
In this chapter we introduce you to core Vaadin concepts and get our project ready for
coding.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
If you prefer to use a drag and drop editor to build the UI, check out the Vaadin Designer
TIP
version of this tutorial.
What is Vaadin?
Vaadin is a Java framework for building web applications. It has a component-based
programming model that allows you to build user interfaces.
Vaadin UI components
The Vaadin platform includes a large library of UI components that you can use as the
building blocks of your application.
You create a new component by initializing a Java object. For instance, to create a
Button, you write:
Layouts
Layouts determine how components display in the browser window. The most common
layout components are HorizontalLayout, VerticalLayout, and Div. The first two set
29
the content orientation as horizontal or vertical, respectively, whereas Div lets you
control the positioning with CSS.
layout.setDefaultVerticalComponentAlignment(Alignment.END);
add(layout);
Events
You can add functionality to your application by listening to events, such as, click events
from buttons, or value-change events from select components.
This example adds the text "Clicked!" to the layout when the button is clicked.
button.addClickListener(clickEvent ->
add(new Text("Clicked!")));
30
Vaadin also supports HTML-templates and customizing the code that runs in the
browser, but in most cases you needn’t worry about this.
Defining packages
Our app has both UI and backend code. To keep our code organized, we need to define
separate packages for each in the project structure.
To define packages:
4. Drag MainView into the ui package. If IntelliJ asks you if you want to refactor the code,
say yes.
31
Setting up the main layout
Next, we clean out unnecessary code and set up our main layout.
To do this:
32
1. Delete the content of MainView and replace it with the code shown below. This
removes all unnecessary code and ensures that we start with a clean slate.
MainView.java
package com.vaadin.tutorial.crm.ui;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@Route("") ①
public class MainView extends VerticalLayout {
public MainView() {
}
• GreetService.java
• frontend/styles/vaadin-text-field-styles.css
You should see an empty window in the browser, and no errors in the console.
33
Before we can start building the UI, we need data to work with. In the next chapter, we’ll
set up a database and populate it with test data.
You can find the completed source code for this tutorial on GitHub.
34
Creating a Spring Boot backend: database, JPA
repositories, and services
Most real-life applications need to persist and retrieve data from a database. In this
tutorial, we use an in-memory H2 database. You can easily adapt the configuration to use
another database, like MySQL or Postgres.
There are a fair number of classes to copy and paste to set up your backend. You can
make your life easier by downloading a project with all the changes, if you prefer. The
download link is at the end of this chapter.
The code from the previous tutorial chapter can be found here, if you want to jump
directly into this chapter.
1. In the <dependencies> tag in your pom.xml file, add the following dependencies for
H2 and Spring Data:
pom.xml
<dependencies>
<!--all existing dependencies -->
2. Save your file and when IntelliJ asks if you want to enable automatic importing of
Maven dependencies, select Enable Auto-Import.
35
If IntelliJ doesn’t ask you to import dependencies, or if you use another IDE, type mvn
install in the command line (while in the root of your project folder) to download
the dependencies.
H2 is a great database for tutorials because you don’t need to install external software. If
you prefer, you can easily change to another database. See:
•
NOTE
Setting up MySQL
• Setting up Postgres
The instructions in the remainder of this tutorial are the same, regardless of which
database you use. To keep things simple, we recommend sticking with H2.
2. Create three classes, AbstractEntity, Contact, and Company, in the new package,
using the code detailed below.
The easiest way to do this is to copy the full class and paste it into the package in the
project view. IntelliJ (and most other IDEs) will automatically create the Java file for
you.
36
a. Start by adding AbstractEntity, the common superclass. It defines how objects
ids are generated and how object equality is determined.
37
AbstractEntity.java
package com.vaadin.tutorial.crm.backend.entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class AbstractEntity {
@Id
@GeneratedValue(strategy= GenerationType.SEQUENCE)
private Long id;
@Override
public int hashCode() {
if (getId() != null) {
return getId().hashCode();
}
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AbstractEntity other = (AbstractEntity) obj;
if (getId() == null || other.getId() == null) {
return false;
}
return getId().equals(other.getId());
}
}
Contact.java
38
package com.vaadin.tutorial.crm.backend.entity;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Entity
public class Contact extends AbstractEntity implements Cloneable {
@NotNull
@NotEmpty
private String firstName = "";
@NotNull
@NotEmpty
private String lastName = "";
@ManyToOne
@JoinColumn(name = "company_id")
private Company company;
@Enumerated(EnumType.STRING)
@NotNull
private Contact.Status status;
@Email
@NotNull
@NotEmpty
private String email = "";
39
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return firstName + " " + lastName;
}
40
Company.java
package com.vaadin.tutorial.crm.backend.entity;
import javax.persistence.*;
import java.util.LinkedList;
import java.util.List;
@Entity
public class Company extends AbstractEntity {
private String name;
public Company() {
}
If you see a lot of errors about missing classes, double check the Maven dependencies
and run mvn install to make sure they are downloaded.
41
1. Create a new package com.vaadin.tutorial.crm.backend.repository.
ContactRepository.java
package com.vaadin.tutorial.crm.backend.repository;
import com.vaadin.tutorial.crm.backend.entity.Contact;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
CompanyRepository.java
package com.vaadin.tutorial.crm.backend.repository;
import com.vaadin.tutorial.crm.backend.entity.Company;
import org.springframework.data.jpa.repository.JpaRepository;
42
ContactService.java
package com.vaadin.tutorial.crm.backend.service;
import com.vaadin.tutorial.crm.backend.entity.Contact;
import com.vaadin.tutorial.crm.backend.repository.CompanyRepository;
import com.vaadin.tutorial.crm.backend.repository.ContactRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
@Service ①
public class ContactService {
private static final Logger LOGGER = Logger.getLogger(ContactService.class
.getName());
private ContactRepository contactRepository;
private CompanyRepository companyRepository;
① The @Service annotation lets Spring know that this is a service class and makes it
available for injection. This allows you to easily use it from your UI code later on.
② The constructor takes 2 parameters: ContactRepository and CompanyRepository.
43
Spring provides instances based on the interfaces we defined earlier.
③ For now, most operations are just passed through to the repository. The only
exception is the save method, which checks for null values before attempting to
save.
CompanyService.java
package com.vaadin.tutorial.crm.backend.service;
import com.vaadin.tutorial.crm.backend.entity.Company;
import com.vaadin.tutorial.crm.backend.repository.CompanyRepository;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class CompanyService {
44
ContactService.java
@PostConstruct ①
public void populateTestData() {
if (companyRepository.count() == 0) {
companyRepository.saveAll( ②
Stream.of("Path-Way Electronics", "E-Tech Management", "Path-E-Tech
Management")
.map(Company::new)
.collect(Collectors.toList()));
}
if (contactRepository.count() == 0) {
Random r = new Random(0);
List<Company> companies = companyRepository.findAll();
contactRepository.saveAll( ③
Stream.of("Gabrielle Patel", "Brian Robinson", "Eduardo Haugen",
"Koen Johansen", "Alejandro Macdonald", "Angel Karlsson", "Yahir
Gustavsson", "Haiden Svensson",
"Emily Stewart", "Corinne Davis", "Ryann Davis", "Yurem Jackson",
"Kelly Gustavsson",
"Eileen Walker", "Katelyn Martin", "Israel Carlsson", "Quinn
Hansson", "Makena Smith",
"Danielle Watson", "Leland Harris", "Gunner Karlsen", "Jamar Olsson
", "Lara Martin",
"Ann Andersson", "Remington Andersson", "Rene Carlsson", "Elvis
Olsen", "Solomon Olsen",
"Jaydan Jackson", "Bernard Nilsen")
.map(name -> {
String[] split = name.split(" ");
Contact contact = new Contact();
contact.setFirstName(split[0]);
contact.setLastName(split[1]);
contact.setCompany(companies.get(r.nextInt(companies.size())));
contact.setStatus(Contact.Status.values()[r.nextInt(Contact
.Status.values().length)]);
String email = (contact.getFirstName() + "." + contact
.getLastName() + "@" + contact.getCompany().getName().replaceAll("[\\s-]", "") +
".com").toLowerCase();
contact.setEmail(email);
return contact;
}).collect(Collectors.toList()));
}
}
① The @PostConstruct annotation tells Spring to run this method after constructing
ContactService.
② Creates 3 test companies.
45
Restart the server to pick up all the new dependencies
You need to stop and restart the application to make sure all the new POM
dependencies are picked up correctly.
You can download the project with a fully set-up back end below. Unzip the project and
follow the instructions in the importing chapter.
In the next chapter, we’ll use the back end to populate data into a data grid in the
browser.
46
Adding data and configuring columns in Vaadin
Grid
Now that your back end is in place, you’re ready to begin building the UI. We start by
listing all contacts in a data grid. To do this, we need to create the necessary UI
components and hook them up to the back end.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
MainView.java
@Route("")
public class MainView extends VerticalLayout {
public MainView() {
addClassName("list-view"); ②
setSizeFull(); ③
configureGrid(); ④
add(grid); ⑤
}
}
③ Calls setSizeFull() to make MainView take up the full size of the browser
47
window.
④ Splits the grid configuration into a separate method. We will add more
components to the class later on and this helps to keep things easy to understand.
⑤ Adds the grid to the main layout using the add(grid) method.
2. Run the application and verify that you now see an empty grid with the correct
columns.
MainView.java
@Route("")
public class MainView extends VerticalLayout {
add(grid);
updateList(); ②
}
② Creates a new method, updateList(), that fetches all contacts from the service, and
passes them to the grid.
2. Build the project and verify that you now see contacts listed in the grid.
49
Adding a custom column to the grid
In the previous chapter, when we set up the data model, we defined that each Contact
belongs to a Company. We now want to show that company information as a column in
the grid. The problem is that if we add "company" to the column list, the grid doesn’t
show the company name, but something like "com.vaadin.tutorial.crm.backend.e…"
instead.
The company property is a reference to another object, and the grid shows the fully
qualified class name because it doesn’t know how we want to display the object. To fix
this, we need to change how the company column is defined.
50
MainView.java
2. Build the application, and you should now see the company names listed in the grid.
51
Defining column widths
By default, the grid makes each column equally wide. Let’s turn on automatic column
sizing so that the email and company fields, which are typically longer, get more space.
Automatic column sizing tries to make the column wide enough to fit the widest
content.
MainView.java
52
① Automatic width needs to be turned on for each column separately. The easiest
way to do it is to call grid.getColumns() and then use forEach to loop over all of
them.
2. Build the app and you should now see that the columns are sized more appropriately.
In the next tutorial, we’ll add filtering to the application, so it’s easier to find the right
contact.
You can find the completed source code for this tutorial on GitHub.
53
Filtering rows in Vaadin Grid
In the previous tutorial, we created a data grid and filled it with contact details stored in
the database. In this tutorial, we continue by adding a text field to filter the contents of
the grid.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
MainView.java
add(filterText, grid); ③
updateList();
}
② Calls the configureFilter() method to configure what the filter should do.
③ Updates the add() method call to add both filterText and grid.
54
⑤ Sets the clear button to visible so users can easily clear the filter.
⑥ Sets the value change mode to LAZY so the text field will notify you of changes
automatically after a short timeout in typing.
⑦ Calls the updateList method whenever the value changes. We’ll update the logic to
filter the content shortly.
1. Keep a copy of the contacts list in the view and filter it using Java streams.
It’s a best practice to avoid keeping references to lists of objects in Vaadin views, as this
can lead to excessive memory usage.
55
ContactService.java
② If the filter text is not empty, search the database for that text.
ContactRepository.java
① Uses the @Query annotation to define a custom query. In this case, it checks if the
string matches the first or the last name, and ignores the case. The query uses Java
Persistence Query Language (JPQL) which is an SQL-like language for querying
JPA managed databases.
56
② Selects the Spring Framework import for @Param.
3. Update the way MainView updates the contacts. This is the method that is called
every time the filter text field changes.
MainView.java
4. Build the application and try out the filtering. You should be able to filter the contacts
by entering a term in the text field.
So far, we’ve created an application that shows and filters contacts that are stored in a
database. Next, we’ll add a form to add, remove, and edit contacts.
You can find the completed source code for this tutorial on GitHub.
57
Creating your own reusable components in
Vaadin
In the previous tutorial, we added filtering to the grid that lists contacts stored in a
database. In this tutorial, we build a form component to add, remove, and edit contacts.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
Instead of building an entire view in a single class, your view can be composed of smaller
components that each handle different parts of the view. The advantage of this
approach is that individual components are easier to understand and test. The top-level
view is used mainly to orchestrate the components.
• An email field.
• Two select fields: one to select the company and the other to select the contact
status.
To create the form, copy the code below and paste it into the ui package. IntelliJ will
create a new Java class, ContactForm.
58
ContactForm.java
public ContactForm() {
addClassName("contact-form"); ④
add(firstName,
lastName,
email,
company,
status,
createButtonsLayout()); ⑤
}
save.addClickShortcut(Key.ENTER); ⑦
close.addClickShortcut(Key.ESCAPE);
59
columns depending on viewport width.
② Creates all the UI components as fields in the component.
③ Uses the com.vaadin.ui import for Button, not the one from the crud package.
⑤ Adds all the UI components. The buttons require a bit of extra configuration so we
create and call a new method, createButtonsLayout().
⑥ Makes the buttons visually distinct from each other using built-in theme variants.
⑦ Defines keyboard shortcuts: Enter to save and Escape to close the editor
configureGrid();
configureFilter();
add(filterText, content); ④
updateList();
}
// other methods omitted.
}
60
① Creates a field for the form so you have access to it from other methods later on.
③ Creates a Div that wraps the grid and the form, gives it a CSS class name, and makes
it full size.
④ Adds the content layout to the main layout.
shared-styles.css
/* List view */
.list-view .content {
display: flex; ①
}
.list-view .contact-grid {
flex: 2; ②
}
.list-view .contact-form {
flex: 1;
padding: var(--lumo-space-m); ③
}
② Allocates 2/3 of the available width to the grid and 1/3 to the form.
③ Uses the Vaadin Lumo theme custom property, --lumo-space-m, to add standard
padding in the form
④ Hides the toolbar and grid when editing on narrow screens (we’ll add some logic to
handle this shortly).
61
Importing CSS styles into main the view
Next, we load the CSS file by adding a CssImport annotation in MainView.
MainView.java
@Route("")
@CssImport("./styles/shared-styles.css") ①
public class MainView extends VerticalLayout {
...
}
3. Verify that the main view looks the way it should. The form should now display next to
the grid.
62
The visual part of the form is now complete. In the next tutorial, we’ll make it functional.
You can find the completed source code for this tutorial on GitHub.
63
Vaadin form data binding and validation
In the previous tutorial, we started building a reusable form component. In this tutorial,
we define the form component API and use data binding to make the form functional.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
Vaadin users the Binder class to build forms. It binds UI fields to data object fields by
name. For instance, it takes a UI field named firstName and maps it to the firstName
field of the data object, and the lastName field to the lastName field, and so on. This is
why the field names in Contact and ContactForm are the same.
Binder also supports an advanced API where you can configure data conversions and
additional validation rules, but for this application, the simple API is sufficient.
NOTE
Binder can use validation rules that are defined on the data object in the UI. This means you
can run the same validations in both the browser and before saving to the database,
without duplicating code.
ContactForm.java
public ContactForm() {
addClassName("contact-form");
binder.bindInstanceFields(this); ②
// Rest of constructor omitted
}
64
① BeanValidationBinder is a Binder that is aware of bean validation annotations. By
passing it in the Contact.class, we define the type of object we are binding to.
② bindInstanceFields matches fields in Contact and ContactForm based on their
names.
With these two lines of code, you’ve made the UI fields ready to be connected to a
contact. We’ll do that next.
Decoupling components
Object-oriented programming allows you to decouple objects and this helps to increase
their reusability.
In our app, we create a form component and use it in the MainView. The most
straightforward approach would appear to be to let the form call methods on MainView
directly to save a contact. But what happens if you need the same form in another view?
Or if you want to write a test to verify that the form works as intended? In both cases, the
dependency on MainView makes it more complex than is necessary. Coupling a
component to a specific parent typically makes it more difficult to reuse and test.
Instead, we should aim to make components that work in the same way as a Button
component: you can use them anywhere. You configure the component by setting
properties, and it notifies you of events through listeners.
A good rule of thumb when designing an API for a reusable component is: properties in,
events out. Users should be able to fully configure a component by setting
properties.They should be notified of all relevant events, without the need to manually
call getters to see if things have changed.
Properties in:
65
• Set the contact.
Events out:
• Save.
• Delete.
• Close.
ContactForm.java
company.setItems(companies); ②
company.setItemLabelGenerator(Company::getName); ③
status.setItems(Contact.Status.values()); ④
//omitted
}
② Sets the list of companies as the items in the company combo box.
③ Tells the combo box to use the name of the company as the display value.
④ Populates the status dropdown with the values from the Contact.Status enum.
You will get a compilation error if you build the application at this point. This is
WARNING
because you have not yet passed a list of companies in MainView.
66
MainView.java
configureGrid();
configureFilter();
ContactForm.java
① Save a reference to the contact so we can save the form values back into it later.
② Calls binder.readBean to bind the values from the contact to the UI fields. readBen
copies the values from the Contact to an internal model, that way we don’t
accidentally overwrite values if we cancel editing.
67
Setting up events
Vaadin comes with an event-handling system for components. We’ve already used it to
listen to value-change events from the filter text field. We want the form component to
have a similar way of informing parents of events.
ContactForm.java
// Events
public static abstract class ContactFormEvent extends ComponentEvent<ContactForm> {
private Contact contact;
① ContactFormEvent is a common superclass for all the events. It contains the contact
that was edited or deleted.
② The addListener method uses Vaadin’s event bus to register the custom event types.
68
Select the com.vaadin import for Registration if IntelliJ asks.
To add save, delete and close event listeners, add the following to the ContactForm
class:
ContactForm.java
② The delete button fires a delete event and passes the currently-edited contact.
④ Validates the form every time it changes. If it is invalid, it disables the save button to
avoid invalid submissions.
⑤ Write the form contents back to the original contact.
⑥ Fire a save event so the parent component can handle the action.
In the next tutorial, we’ll connect the form to the main view so that the selected contact
in the form can be edited.
You can find the completed source code for this tutorial on GitHub.
69
Passing data and events between Vaadin
components
In the previous tutorial, we created a reusable form component to edit contacts. In this
tutorial we’ll hook it up to our application. Our form will:
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
70
MainView.java
grid.asSingleSelect().addValueChangeListener(event -> ②
editContact(event.getValue()));
}
① The closeEditor() call at the end of the constructor Sets the form contact to null,
clearing out old values. Hides the form. Removes the "editing" CSS class from the
view.
② addValueChangeListener adds a listener to the grid. The Grid component supports
multi and single-selection modes. We only want to select a single Contact, so we use
the asSingleSelect() method. The getValue() method returns the Contact in the
selected row or null if there’s no selection.
③ editContact sets the selected contact in the ContactForm and hides or shows the
form, depending on the selection. It also sets the "editing" CSS class name when
editing.
Build the application. You should now be able to select contacts in the grid and see them
in the form. But, none of the buttons work yet.
71
Handling form events We designed the ContactForm API to be reusable: it is
configurable through properties and it fires the necessary events. So far, we’ve passed a
list of companies and the contact to the form. Now all we need to do is listen for the
events to complete the integration.
72
MainView.java
configureGrid();
configureFilter();
add(filterText, content);
updateList();
closeEditor();
}
73
③ Close closes the editor.
Build the application and verify that you are now able to update and delete contacts.
1. In MainView, create a HorizontalLayout that wraps the text field and the button,
rename the configureFilter method to getToolbar, and replace its contents, as
follows:
74
MainView.java
① Returns a HorizontalLayout.
③ Adds a HorizontalLayout with the filter input field and a button, gives it a CSS
class name "toolbar" that is used for the responsive layouting.
MainView.java
void addContact() {
grid.asSingleSelect().clear(); ①
editContact(new Contact()); ②
}
75
MainView.java
add(getToolbar(), content); ②
updateList();
}
Build the application and verify that you are now able to add new contacts. New contacts
are added at the end of the list, so you may need to scroll or use the filter to find them.
76
In the next tutorial, we’ll add a second screen to the application and learn how to
navigate between views.
You can find the completed source code for this tutorial on GitHub.
77
Navigating between views in Vaadin
So far in this tutorial series, we’ve built a CRM application for listing and editing contacts.
In this chapter, we add a dashboard view to the application. We also add a responsive
application layout, with a header and a navigation sidebar that can be toggled on small
screens.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
Defining routes
Any Vaadin component can be made a navigation target by adding an
@Route("<path>") annotation. Routes can be nested by defining the parent layout in the
annotation: @Route(value = "list", parent=MainView.class).
Let’s begin:
1. Start by renaming MainView to ListView. Right click MainView and select Refactor >
Rename. When IntelliJ asks if you want to rename the file, answer yes.
78
2. Create a new Java class named MainLayout with the following content. This is the
shared parent layout of both views in the application.
79
MainLayout.java
@CssImport("./styles/shared-styles.css") ①
public class MainLayout extends AppLayout { ②
public MainLayout() {
createHeader();
createDrawer();
}
header.setDefaultVerticalComponentAlignment(
FlexComponent.Alignment.CENTER); ④
header.setWidth("100%");
header.addClassName("header");
addToNavbar(header); ⑤
}
addToDrawer(new VerticalLayout(listLink)); ⑧
}
}
⑥ Creates a RouterLink with the text "List" and ListView.class as the destination
view
⑦ Sets setHighlightCondition(HighlightConditions.sameLocation()) to avoid
highlighting the link for partial route matches. (Technically, every route starts with
an empty route, so without this it would always show up as active even though the
user is not on the view)
⑧ Wraps the link in a VerticalLayout and adds it to the `AppLayout’s drawer
80
3. Add the following CSS to frontend/styles/shared-styles.css
shared-styles.css
/* Main layout */
a[highlight] {
font-weight: bold;
text-decoration: underline;
}
.header {
padding: 0 var(--lumo-space-m);
}
.header h1.logo {
font-size: 1em;
margin: var(--lumo-space-m);
}
5. Move ListView and ContactForm into the new package. The resulting package
structure should look like this:
81
6. Finally, in ListView update the @Route mapping to use the new MainLayout and
delete the @CSSImport annotation.
ListView.java
① ListView still matches the empty path, but now uses MainLayout as its parent.
7. Run the application. You should now see a header and a sidebar on the list view.
82
Creating the dashboard view
Now let’s create a new dashboard view. This view will show stats: the number of contacts
in the system and a pie chart of the number of contacts per company.
83
1. Create a new package com.vaadin.tutorial.crm.ui.view.dashboard by right
clicking the list package and selecting New > Package.
84
DashboardView.java
package com.vaadin.tutorial.crm.ui.view.dashboard;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.tutorial.crm.backend.service.CompanyService;
import com.vaadin.tutorial.crm.backend.service.ContactService;
import com.vaadin.tutorial.crm.ui.MainLayout;
DashboardView.java
85
shared-styles.css
/* Dashboard view */
.dashboard-view .contact-stats {
font-size: 4em;
margin: 1em 0;
}
CompanyService.java
① Loops through each company and returns a Map containing the company name
and number of employees.
DashboardView.java
86
③ Adds a DataSeriesItem, containing the company name and number of employees,
for each company.
④ Sets the data series to the chart configuration.
DashboardView.java
add(getContactStats(), getCompaniesChart());
}
MainLayout.java
addToDrawer(new VerticalLayout(
listLink,
new RouterLink("Dashboard", DashboardView.class)
));
}
9. Build and run the application. You should now be able to navigate to the dashboard
view and see stats on your CRM contacts. If you want to, go ahead and add or remove
contacts in the list view to see that the dashboard reflects your changes.
87
In the next tutorial, we’ll secure the application by adding a login screen.
You can find the completed source code for this tutorial on GitHub.
88
Adding a login screen to a Vaadin app with Spring
Security
So far in this tutorial series, we’ve built a CRM application that has one view for listing
and editing contacts, and a dashboard view for showing stats.
In this tutorial we set up Spring Security and add a login screen to limit access to logged
in users.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
89
LoginView.java
package com.vaadin.tutorial.crm.ui.view.login;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.login.LoginForm;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import java.util.Collections;
@Route("login") ①
@PageTitle("Login | Vaadin CRM")
public LoginView(){
addClassName("login-view");
setSizeFull();
setAlignItems(Alignment.CENTER); ③
setJustifyContentMode(JustifyContentMode.CENTER);
login.setAction("login"); ④
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
// inform the user about an authentication error
if(beforeEnterEvent.getLocation() ⑤
.getQueryParameters()
.getParameters()
.containsKey("error")) {
login.setError(true);
}
}
}
① Maps the view to the "login" path. LoginView should take up the whole browser
window, so we don’t use MainLayout as the parent.
② Instantiates a LoginForm component to capture username and password.
③ Makes LoginView full size and centers its content both horizontally and vertically,
by calling setAlignItems(Alignment.CENTER) and
setJustifyContentMode(JustifyContentMode.CENTER).
④ Sets the LoginForm action to "login" to post the login form to Spring Security.
90
⑤ Reads query parameters and shows an error if a login attempt fails.
3. Build the application and navigate to http://localhost/login. You should see a centered
login form.
91
pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
2. Check that the dependencies are downloaded. If you enabled automatic downloads
in an earlier tutorial, you’re all set. If you didn’t, or are unsure, run mvn install from
the command line to download the dependencies.
3. Next, disable Spring MVC auto configuration on the Application class, as this
interferes with how Vaadin works and can cause strange reloading behavior.
Application.class
@SpringBootApplication(exclude = ErrorMvcAutoConfiguration.class)
public class Application extends SpringBootServletInitializer {
...
}
2. In the new package create the following classes using the code detailed below:
Paste the class code into the package and IntelliJ will automatically create the class
TIP
for you.
a. SecurityUtils
92
SecurityUtils.java
package com.vaadin.tutorial.crm.security;
import com.vaadin.flow.server.ServletHelper;
import com.vaadin.flow.shared.ApplicationConstants;
import
org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Stream;
private SecurityUtils() {
// Util methods only
}
b. CustomRequestCache
93
CustomRequestCache.java
package com.vaadin.tutorial.crm.security;
import
org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Override
public void saveRequest(HttpServletRequest request, HttpServletResponse
response) { ①
if (!SecurityUtils.isFrameworkInternalRequest(request)) {
super.saveRequest(request, response);
}
}
① Saves unauthenticated requests so we can redirect the user to the page they
were trying to access once they’re logged in.
c. SecurityConfiguration
94
SecurityConfiguration.java
package com.vaadin.tutorial.crm.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
import
org.springframework.security.config.annotation.web.builders.WebSecurity;
import
org.springframework.security.config.annotation.web.configuration.EnableWebS
ecurity;
import
org.springframework.security.config.annotation.web.configuration.WebSecurit
yConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import
org.springframework.security.provisioning.InMemoryUserDetailsManager;
@EnableWebSecurity ①
@Configuration ②
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
② @Configuration tells Spring Boot to use this class for configuring security.
3. Add a method to block unauthenticated requests to all pages, except the login page.
95
SecurityConfiguration.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() ①
.requestCache().requestCache(new CustomRequestCache()) ②
.and().authorizeRequests() ③
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
④
.anyRequest().authenticated() ⑤
.and().formLogin() ⑥
.loginPage(LOGIN_URL).permitAll()
.loginProcessingUrl(LOGIN_PROCESSING_URL) ⑦
.failureUrl(LOGIN_FAILURE_URL)
.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL); ⑧
}
① Disables cross-site request forgery (CSRF) protection, as Vaadin already has CSRF
protection.
② Uses CustomRequestCache to track unauthorized requests so that users are
redirected appropriately after login.
③ Turns on authorization.
SecurityConfiguration.java
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user =
User.withUsername("user")
.password("{noop}password")
.roles("USER")
.build();
96
• Defines a single user with the username "user" and password "password" in an
in-memory DetailsManager.
We do not recommend that you configure users directly in the code for
applications in production. You can easily change this Spring Security
WARNING
configuration to use an authentication provider for LDAP, JAAS, and other real
world sources. Read more about Spring Security authentication providers.
SecuirtyConfiguration.java
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/VAADIN/**",
"/favicon.ico",
"/robots.txt",
"/manifest.webmanifest",
"/sw.js",
"/offline.html",
"/icons/**",
"/images/**",
"/styles/**",
"/h2-console/**");
}
97
ConfigureUIServiceInitListener.java
package com.vaadin.tutorial.crm.security;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.tutorial.crm.ui.view.login.LoginView;
import org.springframework.stereotype.Component;
@Component ①
public class ConfigureUIServiceInitListener implements VaadinServiceInitListener {
@Override
public void serviceInit(ServiceInitEvent event) {
event.getSource().addUIInitListener(uiEvent -> { ②
final UI ui = uiEvent.getUI();
ui.addBeforeEnterListener(this::authenticateNavigation);
});
}
① The @Component annotation registers the listener. Vaadin will pick it up on startup.
② In serviceInit, we listen for the initialization of the UI (the internal root component
in Vaadin) and then add a listener before every view transition.
③ In authenticateNavigation, we reroute all requests to the login, if the user is not
logged in
TIP You can read more about fine-grained access control in the Spring Security tutorial series.
98
MainLayout.java
addToNavbar(header);
}
③ Calls header.expand(logo) to make the logo take up all the extra space in the
layout. This pushes the logout button to the far right.
2. Stop and restart the server to pick up the new Maven dependencies. You should now
be able to log in and out of the app. Verify that you can’t access http://localhost/
dashboard without being logged in.
99
You have now built a full-stack CRM application with navigation and authentication. In
the next tutorial, you’ll learn how to make the application installable on mobile and
desktop.
You can find the completed source code for this tutorial on GitHub.
100
Turning a Vaadin app into an installable PWA
In this chapter, we turn the completed CRM application into a progressive web
application (PWA) so that users can install it.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
What is a PWA?
The term PWA is used to describe modern web applications that offer a native app-like
user experience. PWA technologies make applications faster, more reliable, and more
engaging. PWAs can be installed on most mobile devices and on desktop when using
supported browsers. They can even be listed in the Microsoft Store and Google Play
Store. You can learn more about the underlying technologies and features in the Vaadin
PWA documentation.
• ServiceWorker: A JavaScript worker file that controls network traffic and enables
custom cache control.
• Web app manifest: A JSON file that identifies the web app as an installable app.
MainLayout.java
@CssImport("./styles/shared-styles.css")
@PWA( ①
name = "VaadinCRM", ②
shortName = "CRM") ③
public class MainLayout extends AppLayout {
...
}
① The @PWA annotation tells Vaadin to create a ServiceWorker and a manifest file.
② name is the full name of the application for the manifest file.
101
③ shortName should be short enough to fit under an icon when installed, and should not
exceed 12 characters.
You can use your own icon, or save the image below, by right clicking and selecting Save
Image.
102
Customizing the offline page
Vaadin creates a generic offline fallback page that displays when the application is
launched offline. Replacing this default page with a custom page that follows your own
design guidelines makes your app more polished.
offline.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>Offline | Vaadin CRM</title>
<link rel="stylesheet" href="./styles/offline.css"> ①
</head>
<body>
<div class="content">
<img src="./images/offline.png" alt="VaadinCRM is offline" class="offline-
image"/> ②
<h1>Oh deer, you're offline</h1>
<p>Your internet connection is offline. Get back online to continue using
Vaadin CRM.</p>
</div>
<script>
window.addEventListener('online', location.reload);
</script> ③
</body>
</html>
③ The JavaScript snippet reloads the page if the browser detects that it’s back online.
3. In the styles folder, create offline.css and add the following styles:
103
offline.css
body {
display: flex; ①
flex-direction: column;
align-items: center;
font-family: sans-serif;
color: #555;
}
.content {
width: 80%;
}
.offline-image {
width: 100%;
margin: 4em 0px;
}
4. Add the following image (or use one of your own) to the images folder and name it
offline.png.
5. Make the files available offline by adding them to the @PWA annotation in MainLayout
as follows:
MainLayout.java
@CssImport("./styles/shared-styles.css")
@PWA(
name = "VaadinCRM",
shortName = "VaadinCRM",
offlineResources = { ①
"./styles/offline.css",
"./images/offline.png"})
public class MainLayout extends AppLayout {
...
}
① offlineResources is a list of files that Vaadin will make available offline through
the ServiceWorker.
104
Even though the paths for the CSS files is identical in the Java file, shared-
styles.css is loaded from frontend/styles/shared-styles.css, whereas
WARNING offline.css is loaded from src/main/java/webapp/styles/offline.css.
If you have trouble accessing files while offline, check that these files are in the
correct folders.
6. Restart the app. On supported browsers, your will now see an install prompt that you
can use to install the application:
105
In the next chapter, we cover testing the application: both unit tests and in-browser
tests.
You can find the completed source code for this tutorial on GitHub.
106
Testing Spring Boot apps with unit and
integration tests
It is a common best practice to test as little code as possible in a single test. In this way,
when things go wrong, only relevant tests fail. For UI testing, there are three main
approaches:
Unit and integration tests can be run standalone, that is, without any external
dependencies, such as a running server or database.
End-to-end tests require the application to be deployed and are run in a browser window
to simulate an actual user.
In this chapter, we write and run unit and integration tests. We cover end-to-end tests in
the next chapter.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
All test classes should go in the test folder, src/test/java. Pay special attention to
CAUTION the package names. We use package-access to class fields. If the test isn’t in the same
package as the class that’s being tested, you will get errors.
1. Start by deleting the tests included with the starter: AbstractViewTest.java` and
LoginViewIT.java.
107
ContactFormTest.java
@Before
public void setupData() {
companies = new ArrayList<>();
company1 = new Company("Vaadin Ltd");
company2 = new Company("IT Mill");
companies.add(company1);
companies.add(company2);
• The @Before annotation adds dummy data that is used for testing. This method is
executed before each @Test method.
ContactFormTest.java
@Test
public void formFieldsPopulated() {
ContactForm form = new ContactForm(companies);
form.setContact(marcUsher); ①
Assert.assertEquals("Marc", form.firstName.getValue());
Assert.assertEquals("Usher", form.lastName.getValue());
Assert.assertEquals("marc@usher.com", form.email.getValue());
Assert.assertEquals(company2, form.company.getValue());
Assert.assertEquals(Contact.Status.NotContacted, form.status.getValue()); ②
}
① Validates that the fields are populated correctly, by first initializing the contact
form with some companies, and then setting a contact bean for the form.
② Uses standard JUnit assertEquals methods to compare the values from the fields
available through the ContactForm instance:
108
ContactFormTest.java
@Test
public void saveEventHasCorrectValues() {
ContactForm form = new ContactForm(companies);
Contact contact = new Contact();
form.setContact(contact);
}
saveEventHasCorrectValues()
form.firstName.setValue("John");
form.lastName.setValue("Doe");
form.company.setValue(company1);
form.email.setValue("john@doe.com");
form.status.setValue(Contact.Status.Customer);
saveEventHasCorrectValues()
Assert.assertEquals("John", savedContact.getFirstName());
Assert.assertEquals("Doe", savedContact.getLastName());
Assert.assertEquals("john@doe.com", savedContact.getEmail());
Assert.assertEquals(company1, savedContact.getCompany());
Assert.assertEquals(Contact.Status.Customer, savedContact.getStatus()); ③
① As ContactForm fires an event on save and the event data is needed for the test,
an AtomicReference is used to store the event data, without using a class field.
② Clicks the save button and asserts that the values from the fields end up in the
bean.
③ Once the event data is available, you can use standard assertEquals calls to
verify that the bean contains the expected values.
5. To run the unit test, right click ContactFormTest and Select Run 'ContactFormTest'.
109
6. When the test finishes, you will see the results at the bottom of the IDE window in the
test runner panel. As you can see, both tests passed.
110
Creating and running integration tests for more advanced UI
logic
To test a class that uses @Autowire, a database, or any other feature provided by Spring
Boot, you can no longer use plain JUnit tests. Instead, you can use the Spring Boot test
runner. This does add a little overhead, but it makes more features available to your test.
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
2. To set up a unit test for ListView, create a new file, ListViewTest, in the
com.vaadin.tutorial.crm.ui.views.list package:
ListViewTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ListViewTest {
@Autowired
private ListView listView;
@Test
public void formShownWhenContactSelected() {
}
}
a. Add the Spring @Component annotation to make it possible to @Autowire it. Also
add @Scope("prototype") to ensure every test run gets a fresh instance.
111
We didn’t need to add the annotation for normal application usage, as all @Route
TIP
classes are automatically instantiated by Vaadin in a Spring-compatible way.
b. Remove the private keyword. This changes the private fields to package private,
and allows you to access the grid and form of the ListView in your test case.
ListView.java
@Component
@Scope("prototype")
@Route(value = "", layout = MainLayout.class)
@PageTitle("Contacts | Vaadin CRM")
public class ListView extends VerticalLayout {
ContactForm form;
Grid<Contact> grid = new Grid<>(Contact.class);
TextField filterText = new TextField();
ContactService contactService;
// rest omitted
}
4. Right click the package that contains both tests, and select Run tests in
'com.vaadin.tutorial.crm.ui.views.list'.
5. You should see that both test classes run and result in 3 successful tests.
112
You probably noticed that running the tests the second time took much longer. This is
the price of being able to use @Autowire and other Spring features and can cost many
seconds of startup time.
NOTE
You can improve the startup time by explicitly listing the needed dependencies in the
@SpringBootTest annotation using classes={…}, mock parts of the application, or
using other advanced techniques which are out of scope for this tutorial. Pivotal’s
Spring Boot Testing Best Practices has tips to speed up your tests.
6. You can now add the actual test implementation, which selects the first row in the
grid and validates that this shows the form with the selected Contact:
ListViewTest.java
@Test
public void formShownWhenContactSelected() {
Grid<Contact> grid = listView.grid;
Contact firstContact = getFirstItem(grid);
Assert.assertFalse(form.isVisible());
grid.asSingleSelect().setValue(firstContact);
Assert.assertTrue(form.isVisible());
Assert.assertEquals(firstContact, form.binder.getBean());
}
private Contact getFirstItem(Grid<Contact> grid) {
return( (ListDataProvider<Contact>) grid.getDataProvider()).getItems()
.iterator().next();
}
113
•• Selecting the first item in the grid and verifying that:
You now know how to test the application logic both in isolation with unit tests and by
injecting dependencies to test the integration between several components. In the next
chapter, we cover how to test the entire application in the browser.
You can find the completed source code for this tutorial on GitHub.
114
Testing Vaadin apps in the browser with end-to-
end tests
End-to-end (e2e) tests are used to test the entire application. They’re far more coarse
grained than unit or integration tests. This makes them well suited to check that the
application works as a whole, and to catch any regressions that may be missed by more
specific tests.
End-to-end tests are executed in a browser window, controlled by a web driver and run
on the server where the application is deployed. You need to setup 3 things:
1. Configure Maven to start the Spring Boot server before running tests and to stop it
afterwards.
2. Make sure you have Chrome installed and install a web driver manager that will
download the needed web driver.
3. Create a base test class that starts a browser and opens the application URL.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
The end-to-end tests use Vaadin TestBench, which is a commercial tool that is a part of the
NOTE Vaadin Pro Subscription. You can get a free trial at https://vaadin.com/trial. All Vaadin Pro
tools and components are free for students through the GitHub Student Developer Pack.
In you pom.xml, remove the existing integration-test profile and add the following
profile:
115
pom.xml
<profile>
<id>it</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>start-spring-boot</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop-spring-boot</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Runs the integration tests (*IT) after the server is started -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<trimStackTrace>false</trimStackTrace>
<enableAssertions>true</enableAssertions>
</configuration>
</plugin>
</plugins>
</build>
</profile>
Integration tests are run by executing mvn -Pit verify from the command line, or by
selecting the it profile and running the verify goal from the Maven menu in IntelliJ.
116
The pre-integration-test and post-integration-test executions take care of
starting and stopping Spring Boot before the integration test phase of the build is
executed. In the integration test phase, the maven-failsafe-plugin runs any tests
named *IT.java found in src/test/java. You should set the trimStackTrace option to
false to print full stack traces and ease debugging.
117
Setting up Chrome and its webdriver to control the browser
For browser tests to work, you need Chrome installed on the machine that runs the tests.
pom.xml
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>3.7.1</version>
<scope>test</scope>
</dependency>
----
• This is used from the JUnit test class and downloads the correct version of the
Chrome web driver, if it is missing.
IntelliJ collapses empty packages by default, so it’s easiest to first create the class in the
TIP
existing test package, and then move it to the correct package.
118
119
AbstractTest.java
package com.vaadin.tutorial.crm.it;
@Rule
public ScreenshotOnFailureRule rule = new ScreenshotOnFailureRule(this, true
); ②
}
2. Next, add the application URL that the tests should open before trying to interact with
the application. For this you need the host name where the application runs
("localhost" in development), the port the server uses (set to 8080 in
application.properties), and information about the route to start from.
AbstractTest.java
@Before
public void setup() throws Exception {
super.setup();
getDriver().get(getURL(route)); // Opens the given URL in the browser
}
3. To avoid excessive logging from WebDriverManager when running the tests, add the
following workaround:
120
AbstractTest.java
static {
// Prevent debug logging from Apache HTTP client
Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.INFO);
}
a. org.slf4j.LoggerFactory
b. ch.qos.logback.classic.Level
c. ch.qos.logback.classic.Logger
LoginIT.java
package com.vaadin.tutorial.crm.it;
The name of the class should end in IT for the test runner to pick it up as an integration
NOTE test. If you name it LoginTest instead, it will be run as a unit test and the server will
not be started and the test will fail.
121
LoginIT.java
@Test
public void loginAsValidUserSucceeds() {
// Find the LoginForm used on the page
LoginFormElement form = $(LoginFormElement.class).first();
// Enter the credentials and log in
form.getUsernameField().setValue("user");
form.getPasswordField().setValue("password");
form.getSubmitButton().click();
// Ensure the login form is no longer visible
Assert.assertFalse($(LoginFormElement.class).exists());
}
While developing tests it is not very efficient to run the tests as mvn -Pit verify.
Instead, you can start the server manually by launching the Application class or with
TIP
spring-boot:run. You can then execute the selected test in your IDE and you do not
have to wait for the server to start every time.
3. Start the application normally, then right click LoginIT.java and select Run 'LoginIT'.
the first time you run the test, you will be asked to start a trial or validate your existing
NOTE
license. Follow the instructions in the browser window that opens.
For this text, you need to write the same code to access the components in the view, as
you did for the first test. To make your tests more maintainable, you can create a view
object (a.k.a. call page object or element class) for each view. A view object provides a
high-level API to interact with the view and hides the implementation details.
1. For the login view, create the LoginViewElement class in a new package,
com.vaadin.tutorial.crm.it.elements.login:
122
LoginViewElement.java
package com.vaadin.tutorial.crm.it.elements.login;
To make the correct functionality available from super classes, the hierarchy of
the view object should match the hierarchy of the view (public class
CAUTION
LoginView extends VerticalLayout vs public class
LoginViewElement extends VerticalLayoutElement).
The annotation searches for the login-view class name, which is set for the login
view in the constructor. The onPage() call ensures that the whole page is searched.
By default a $ query starts from the active element.
2. Now that the the LoginViewElement class is available, you can refactor your
loginAsValidUserSucceeds test to be:
LoginIT.java
@Test
public void loginAsValidUserSucceeds() {
LoginViewElement loginView = $(LoginViewElement.class).onPage().first();
Assert.assertTrue(loginView.login("user", "password"));
}
123
3. Add a test to use an invalid password as follows:
LoginIT.java
@Test
public void loginAsInvalidUserFails() {
LoginViewElement loginView = $(LoginViewElement.class).onPage().first();
Assert.assertFalse(loginView.login("user", "invalid"));
}
4. Continue testing the other views by creating similar view objects and IT classes.
In the next tutorial we cover how to make a production build of the application and
deploy it to a cloud platform.
You can find the completed source code for this tutorial on GitHub.
124
Deploying a Spring Boot app on AWS Elastic
Beanstalk
In this final tutorial in the series, we show you how to deploy the Spring Boot application
we have built on Amazon Web Services (AWS) with AWS Elastic Beanstalk (EB). EB is a
service that orchestrates other AWS services like virtual servers, load balancers, storage,
and databases.
So far in this series, the application has used an in-memory H2 database. For the
deployed application, we will use a MySQL server for persistent storage instead.
You can download the completed source code at the bottom. The code from the
previous tutorial chapter can be found here, if you want to jump directly into this
chapter.
When you build a production version of a Vaadin app, the following happens: All the
front-end resources are bundled and minified to speed up the app load time. Vaadin
runs in production mode to hide debugging and other sensitive information from the
browser.
Do not store sensitive information like passwords in properties files that get committed to a
TIP version control system like Git. Instead, use environment variables that can be kept on the
server.
125
application-prod.properties
server.port=5000 ①
spring.datasource.url=jdbc:mysql://${RDS_HOSTNAME}:${RDS_PORT}/${RDS_DB_NAME} ②
spring.datasource.username=${RDS_USERNAME}
spring.datasource.password=${RDS_PASSWORD}
spring.jpa.hibernate.ddl-auto=create
① Elastic Beanstalk maps the internal port 5000 to the external port 80 to expose the
application to the internet.
② Elastic Beanstalk will provide environment variables with information about the
database so we don’t need to store them in the property file.
pom.xml
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
126
products.
AWS is a paid service and following the instructions below may result in charges.
WARNING
Carefully review the billing costs on AWS to avoid any surprises.
2. Create an environment for the application with the following configurations, and then
click Configure more options:
127
• Application code: Sample application. You can leave the other fields empty.
3. Go to Software > Modify and add an environment property, and then click Save:
• Name: SPRING_PROFILES_ACTIVE
• Value: prod
128
4. Go to Database > Modify and set up an Amazon RDS SQL database with the following
configurations, and then click Save:
• Add a username and password. Elastic Beanstalk will make these available to your
application through the environment variables you set up in the properties file.
129
This database setup is suitable for the tutorial, but in a real production
application, the database should not be tied to the lifecycle of the environment.
CAUTION
Otherwise you may inadvertently delete the database if you remove the server.
See Using Elastic Beanstalk with Amazon Relational Database Service.
NOTE Creating the application environment and database can take up to 15 minutes.
130
131
2. After the environment has updated (this can take several minutes), the environment
Health should indicate as Ok (green tick) and your application should run and be
accessible on the web through the link at the top of the dashboard. If the health is not
Ok, go to Logs (in the EB console) to troubleshoot the problem.
132
133
You can find the completed source code for this tutorial on GitHub.
Next steps
Good job on completing the tutorial series! You now have all the skills you need to get
started building real-life applications with Spring Boot and Vaadin.
You can find more information about both in the respective frameworks' documentation:
• Vaadin documentation
134
This guide is specifically designed as a practical
introduction to web application development
using Java. It covers the entire development
process, from setup to deployment, following a
step-by-step approach. You can replicate each
section at your own pace as you follow along.