> Markdown version of [Custom Field](https://vaadin.com/docs/v23/components/custom-field). Section index: [llms.txt](https://vaadin.com/docs/v23/components/llms.txt)

# Custom Field

Custom Field is a component for wrapping multiple components as a single field. It has the same features as [Input Fields](https://vaadin.com/docs/v23/components/input-fields.md), such as its label, helper, validation, and data binding. Use it to create custom input components.

**TypeScript** — `custom-field-basic.ts`

```html
<vaadin-custom-field
  label="Enrollment period"
  helper-text="Cannot be longer than 30 days"
  required
  ${field(this.binder.model.enrollmentPeriod)}
>
  <vaadin-date-picker id="start" placeholder="Start date"></vaadin-date-picker>
  &ndash;
  <vaadin-date-picker id="end" placeholder="End date"></vaadin-date-picker>
</vaadin-custom-field>
```

**Java** — `DateRangePicker.java`

```java
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.getElement().executeJs(
                "this.focusElement.setAttribute('title', 'Start date')");

        end = new DatePicker();
        end.setPlaceholder("End date");
        end.getElement().executeJs(
                "this.focusElement.setAttribute('title', 'End date')");

        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());
    }
}
```

**Java** — `CustomFieldBasic.java`

```java
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());
                });
```

**Java** — `LocalDateRange.java`

```java
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;
    }
}
```

**Java** — `Appointment.java`

```java
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;
    }
}
```

## <a id="basic-usage"></a>Basic Usage

Custom Field is optimized for wrapping the following components:

- [Text Field](https://vaadin.com/docs/v23/components/text-field.md)

- [Number Filed](https://vaadin.com/docs/v23/components/number-field.md)

- [Password Field](https://vaadin.com/docs/v23/components/password-field.md)

- [Text Area](https://vaadin.com/docs/v23/components/text-area.md)

- [Select](https://vaadin.com/docs/v23/components/select.md)

- [Combo Box](https://vaadin.com/docs/v23/components/combo-box.md)

- [Date Picker](https://vaadin.com/docs/v23/components/date-picker.md)

- [Time Picker](https://vaadin.com/docs/v23/components/time-picker.md)

It can also be used to give a label, helper, and other field features for components that don’t have them built-in, such as [List Box](https://vaadin.com/docs/v23/components/list-box.md).

## <a id="value-type-and-format"></a>Value Type and Format

Custom Field handles the value types and formats differently in Java and TypeScript.

### <a id="java"></a>Java

Custom Field is a generic class that requires a value type. The value type can be anything: String, List, a bean, and so forth.

Implement the `generateModelValue` and `setPresentationValue` methods to define how to build the Custom Field value from the child component values and how to propagate the Custom Field value to the child components respectively.

The following example shows how to set up value propagation, using a bean as the value type:

```java
class Phone {
    private final String code;
    private final String number;

    public Phone(String code, String number) {
        this.code = code;
        this.number = number;
    }

    public String getCode() {
        return code;
    }

    public String getNumber() {
        return number;
    }
}

class PhoneField extends CustomField<Phone> {
    private final Select code = new Select();
    private final TextField number = new TextField();

    public PhoneField() {
        ...

        add(code, number);
    }

    @Override
    protected String generateModelValue() {
        return new Phone(code.getValue(), number.getValue());
    }

    @Override
    protected void setPresentationValue(Phone value) {
        code.setValue(value.getCode());
        number.setValue(value.getNumber());
    }
}
```

### <a id="typescript"></a>TypeScript

Custom Field only supports string values. The default value format is a string concatenation of the child component values, separated by the `\t` character.

You can customize the value format by defining your own value formatter and parser, as shown in the following example:

```typescript
render() {
  return html`
    <!-- Phone Custom Field -->
    <vaadin-custom-field
      .formatValue="${([code, number]: unknown[]) => {
        return code && number ? [code, number].join('|') : '';
      }}"
      .parseValue="${(value: string) => {
        return value ? value.split('|') : ['', ''];
      }}"
    >
      <!-- Country code -->
      <vaadin-select></vaadin-select>

      <!-- Phone number -->
      <vaadin-text-field></vaadin-text-field>
    </vaadin-custom-field>
  `
}
```

## <a id="native-input-fields"></a>Native Input Fields

Custom Field works with native HTML elements.

The `whitespace` variant can be used when components without an outer margin are used within Custom Field to compensate for the missing space between the label and the component itself.

**TypeScript** — `custom-field-native-input.ts`

```html
<vaadin-custom-field
  label="Payment information"
  theme="whitespace"
  @change="${(event: CustomFieldChangeEvent) => {
    this.customFieldValue = event.target.value ?? '';
  }}"
>
  <vaadin-horizontal-layout theme="spacing-s">
    <input
      aria-label="Cardholder name"
      pattern="[\\p{L} \\-]+"
      placeholder="Cardholder name"
      required
      type="text"
    />
    <input
      aria-label="Card number"
      pattern="[\\d ]{12,23}"
      placeholder="Card number"
      required
      type="text"
    />
    <input
      aria-label="Security code"
      pattern="[0-9]{3,4}"
      placeholder="Security code"
      required
      type="text"
    />
  </vaadin-horizontal-layout>
</vaadin-custom-field>
<p><b>Payment information:</b> ${this.customFieldValue}</p>
```

**Java** — `PaymentInformationField.java`

```java
public class PaymentInformationField extends CustomField<PaymentInformation> {

    private Input cardholderName;
    private Input cardNumber;
    private Input securityCode;

    public PaymentInformationField(String label) {
        this();
        setLabel(label);
    }

    public PaymentInformationField() {
        cardholderName = createInput("Cardholder name", "[\\p{L} \\-]+");
        cardNumber = createInput("Card number", "[\\d ]{12,23}");
        securityCode = createInput("Security code", "[0-9]{3,4}");

        HorizontalLayout layout = new HorizontalLayout(cardholderName,
                cardNumber, securityCode);
        // Removes default spacing
        layout.setSpacing(false);
        // Adds small amount of space between the components
        layout.getThemeList().add("spacing-s");

        // Increases padding of field's label
        addThemeVariants(CustomFieldVariant.LUMO_WHITESPACE);

        add(layout);
    }

    private Input createInput(String label, String pattern) {
        Input input = new Input();
        input.getElement().setAttribute("aria-label", label);
        input.getElement().setAttribute("pattern", pattern);
        input.getElement().setAttribute("required", true);
        input.setPlaceholder(label);
        input.setType("text");
        return input;
    }

    @Override
    protected PaymentInformation generateModelValue() {
        return new PaymentInformation(cardholderName.getValue(),
                cardNumber.getValue(), securityCode.getValue());
    }

    @Override
    protected void setPresentationValue(PaymentInformation paymentInformation) {
        cardholderName.setValue(paymentInformation.getCardholderName());
        cardNumber.setValue(paymentInformation.getCardNumber());
        securityCode.setValue(paymentInformation.getSecurityCode());
    }
}
```

**Java** — `CustomFieldNativeInput.java`

```java
PaymentInformationField paymentInformationField = new PaymentInformationField(
        "Payment information");
paymentInformationField.addValueChangeListener(event -> {
    value.setText(event.getValue().getCardholderName() + " "
            + event.getValue().getCardNumber() + " "
            + event.getValue().getSecurityCode());
});

value = new Span();
Paragraph paragraph = new Paragraph(
        new Html("<span><b>Payment information:</b> </span>"), value);

add(paymentInformationField, paragraph);
```

**Java** — `PaymentInformation.java`

```java
public class PaymentInformation {

    private String cardholderName;
    private String cardNumber;
    private String securityCode;

    public PaymentInformation(String cardholderName, String cardNumber,
            String securityCode) {
        this.cardholderName = cardholderName;
        this.cardNumber = cardNumber;
        this.securityCode = securityCode;
    }

    public String getCardholderName() {
        return cardholderName;
    }

    public void setCardholderName(String cardholderName) {
        this.cardholderName = cardholderName;
    }

    public String getCardNumber() {
        return cardNumber;
    }

    public void setCardNumber(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    public String getSecurityCode() {
        return securityCode;
    }

    public void setSecurityCode(String securityCode) {
        this.securityCode = securityCode;
    }
}
```

## <a id="supported-features"></a>Supported Features

- [Label](https://vaadin.com/docs/v23/components/input-fields.md#label)

- [Helper](https://vaadin.com/docs/v23/components/input-fields.md#helper)

- [Required](https://vaadin.com/docs/v23/components/input-fields.md#required)

## <a id="size-variants"></a>Size Variants

The small theme variant can be used to make Custom Field’s label, helper, and error message smaller. Custom Field doesn’t propagate its theme variant to its internal components, meaning each internal component’s theme variant must be set individually.

**TypeScript** — `custom-field-size-variants.ts`

```html
<vaadin-custom-field label="Price" theme="small">
  <vaadin-horizontal-layout theme="spacing-s">
    <vaadin-text-field id="amount" theme="small"></vaadin-text-field>
    <vaadin-select
      id="currency"
      .items="${this.currencies}"
      theme="small"
      style="width: 6em;"
    ></vaadin-select>
  </vaadin-horizontal-layout>
</vaadin-custom-field>
```

**Java** — `MoneyField.java`

```java
public class MoneyField extends CustomField<Money> {

    private TextField amount;
    private Select<String> currency;

    public MoneyField(String label) {
        this();
        setLabel(label);
    }

    public MoneyField() {
        amount = new TextField();
        // Sets title for screen readers
        amount.getElement()
                .executeJs("this.focusElement.setAttribute('title', 'Amount')");

        currency = new Select<>();
        currency.setItems("AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "USD");
        currency.setWidth("6em");
        currency.getElement().executeJs(
                "this.focusElement.setAttribute('title', 'Currency')");

        HorizontalLayout layout = new HorizontalLayout(amount, currency);
        // Removes default spacing
        layout.setSpacing(false);
        // Adds small amount of space between the components
        layout.getThemeList().add("spacing-s");

        add(layout);
    }

    public void addThemeVariant(CustomFieldVariant variant) {
        super.addThemeVariants(variant);
        amount.addThemeVariants(TextFieldVariant.valueOf(variant.name()));
        currency.addThemeVariants(SelectVariant.valueOf(variant.name()));
    }

    @Override
    protected Money generateModelValue() {
        return new Money(amount.getValue(), currency.getValue());
    }

    @Override
    protected void setPresentationValue(Money money) {
        amount.setValue(money.getAmount());
        currency.setValue(money.getCurrency());
    }
}
```

**Java** — `CustomFieldSizeVariants.java`

```java
MoneyField moneyField = new MoneyField("Price");
moneyField.addThemeVariant(CustomFieldVariant.LUMO_SMALL);
```

**Java** — `Money.java`

```java
public class Money {

    private String amount;
    private String currency;

    public Money(String amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public String getAmount() {
        return amount;
    }

    public void setAmount(String amount) {
        this.amount = amount;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }
}
```

`CB7FDF39-7653-4DF0-A0C0-9C2A2EE7EDBA`
