1. Overview
Vaadin Flow is a server-side Java framework for creating web user interfaces.
In this tutorial, we’ll explore how to build a Vaadin Flow based CRUD UI for a Spring Boot based backend. For an introduction to Vaadin Flow refer to this tutorial.
2. Setup
Let’s start by adding Maven dependencies to a standard Spring Boot application:
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
Vaadin is also a recognized dependency by the Spring Initializr.
We can also add the Vaadin Bill of Materials manually to the project if we’re not using Spring Initializr:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>24.3.8</version> <!-- check latest version -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. Backend Service
We’ll use an Employee entity with firstName and lastName properties to perform CRUD operations on it. We also define validation rules for the properties so that we can enforce them in the UI we are building:
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
@Size(min = 2, message = "Must have at least 2 characters")
private String firstName;
@Size(min = 2, message = "Must have at least 2 characters")
private String lastName;
public Employee() {
}
// Getters and setters
}
Here’s the simple, corresponding Spring Data repository to manage the CRUD operations:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByLastNameStartsWithIgnoreCase(String lastName);
}
We declare a query method findByLastNameStartsWithIgnoreCase on the EmployeeRepository interface. It will return the list of employees for the given last name.
Let’s also pre-populate the DB with a few sample employees:
@Bean
public CommandLineRunner loadData(EmployeeRepository repository) {
return (args) -> {
repository.save(new Employee("Bill", "Gates"));
repository.save(new Employee("Mark", "Zuckerberg"));
repository.save(new Employee("Sundar", "Pichai"));
repository.save(new Employee("Jeff", "Bezos"));
};
}
4. Vaadin Flow UI
The application we are building will feature a filterable data grid displaying employees and a form for editing and creating employees. We’ll begin by creating the form as a custom component, then create the main layout using standard Vaadin components and our custom form component:
4.1. The EmployeeEditor Form Component
The EmployeeEditor is a custom component that we create by composing existing Vaadin components and defining a custom API.
EmployeeForm uses a VerticalLayout as its base. VerticalLayout is a Layout, which shows the subcomponents in the order of their addition (vertically). By extending Composite
Let’s begin by defining the API of our component so that the user can set the Employee they want to edit, and subscribe to save, cancel, and delete events:
public class EmployeeEditor extends Composite<VerticalLayout> {
public interface SaveListener {
void onSave(Employee employee);
}
public interface DeleteListener {
void onDelete(Employee employee);
}
public interface CancelListener {
void onCancel();
}
private Employee employee;
private SaveListener saveListener;
private DeleteListener deleteListener;
private CancelListener cancelListener;
private final Binder<Employee> binder = new BeanValidationBinder<>(Employee.class);
public void setEmployee(Employee employee) {
this.employee = employee;
binder.readBean(employee);
}
// Getters and setters
}
The setEmployee method saves a reference to the current employee and reads the bean into the Vaadin BeanValidationBinder so that we can bind it to the input fields of our form and validate them.
Next, we construct the UI of the component using TextField and Button components. We use the binder to map input fields to fields on the model. Reading the bean into the binder means that whenever we call setEmployee, the input field values get updated.
We add all the components to the VerticalLayout that is the root of our composition:
public EmployeeEditor() {
var firstName = new TextField("First name");
var lastName = new TextField("Last name");
var save = new Button("Save", VaadinIcon.CHECK.create());
var cancel = new Button("Cancel");
var delete = new Button("Delete", VaadinIcon.TRASH.create());
binder.forField(firstName).bind("firstName");
binder.forField(lastName).bind("lastName");
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
save.addClickListener(e -> save());
save.addClickShortcut(Key.ENTER);
delete.addThemeVariants(ButtonVariant.LUMO_ERROR);
delete.addClickListener(e -> deleteListener.onDelete(employee));
cancel.addClickListener(e -> cancelListener.onCancel());
getContent().add(firstName, lastName, new HorizontalLayout(save, cancel, delete));
}
Finally, we handle the save button click by reading the input field values into a new Employee object if field validations pass. We avoid saving the values to the original object as it is bound to other parts of the UI, and we want to avoid creating side effects when using our component. Once we have the updated employee, we call the saveListener to notify the parent component.
4.2. The Main View
The EmployeesView class is the entry point for our application. The @Route(“”) annotation tells Vaadin Flow to map the component to the root path when the application starts.
We extend VerticalLayout as the base for our view, and then construct the UI using standard Vaadin components and the custom EmployeeEditor we created.
Vaadin Flow Views are Spring beans, which means we can auto-wire EmployeeRepository into our view through the constructor:
@Route("")
public class EmployeesView extends VerticalLayout {
private final EmployeeRepository employeeRepository;
private final TextField filter;
private final Grid<Employee> grid;
private final EmployeeEditor editor;
public EmployeesView(EmployeeRepository repo) {
employeeRepository = repo;
// Create components
var addButton = new Button("New employee", VaadinIcon.PLUS.create());
filter = new TextField();
grid = new Grid<>(Employee.class);
editor = new EmployeeEditor();
// Compose layout
var actionsLayout = new HorizontalLayout(filter, addButton);
add(actionsLayout, grid, editor);
}
}
4.3. Configuring Components and Data
Next, we create two helper methods: one for updating the grid based on a search string, and one for handling employee selection:
private void updateEmployees(String filterText) {
if (filterText.isEmpty()) {
grid.setItems(employeeRepository.findAll());
} else {
grid.setItems(employeeRepository.findByLastNameStartsWithIgnoreCase(filterText));
}
}
private void editEmployee(Employee employee) {
editor.setEmployee(employee);
if (employee != null) {
editor.setVisible(true);
} else {
// Deselect grid
grid.asSingleSelect().setValue(null);
editor.setVisible(false);
}
}
Finally, we configure the components:
- We configure the EmployeeEditor component to be hidden initially and define listeners for handling the save, delete, and cancel events.
- We set ValueChangeMode.LAZY on the filter TextField to call updateEmployees with the filter value whenever the user stops typing.
- We define a fixed 200px height for the grid and call editEmployee whenever the selected row changes.
public EmployeesView(EmployeeRepository repo) {
// Component creation code from above
// Configure components
configureEditor();
addButton.addClickListener(e -> editEmployee(new Employee()));
filter.setPlaceholder("Filter by last name");
filter.setValueChangeMode(ValueChangeMode.EAGER);
filter.addValueChangeListener(e -> updateEmployees(e.getValue()));
grid.setHeight("200px");
grid.asSingleSelect().addValueChangeListener(e -> editEmployee(e.getValue()));
// List customers
updateEmployees("");
}
private void configureEditor() {
editor.setVisible(false);
editor.setSaveListener(employee -> {
var saved = employeeRepository.save(employee);
updateEmployees(filter.getValue());
editor.setEmployee(null);
grid.asSingleSelect().setValue(saved);
});
editor.setDeleteListener(employee -> {
employeeRepository.delete(employee);
updateEmployees(filter.getValue());
editEmployee(null);
});
editor.setCancelListener(() -> {
editEmployee(null);
});
}
4.4. Running the Application
We can start the application with Maven:
mvn spring-boot:run
The application is now running on localhost:8080:
5. Conclusion
In this article, we wrote a full-featured CRUD UI application using Spring Boot and Spring Data JPA for persistence.
As usual, the code is available over on GitHub.