Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

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:

A data grid displaying employees. The first employee is selected and displayed in a form below.

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<VerticalLayout> instead of VerticalLayout, we don’t expose all the methods of VerticalLayout, giving us full control over the API of our component.

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:

A data grid displaying employees. The first employee is selected and displayed in a form below.

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.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.