Auto Form
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 dev.hilla.BrowserCallable;
import dev.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 what the rendered form looks like:
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, which can be useful, for example, if you want to use the Data Transfer Object (DTO) pattern. See the Working with Plain Java Classes section.
Note
|
Default Responsive Breakpoints
Auto Form uses the Form Layout component's default responsive breakpoints. To customize them, see the Customizing Form Layout section.
|
Customizing Visibility & Order
Auto Form by default renders 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 order of fields using visibleFields
property:
new tab
<AutoForm
service={EmployeeService}
model={EmployeeModel}
visibleFields={['firstName', 'lastName', 'startDate', 'shiftStartsAt', 'gender', 'version']}
/>
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 does not 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, for example, 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 by using 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.
To use a custom renderer, define a function that takes the AutoFormLayoutRendererProps
as a parameter and returns any React element. The AutoFormLayoutRendererProps
contains the following properties:
-
children
: the read-only list of bound fields -
form
: the form binder instance
As the children property is a read-only list of bound fields, you can use them right away 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, form }: 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({ children, 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 enables you to manage the form state in different scenarios. For instance, you can edit an item versus by creating a new one, handling after submission events, and so on.
The following properties are available for managing the form state or reacting to form events:
-
item
: 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. -
onSubmitSuccess
: a callback function that is 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. In addition, it uses disabled
property is also used 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, setEditedItem] = useState<Employee | null>(null);
const [disabled, setDisabled] = useState<boolean>(false);
const handleEdit = () => {
setEditedItem(existingItem);
};
const handleCreate = () => {
setEditedItem(null);
};
const toggleDisabled = () => {
setDisabled(!disabled);
};
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 ? 'Enable Form' : 'Disable Form'}</Button>
</HorizontalLayout>
<AutoForm
service={EmployeeService}
model={EmployeeModel}
item={editedItem}
disabled={disabled}
onSubmitSuccess={handleSubmitSuccess}
/>
</VerticalLayout>
);
Auto Form component supports Deletion functionality out-of-the-box, 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
: 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
: a callback function that is 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, setEditedItem] = useState<Employee | null>(null);
const [showDeleteButton, setShowDeleteButton] = useState<boolean>(false);
const handleEdit = () => {
setEditedItem(existingItem);
};
const handleCreate = () => {
setEditedItem(null);
};
const toggleDeleteButtonVisibility = () => {
setShowDeleteButton(!showDeleteButton);
};
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 ? 'Hide Delete Button' : 'Show Delete Button'}
</Button>
</HorizontalLayout>
<AutoForm
service={EmployeeService}
model={EmployeeModel}
item={editedItem}
deleteButtonVisible={showDeleteButton}
onDeleteSuccess={handleDeleteSuccess}
/>
</VerticalLayout>
);
The following properties are associated with managing the form actions error handling:
-
onSubmitError
: a callback that is called if an unexpected error occurs while submitting the form. The callback receives the error as a parameter. -
onDeleteError
: a callback that is called if an unexpected error occurs while deleting the item. The callback receives the error as a parameter.
The following example shows how to use the onSubmitError
and onDeleteError
callbacks to inform the user about the respective error:
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.
|
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 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 CrudService<T>
interface, as the CrudRepositoryService<T>
base class only works with JPA entities. The following methods need to be implemented:
-
List<T> list(Pageable pageable, Filter filter)
: Returns a list of paginated, sorted and filtered items. -
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.
Note
|
CrudService is a common interface shared with other components such as Auto CRUD and as such requires implementing a list method for fetching items. If you intend to use the service only with the Auto Form component, you can create a fake implementation of that method, for example by throwing an exception.
|
The following example shows a basic implementation of a custom CrudService
that wraps a JPA repository. The service only exposes DTO instances to the client and maps between the DTO instances and JPA entities internally.
@BrowserCallable
@AnonymousAllowed
public class ProductDtoCrudService implements CrudService<ProductDto, Long> {
private final ProductRepository productRepository;
private final JpaFilterConverter jpaFilterConverter;
public ProductDtoCrudService(ProductRepository productRepository, JpaFilterConverter jpaFilterConverter) {
this.productRepository = productRepository;
this.jpaFilterConverter = jpaFilterConverter;
}
@Override
public List<ProductDto> list(Pageable pageable, @Nullable Filter filter) {
// Basic list implementation that only covers pagination,
// but not sorting or filtering
Page<Product> products = productRepository.findAll(pageable);
return products.stream().map(ProductDto::fromEntity).toList();
}
@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);
}
}