Hilla - Proper way of handling relationships in forms

Situation

I have two simple entities “Employee” (id, name, department_id) and “Department” (id, name).

I do not want to expose my entities directly, so I am using the DTO pattern.

“EmployeeDetailDto.java” looks as follows:

@Data
public class EmployeeDetailDto {

  /** If null, create, otherwise update */
  private UUID id;

  @NotBlank
  private String name;

  @NotNull 
  private UUID departmentId;
}

As you can see, I am expecting the client to tell me the department id.

I have created a simple form that handles two scenarios:

  • No id is provided in the URL, hence no employee is fetched from the database. Thus, a new employee will be created.
  • There is an id in the URL. In this case, I am loading the employee from the database.

So far so good. In below screenshot you can see how fetching an employee looks like:

What I want

As you might imagine, having to type out the department id by hand is very cumbersome, especially if there are hundreds or even thousands of departments.
Therefore, I want to implement an autocomplete field that does the following things:

  • When creating a new employee (no id in the URL), I want to be able to search for departments (with pagination/lazy loading) based on their name and store the id of the department inside my EmployeeDetailDto.
  • When editing an employee, the corresponding department autocomplete has to be prefilled with the employee’s department name. Additionally, the same functionality (paging+search) of the previous bullet point still has to be applied.

Questions

  1. How can one achieve this functionality? It is important for me to only provide the departmentId to my endpoint, the autocomplete with pagination and searching by name is just a help for users to enter the right department id.

  2. Furthermore, is it possible to combine this with AutoCRUD, AutoGrid and AutoForm components?

You could use vaadin-combo-box. You can type text in the field, filter and reload data on change. It also supports a lazy data provider function in frontend.

Hi! I have already tried that.

This is related to my other question: Creating an AutoComplete textbox using Hilla's ComboBox

Could you further elaborate, as the code is quite ugly using ComboBox?

I never used CRUD Endpoints and DataProvider in Hilla. I would just implement myself with the same parameter as ComboBoxDataProviderParams.

For that you could use the Binder.

Here I had extracted some code where I used the filter with data caching instead of lazy data loading.

import "@vaadin/icon";
import "@vaadin/icons";
import "@vaadin/tooltip";
import "@vaadin/combo-box"
import "@vaadin/text-field"

import { html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { View } from "../view";
import { ComboBoxFilterChangedEvent } from "@vaadin/combo-box";
import { ChatEndpoint } from "Frontend/generated/endpoints";
import { Binder, field } from "@vaadin/hilla-lit-form";


// replaces these types by the generaed from hilla
type DepartmentDto = { name: string, id: string }
type EmployeeDetailDto = { name: string, id: string, departmentId: string }


@customElement("test-view")
export class TestView extends View {

    connectedCallback(): void {
        super.connectedCallback();
        this.classList.add("centered-content", "w-full");
        this.load()
    }

    private async load() {
        this.items = cache.getDepartmentItems() // TODO get your departments
        this.filteredItems = this.items;
        this.binder.read({}) // TODO get your employee or new bean
    }

    @state()
    items: DepartmentDto[] = [];
    @state()
    filteredItems: DepartmentDto[] = [];

    private readonly binder = new Binder<EmployeeDetailDtoModel>(this, EmployeeDetailDtoModel)

    private onFilterChange(e: ComboBoxFilterChangedEvent) {
        const filter = e.detail.value
        if (filter && filter.length > 0) {
            this.filteredItems = this.items.filter(dep => dep.name.startsWith(filter))
        } else {
            this.filteredItems = this.items
        }
    }

    render() {
        return html`
            <div 
                class="flex flex-col flex-nowrap gap-m items-center p-m h-full"
                style="overflow-y: auto;"
                >
                <vaadin-text-field
                    ${field(this.binder.model.name)}
                    >
                </vaadin-text-field>

                <vaadin-combo-box
                    .items=${this.filteredItems}
                    @filter-changed=${this.onFilterChange.bind(this)}
                    item-label-path="name"
                    item-value-path="id"
                    ${field(this.binder.model.departmentId)}
                    >
                </vaadin-combo-box>
            </div>
        `
    }

}