From vaadin-claude
Guides building forms with Binder and validation in Vaadin 25 Flow. Covers field binding, converters, validators, BeanValidationBinder, and form error handling.
npx claudepluginhub vaadin/claude-plugin --plugin vaadin-claudeThis skill uses the workspace's default tool permissions.
Use the Vaadin MCP tools (`search_vaadin_docs`, `get_component_java_api`) to look up the latest documentation whenever uncertain about a specific API detail. Always set `vaadin_version` to `"25"` and `ui_language` to `"java"`.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use the Vaadin MCP tools (search_vaadin_docs, get_component_java_api) to look up the latest documentation whenever uncertain about a specific API detail. Always set vaadin_version to "25" and ui_language to "java".
Binder connects UI fields to a Form Data Object (FDO) — a Java bean, record, or DTO. It handles reading values from the FDO into fields, writing field values back, converting between field types and model types, and validating at every level.
Binder can only bind components that implement HasValue (TextField, ComboBox, DatePicker, Checkbox, etc.).
Buffered mode — changes are held in the Binder until explicitly written:
Binder<Person> binder = new Binder<>(Person.class);
binder.readBean(person); // populate fields from bean
// ... user edits fields ...
if (binder.writeBeanIfValid(person)) {
service.save(person); // only writes if all validation passes
}
Write-through mode — changes are written to the bean immediately on field change:
binder.setBean(person); // binds directly; changes write through
Use buffered mode for forms with Save/Cancel buttons. Use write-through mode for settings panels or simple filters where every change should apply immediately.
binder.forField(nameField)
.asRequired("Name is required")
.withValidator(new StringLengthValidator("1-100 characters", 1, 100))
.bind(Person::getName, Person::setName);
binder.forField(emailField)
.asRequired()
.withValidator(new EmailValidator("Invalid email"))
.bind(Person::getEmail, Person::setEmail);
The chain order matters: forField() → asRequired() → withValidator() → withConverter() → withValidator() → bind(). Validators and converters execute in the order they appear.
binder.bind(nameField, Person::getName, Person::setName);
No validation configuration — only useful for simple cases.
If your bean uses Jakarta Bean Validation annotations (@NotEmpty, @Max, @Email, etc.), use BeanValidationBinder to pick them up automatically:
BeanValidationBinder<Person> binder = new BeanValidationBinder<>(Person.class);
binder.bindInstanceFields(this); // binds fields matching property names
bindInstanceFields() scans the view for fields whose names match bean properties. This is convenient but less explicit — prefer forField().bind() for complex forms.
When the field's value type doesn't match the bean property type, add a converter:
binder.forField(yearOfBirthField)
.withConverter(new StringToIntegerConverter("Enter a number"))
.bind(Person::getYearOfBirth, Person::setYearOfBirth);
Converters implicitly validate — if conversion fails, the error message is shown as a validation error.
Create type-safe value objects with converters:
binder.forField(emailField)
.withConverter(new EmailAddressConverter()) // String → EmailAddress
.withValidator(emailService::notAlreadyInUse, "Email already in use")
.bind(Person::getEmail, Person::setEmail);
Validators can be added after converters — they then validate the converted type.
binder.forField(titleField)
.asRequired() // visual indicator, empty check
.bind(Proposal::getTitle, Proposal::setTitle);
binder.forField(typeComboBox)
.asRequired("Please select a type") // custom error message
.bind(Proposal::getType, Proposal::setType);
Run whenever the field value changes. Use built-in validators when possible:
StringLengthValidator, EmailValidator, RegexpValidatorIntegerRangeValidator, DoubleRangeValidator, LongRangeValidatorDateRangeValidator, DateTimeRangeValidatorRangeValidator (generic, with Comparator)Custom validator with lambda:
binder.forField(ageField)
.withValidator(age -> age >= 0, "Age must be positive")
.bind(Person::getAge, Person::setAge);
Custom validator class:
public class PositiveIntegerValidator implements Validator<Integer> {
@Override
public ValidationResult apply(Integer value, ValueContext context) {
return value >= 0
? ValidationResult.ok()
: ValidationResult.error("Must be positive");
}
}
Some components have built-in validation (e.g., DatePicker min/max). These work alongside Binder validators. Disable them if needed:
binder.forField(datePicker)
.withDefaultValidator(false)
.bind(Bean::getDate, Bean::setDate);
Validate the entire FDO after all fields are processed. Essential for rules that span multiple fields:
binder.withValidator((bean, context) -> {
if (bean.getStartDate() != null && bean.getEndDate() != null
&& bean.getStartDate().isAfter(bean.getEndDate())) {
return ValidationResult.error("Start date must be before end date");
}
return ValidationResult.ok();
});
In buffered mode, binder-level validators only run when writeBean() or writeBeanIfValid() is called. In write-through mode, they run on every field change.
binder.validate() — runs all validators and updates UIbinder.isValid() — checks without updating UIbinder.writeBeanIfValid(bean) — returns false if invalidBinding-level errors display next to the field automatically.
Binder-level errors need a status label:
Div errorDisplay = new Div();
errorDisplay.addClassName(LumoUtility.TextColor.ERROR); // Lumo theme only; for Aura, use a custom CSS class
binder.setStatusLabel(errorDisplay);
Use FormLayout for automatic responsive column adjustment:
FormLayout form = new FormLayout();
form.add(nameField, emailField, phoneField);
form.setColspan(descriptionField, 2); // span multiple columns
form.setResponsiveSteps(
new ResponsiveStep("0", 1), // 1 column on mobile
new ResponsiveStep("500px", 2) // 2 columns at 500px+
);
bindInstanceFields — it's more readable, easier to maintain, and doesn't rely on field naming conventions.asRequired() on mandatory fields — it provides both the visual indicator and the empty-value check in one call.For the complete list of built-in validators, converter patterns, and form templates, see references/form-patterns.md.