Auto Form Hilla React
- Basic Usage
- Customizing Visibility & Order
- Customizing Field Properties
- Customizing Form Layout
- Using Custom Renderer
- Managing Form State
- Validation
- Working with Plain Java Classes
Auto Form is a component that automatically renders a form for editing, updating and deleting items from a backend service.
Important
|
Scaled down examples
The examples on this page are scaled down so that their viewport-size-dependent behavior can be demonstrated.
Some examples also change their behavior based on your browser viewport size.
|
Basic Usage
Auto Form requires a Java service that implements the CrudService<T, ID>
interface. In the example here, the EmployeeService
class extends CrudRepositoryService<T, ID, R>
, which in turn implements the CrudService<T, ID>
:
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
import com.vaadin.hilla.crud.CrudRepositoryService;
@BrowserCallable
@AnonymousAllowed
public class EmployeeService
extends CrudRepositoryService<Employee, Long, EmployeeRepository> {
}
Hilla generates TypeScript objects for each @BrowserCallable
service implementing the CrudService<T, ID>
interface. These TypeScript objects have callable methods that execute the corresponding Java service methods, enabling the Add, Update, and Delete operations, as well as lazy data loading with sorting and filtering.
For the example above, you can import the generated TypeScript objects in your React view and configure the component like so:
<AutoForm service={EmployeeService} model={EmployeeModel} />
Here’s how the rendered form would look:
As you can see, Auto Form component renders a form with fields for all properties of the Employee
entity. When submitting the form, the data is automatically submitted to the save
method of the configured CrudService
.
Auto Form supports working with plain, non-JPA classes. This can be useful, for example, if you want to use the Data Transfer Object (DTO) pattern. See the Working with Plain Java Classes section for more on this.
Note
|
Default Responsive Breakpoints
Auto Form uses the Form Layout component's default responsive breakpoints. To learn how to customize them, see the Customizing Form Layout section.
|
Customizing Visibility & Order
Auto Form renders by default input fields for all properties of the entity in a Form Layout with a two-column layout, except for the properties annotated by @Id
and @Version
. You can customize the visibility and the order of fields using visibleFields
property:
new tab
<AutoForm
service={EmployeeService}
model={EmployeeModel}
visibleFields={['firstName', 'lastName', 'startDate', 'shiftStartsAt', 'gender', 'version']}
/>
Alternatively, you can also use the hiddenFields
property to specify which fields should be hidden.
Caution
|
Don’t Expose Sensitive Data on Client-Side
When using Auto Form, it’s important to check that you don’t expose sensitive data to the client-side. For example, if you have a password property in your entity, hiding it using visibleFields or hiddenFields doesn’t prevent the data from being sent to the client-side. Make sure your service implementation doesn’t expose sensitive data to the client-side. You could do this by using a @JsonIgnore annotation on the property.
|
Customizing Field Properties
You can customize the properties of the fields rendered by Auto Form by using the fieldOptions
property.
Overriding Field Options
You can customize some options of the automatically rendered fields by Auto Form by using the fieldOptions
property.
Customizing Field Label
To customize the label of a field, use the label
property of the fieldOptions
property as follows:
new tab
<AutoForm
service={EmployeeService}
model={EmployeeModel}
fieldOptions={{
firstName: { label: 'Employee First name' },
lastName: { label: 'Employee Last name' },
}}
/>
The previous example shows how to customize the label of the firstName
and lastName
fields.
Customizing Field Column Span
As Auto Form component uses the Form Layout component as the layout, you can customize the colspan
property of the fields rendered by Auto Form, with the fieldOptions
property as follows:
new tab
<AutoForm
service={EmployeeService}
model={EmployeeModel}
fieldOptions={{
description: { colspan: 2 },
}}
/>
Customizing Input Component
You can use the renderer
property of the fieldOptions
to override the rendered field by Auto Form. The following example shows how to render a Text Area component for the description field:
new tab
<AutoForm
service={EmployeeService}
model={EmployeeModel}
fieldOptions={{
description: {
renderer: ({ field }) => <TextArea {...field} label="Full description" />,
},
}}
/>
Customizing Form Layout
You can customize the Form Layout component used by Auto Form, with the formLayoutProps
property. For example, you can set the responsiveSteps
property to customize the responsive breakpoints of the Form Layout component:
new tab
<AutoForm
service={EmployeeService}
model={EmployeeModel}
formLayoutProps={{
responsiveSteps: [
{ minWidth: '0', columns: 1 },
{ minWidth: '800px', columns: 2 },
{ minWidth: '1200px', columns: 3 },
],
}}
fieldOptions={{
description: { colspan: 3 },
}}
/>
Using Custom Renderer
To render a form in a completely different layout, you can use a custom renderer to define any arbitrary layout using the layoutRenderer
property. Define a function that takes the AutoFormLayoutRendererProps
as a parameter, that returns any React element. The parameter will contain the following properties:
-
children
, the read-only list of bound fields; and -
form
, the form binder instance.
Since the children property is a read-only list of bound fields, you can use them in your custom renderer. The following example shows how to use the children
property to render the fields in separate sub-form sections using the Vertical and Horizontal layouts:
new tab
function GroupingLayoutRenderer({ children }: AutoFormLayoutRendererProps<EmployeeModel>) {
const fieldsMapping = new Map<string, JSX.Element>();
children.forEach((field) => fieldsMapping.set(field.props?.propertyInfo?.name, field));
return (
<VerticalLayout>
<h4>Personal Information:</h4>
<HorizontalLayout theme="spacing" className="pb-l">
{fieldsMapping.get('firstName')}
{fieldsMapping.get('lastName')}
{fieldsMapping.get('gender')}
</HorizontalLayout>
<h4>Employment Information:</h4>
<HorizontalLayout theme="spacing" className="pb-l items-baseline">
{fieldsMapping.get('startDate')}
{fieldsMapping.get('shiftStartsAt')}
{fieldsMapping.get('active')}
</HorizontalLayout>
<h4>Other:</h4>
<HorizontalLayout theme="spacing">{fieldsMapping.get('description')}</HorizontalLayout>
</VerticalLayout>
);
}
<AutoForm
service={EmployeeService}
model={EmployeeModel}
layoutRenderer={GroupingLayoutRenderer}
/>
Alternatively, you can manually create input fields, and use the form
instance to connect them to the form.
The following example shows how to render fields, manually:
new tab
function GroupingLayoutRenderer({ form }: AutoFormLayoutRendererProps<EmployeeModel>) {
const { field, model } = form;
return (
<VerticalLayout>
<h4>Personal Information:</h4>
<HorizontalLayout theme="spacing" className="pb-l">
<TextField label="First Name" {...field(model.firstName)} />
<TextField label="Last Name" {...field(model.lastName)} />
<Select
label="Gender"
{...field(model.gender)}
items={[
{ label: 'Female', value: Gender.FEMALE },
{ label: 'Male', value: Gender.MALE },
{ label: 'Non-Binary', value: Gender.NON_BINARY },
{ label: 'Prefer not to say', value: Gender.OTHER },
]}
/>
</HorizontalLayout>
<h4>Employment Information:</h4>
<HorizontalLayout theme="spacing" className="pb-l items-baseline">
<DatePicker label="Start Date" {...field(model.startDate)} />
<TimePicker label="Shift Starts At" {...field(model.shiftStartsAt)} />
<Checkbox label="Active" {...field(model.active)} />
</HorizontalLayout>
<h4>Other:</h4>
<HorizontalLayout theme="spacing">
<TextArea label="Description" {...field(model.description)} style={{ flexGrow: 1 }} />
</HorizontalLayout>
</VerticalLayout>
);
}
<AutoForm
service={EmployeeService}
model={EmployeeModel}
layoutRenderer={GroupingLayoutRenderer}
/>
Managing Form State
Auto Form component allows you to manage the form state in different scenarios. For instance, you can edit an item — versus creating a new one — by handling after submission events, and so on.
The following properties are available for managing the form state or reacting to form events:
-
item
is the item to be edited. If the item isnull
, the form is in new mode, otherwise it is in edit mode; -
disabled
disables the whole form; and -
onSubmitSuccess
is a callback function that’s called after the form is submitted successfully.
The following example shows how to use the item
property to manage switching between new and edit modes, and onSubmitSuccess
for informing the user about the form submission success. Additionally, it uses disabled
property to toggle the form’s disabled state:
new tab
const existingItem: Employee = {
firstName: 'Jennifer',
lastName: 'Smith',
gender: Gender.NON_BINARY,
startDate: '2014-08-05',
active: true,
description: 'Customer Service Manager',
};
const editedItem = useSignal<Employee | null>(null);
const disabled = useSignal<boolean>(false);
const handleEdit = () => {
editedItem.value = existingItem;
};
const handleCreate = () => {
editedItem.value = null;
};
const toggleDisabled = () => {
disabled.value = !disabled.value;
};
const handleSubmitSuccess = ({ item }: { item: Employee }) => {
const json = JSON.stringify(item);
Notification.show(`Form was submitted: ${json}`);
};
return (
<VerticalLayout>
<HorizontalLayout theme="spacing">
<Button onClick={handleEdit}>Edit item</Button>
<Button onClick={handleCreate}>Create item</Button>
<Button onClick={toggleDisabled}>{disabled.value ? 'Enable Form' : 'Disable Form'}</Button>
</HorizontalLayout>
<AutoForm
service={EmployeeService}
model={EmployeeModel}
item={editedItem.value}
disabled={disabled.value}
onSubmitSuccess={handleSubmitSuccess}
/>
</VerticalLayout>
);
Auto Form component supports Deletion functionality, which is associated with calling the delete
method from the CrudService
introduced to the Auto Form. The following properties are associated with managing this functionality:
-
deleteButtonVisible
is to stipulate whether to show the delete button in the form, which is hidden by default. If enabled, the delete button is only shown when editing an existing item: that means thatitem
property is not null. -
onDeleteSuccess
is a callback function that’s called after the item is deleted successfully.
The following example shows how to use the deleteButtonVisible
property to show the Delete button, and uses the onDeleteSuccess
property to inform the user about the deletion success. You can toggle between the edit and new modes to see its effect on the visibility of the Delete button in action:
new tab
const existingItem: Employee = {
firstName: 'Jennifer',
lastName: 'Smith',
gender: Gender.NON_BINARY,
startDate: '2014-08-05',
active: true,
description: 'Customer Service Manager',
};
const editedItem = useSignal<Employee | null>(null);
const showDeleteButton = useSignal<boolean>(false);
const handleEdit = () => {
editedItem.value = existingItem;
};
const handleCreate = () => {
editedItem.value = null;
};
const toggleDeleteButtonVisibility = () => {
showDeleteButton.value = !showDeleteButton.value;
};
const handleDeleteSuccess = ({ item }: { item: Employee }) => {
const json = JSON.stringify(item);
Notification.show(`Item deleted: ${json}`);
};
return (
<VerticalLayout>
<HorizontalLayout theme="spacing">
<Button onClick={handleEdit}>Edit item</Button>
<Button onClick={handleCreate}>Create item</Button>
<Button onClick={toggleDeleteButtonVisibility}>
{showDeleteButton.value ? 'Hide Delete Button' : 'Show Delete Button'}
</Button>
</HorizontalLayout>
<AutoForm
service={EmployeeService}
model={EmployeeModel}
item={editedItem.value}
deleteButtonVisible={showDeleteButton.value}
onDeleteSuccess={handleDeleteSuccess}
/>
</VerticalLayout>
);
The following properties are associated with managing the form actions error handling:
-
onSubmitError
is a callback that’s called if an unexpected error occurs while submitting the form. The callback receives the error as a parameter. -
onDeleteError
is a callback that’s called if an unexpected error occurs while deleting the item. The callback receives the error as a parameter.
The next example shows how to use the onSubmitError
and onDeleteError
callbacks to inform the user about their respective errors:
const existingItem: Employee = {
firstName: 'Jennifer',
lastName: 'Smith',
gender: Gender.NON_BINARY,
startDate: '2014-08-05',
active: true,
description: 'Customer Service Manager',
};
const handleOnSubmitError = ({ error }: { error: unknown }) => {
const json = JSON.stringify(error);
Notification.show(`Error while submitting: ${json}`);
};
const handleOnDeleteError = ({ error }: { error: unknown }) => {
const json = JSON.stringify(error);
Notification.show(`Error while deleting: ${json}`);
};
return (
<AutoForm
service={EmployeeService}
model={EmployeeModel}
item={existingItem}
deleteButtonVisible
onSubmitError={handleOnSubmitError}
onDeleteError={handleOnDeleteError}
/>
);
Note
|
On Submit Error Not Called
Note that onSubmitError callback is not be called for form validation errors, which are handled automatically.
|
Validation
Auto Form uses the same validation mechanism as default Hilla Forms. You can add Bean validation annotations to your entity classes, and the form automatically validates the input values on both client and server.
In the following example, annotations are added to the Employee
class to ensure that firstName
is neither null
nor an empty string:
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class Employee {
@NotNull
@NotBlank
private String firstName;
// ...
}
For further information on using validation, refer to the Forms documentation.
One notable difference to default forms is adding custom client-side validators. With Auto Form, custom client-side validators are added using the validators
property in the fieldOptions
. The following example shows how to add a custom validator to the firstName
field:
<AutoForm
service={EmployeeService}
model={EmployeeModel}
fieldOptions={{
firstName: {
validators: [
{
message: 'First name must be longer than 3 characters',
validate: (value: string) => value.length > 3,
},
],
},
}}
/>
Working with Plain Java Classes
Auto Form supports working with plain, non-JPA Java classes. This can be useful, for example if you want to use the Data Transfer Object (DTO) pattern to decouple your domain from your presentation logic, or you want to avoid serializing your JPA entities to the client due to performance concerns.
Implementing a Custom Service
To use plain Java classes, you need to provide a custom implementation of the FormService<T>
interface. The following methods need to be implemented:
-
T save(T value)
: Either creates a new item or updates an existing one, depending on whether the item has an ID or not. Returns the saved item. -
void delete(ID id)
: Deletes the item with the given ID.
The following example shows a basic implementation of a custom FormService
that wraps a JPA repository. The service only accepts DTO instances from the client and maps between the DTO instances and JPA entities internally.
@BrowserCallable
@AnonymousAllowed
public class ProductDtoFormService implements FormService<ProductDto, Long> {
private final ProductRepository productRepository;
public ProductDtoFormService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public @Nullable ProductDto save(ProductDto value) {
Product product = value.id() != null && value.id() > 0
? productRepository.getReferenceById(value.id())
: new Product();
product.setName(value.name());
product.setCategory(value.category());
product.setPrice(value.price());
return ProductDto.fromEntity(productRepository.save(product));
}
@Override
public void delete(Long id) {
productRepository.deleteById(id);
}
}