DateRangePicker.java
package com.vaadin.demo.component.customfield;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.customfield.CustomField;
import com.vaadin.flow.component.datepicker.DatePicker;
// tag::snippet[]
public class DateRangePicker extends CustomField<LocalDateRange> {
private DatePicker start;
private DatePicker end;
public DateRangePicker(String label) {
this();
setLabel(label);
}
public DateRangePicker() {
start = new DatePicker();
start.setPlaceholder("Start date");
// Sets title for screen readers
start.setAriaLabel("Start date");
end = new DatePicker();
end.setPlaceholder("End date");
end.setAriaLabel("End date");
// Enable manual validation on both date pickers to
// be able to override their invalid state
start.setManualValidation(true);
end.setManualValidation(true);
add(start, new Text(" – "), end);
}
@Override
protected LocalDateRange generateModelValue() {
return new LocalDateRange(start.getValue(), end.getValue());
}
@Override
protected void setPresentationValue(LocalDateRange dateRange) {
start.setValue(dateRange.getStartDate());
end.setValue(dateRange.getEndDate());
}
@Override
public void setInvalid(boolean invalid) {
super.setInvalid(invalid);
// Propagate invalid state to both date pickers so
// that they show a red background
start.setInvalid(invalid);
end.setInvalid(invalid);
}
}
// end::snippet[]
CustomFieldBasic.java
package com.vaadin.demo.component.customfield;
import com.vaadin.demo.domain.Appointment;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.router.Route;
import java.time.temporal.ChronoUnit;
@Route("custom-field-basic")
public class CustomFieldBasic extends Div {
public CustomFieldBasic() {
// tag::snippet[]
DateRangePicker dateRangePicker = new DateRangePicker();
dateRangePicker.setLabel("Enrollment period");
dateRangePicker.setHelperText("Cannot be longer than 30 days");
add(dateRangePicker);
Binder<Appointment> binder = new Binder();
binder.forField(dateRangePicker)
.asRequired("Enter a start and end date")
.withValidator(
localDateRange -> localDateRange.getStartDate() == null
|| localDateRange.getEndDate() == null
|| ChronoUnit.DAYS.between(
localDateRange.getStartDate(),
localDateRange.getEndDate()) <= 30,
"Dates cannot be more than 30 days apart")
.withValidator(
localDateRange -> localDateRange.getStartDate() == null
|| localDateRange.getEndDate() == null
|| localDateRange.getStartDate()
.isBefore(localDateRange.getEndDate()),
"Start date must be earlier than end date")
.bind(appointment -> new LocalDateRange(
appointment.getStartDate(), appointment.getEndDate()),
(appointment, localDateRange) -> {
appointment.setStartDate(
localDateRange.getStartDate());
appointment.setEndDate(localDateRange.getEndDate());
});
// end::snippet[]
}
}
LocalDateRange.java
package com.vaadin.demo.component.customfield;
import java.time.LocalDate;
// tag::snippet[]
public class LocalDateRange {
private LocalDate startDate;
private LocalDate endDate;
public LocalDateRange(LocalDate startDate, LocalDate endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
}
// end::snippet[]
Appointment.java
package com.vaadin.demo.domain;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
// tag::snippet[]
public class Appointment {
private LocalTime startTime;
private LocalDateTime startDateTime;
private LocalDate startDate;
private LocalTime endTime;
private LocalDateTime endDateTime;
private LocalDate endDate;
private String enrollmentPeriod;
private Integer id;
public LocalTime getStartTime() {
return startTime;
}
public void setStartTime(LocalTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getStartDateTime() {
return startDateTime;
}
public void setStartDateTime(LocalDateTime startDateTime) {
this.startDateTime = startDateTime;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalTime getEndTime() {
return endTime;
}
public void setEndTime(LocalTime endTime) {
this.endTime = endTime;
}
public LocalDateTime getEndDateTime() {
return endDateTime;
}
public void setEndDateTime(LocalDateTime endDateTime) {
this.endDateTime = endDateTime;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Appointment)) {
return false;
}
Appointment other = (Appointment) obj;
return id == other.id;
}
public String getEnrollmentPeriod() {
return enrollmentPeriod;
}
public void setEnrollmentPeriod(String enrollmentPeriod) {
this.enrollmentPeriod = enrollmentPeriod;
}
}
// end::snippet[]
custom-field-basic.tsx
import React from 'react';
import { differenceInDays, isAfter, parseISO } from 'date-fns';
import { useForm, useFormPart } from '@vaadin/hilla-react-form';
import { CustomField } from '@vaadin/react-components/CustomField.js';
import { DatePicker } from '@vaadin/react-components/DatePicker.js';
import AppointmentModel from 'Frontend/generated/com/vaadin/demo/domain/AppointmentModel';
function Example() {
const { model, field } = useForm(AppointmentModel);
const periodField = useFormPart(model.enrollmentPeriod);
periodField.addValidator({
message: 'Dates cannot be more than 30 days apart',
validate: (enrollmentPeriod: string) => {
const [from, to] = enrollmentPeriod.split('\t');
if (from === '' || to === '') {
return true;
}
return differenceInDays(parseISO(to), parseISO(from)) <= 30;
},
});
periodField.addValidator({
message: 'Start date must be earlier than end date',
validate: (enrollmentPeriod: string) => {
const [from, to] = enrollmentPeriod.split('\t');
if (from === '' || to === '') {
return true;
}
return isAfter(parseISO(to), parseISO(from));
},
});
return (
// tag::snippet[]
<CustomField
label="Enrollment period"
helperText="Cannot be longer than 30 days"
required
{...field(model.enrollmentPeriod)}
>
<DatePicker accessibleName="Start date" placeholder="Start date" />
–
<DatePicker accessibleName="End date" placeholder="End date" />
</CustomField>
// end::snippet[]
);
}
custom-field-basic.ts
import '@vaadin/custom-field';
import '@vaadin/date-picker';
import { differenceInDays, isAfter, parseISO } from 'date-fns';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { Binder, field } from '@vaadin/hilla-lit-form';
import AppointmentModel from 'Frontend/generated/com/vaadin/demo/domain/AppointmentModel';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('custom-field-basic')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
private binder = new Binder(this, AppointmentModel);
protected override firstUpdated() {
this.binder.for(this.binder.model.enrollmentPeriod).addValidator({
message: 'Dates cannot be more than 30 days apart',
validate: (enrollmentPeriod: string) => {
const [from, to] = enrollmentPeriod.split('\t');
if (from === '' || to === '') {
return true;
}
return differenceInDays(parseISO(to), parseISO(from)) <= 30;
},
});
this.binder.for(this.binder.model.enrollmentPeriod).addValidator({
message: 'Start date must be earlier than end date',
validate: (enrollmentPeriod: string) => {
const [from, to] = enrollmentPeriod.split('\t');
if (from === '' || to === '') {
return true;
}
return isAfter(parseISO(to), parseISO(from));
},
});
}
protected override render() {
return html`
<!-- tag::snippet[] -->
<vaadin-custom-field
label="Enrollment period"
helper-text="Cannot be longer than 30 days"
required
${field(this.binder.model.enrollmentPeriod)}
>
<vaadin-date-picker
accessible-name="Start date"
placeholder="Start date"
></vaadin-date-picker>
–
<vaadin-date-picker accessible-name="End date" placeholder="End date"></vaadin-date-picker>
</vaadin-custom-field>
<!-- end::snippet[] -->
`;
}
}