You need this when one value in your model takes more than one input to enter, such as an amount with a currency, or a date split into day, month and year. A plain Panel can hold those inputs, but it is not a form component: it cannot be required, have validators, or write one combined value to its model. A FormComponentPanel is a panel with its own markup that is also a FormComponent.
Build the panel
Take a field for an amount of money in a currency. The model holds one Money value; the panel shows a number input and a currency list.
Money.java
public record Money(BigDecimal amount, Currency currency) implements Serializable {
}
MoneyField.java
public class MoneyField extends FormComponentPanel<Money> {
private final NumberTextField<BigDecimal> amount;
private final DropDownChoice<Currency> currency;
public MoneyField(String id, IModel<Money> model) {
super(id, model);
amount = new NumberTextField<>("amount", Model.of(), BigDecimal.class);
amount.setStep(new BigDecimal("0.01"));
add(amount);
List<Currency> currencies = List.of(
Currency.getInstance("EUR"), Currency.getInstance("USD"), Currency.getInstance("GBP"));
currency = new DropDownChoice<>("currency", Model.of(), currencies,
new LambdaChoiceRenderer<>(Currency::getCurrencyCode, Currency::getCurrencyCode));
add(currency);
}
MoneyField.html
<wicket:panel>
<input type="number" wicket:id="amount">
<select wicket:id="currency"></select>
</wicket:panel>
The inner fields get models of their own. The panel copies values between those models and its own model in two methods.
Combine the input in convertInput
When the form is processed, Wicket converts the inner fields before the panel. In convertInput(), read their converted input and set the combined value with setConvertedInput():
@Override
public void convertInput() {
BigDecimal value = amount.getConvertedInput();
Currency unit = currency.getConvertedInput();
setConvertedInput(value == null || unit == null ? null : new Money(value, unit));
}
The panel’s validators then run on that Money value, and if the whole form is valid, Wicket writes it to the panel’s model. If the panel is required and convertInput() produces null, the panel fails with the Required message.
Split the value in onBeforeRender
Before the panel renders, copy the parts of the model value into the inner fields:
@Override
protected void onBeforeRender() {
Money money = getModelObject();
amount.setModelObject(money == null ? null : money.amount());
currency.setModelObject(money == null ? null : money.currency());
super.onBeforeRender();
}
}
When the form has errors, the inner fields show what the user typed rather than their models, so this does not overwrite the user’s input.
Use the panel in a form
Bind the panel to any tag; Wicket renders the panel’s markup inside it.
form.add(new MoneyField("price",
LambdaModel.of(product, Product::getPrice, Product::setPrice)).setRequired(true));
<div wicket:id="price"></div>
Messages about the inner fields, such as an amount that is not a number, are reported on the inner fields and use their labels. Give them a label with setLabel() or a resource keyed by their id.
Update the panel with Ajax
Attach an AjaxFormComponentUpdatingBehavior to the panel to react when any of its inputs changes:
MoneyField price = new MoneyField("price", priceModel);
price.add(AjaxFormComponentUpdatingBehavior.onUpdate("change", target -> target.add(total)));
The change event of an inner input bubbles up to the panel’s tag. The behavior sends the inputs inside the panel, checks the inner fields and the panel, and updates their models before onUpdate() runs.
Since Wicket 11,
AjaxFormComponentUpdatingBehaviorprocesses the inner components of aFormComponentPanel. In Wicket 10 it only processes the panel itself, so attach the behavior to the inner fields instead and callprocessInput()on the panel inonUpdate(). To get the Wicket 10 behavior back in Wicket 11, overrideupdateInnerComponents()to returnfalse.