Skip to content

Subform

A subform is a nested form rendered inside a parent form. It has its own title, toolbar, button bar, and fields — and its actions are dispatched directly to the nested object, not to the parent.

Mateu detects subforms automatically from the type of a field. No special annotation is needed on the field itself.


A field of a complex type (class or record) is rendered as a subform when its type meets both of these conditions:

  1. It has at least one method (or field) annotated with @Button or @Toolbar.
  2. It has at least one non-action data field.

If neither condition is met, the nested type is rendered as an embedded field group (the legacy behaviour): the sub-fields appear inline inside the parent form with no chrome around them.


// Nested type — qualifies as a subform because it has @Toolbar and data fields
public record Subform1(String address, String city) {
@Toolbar
Object save() {
return Message.builder()
.text("Saved: " + this)
.build();
}
}
// Another nested type — qualifies as a subform because it has @Button
public record Subform2(@Stereotype(FieldStereotype.radio) Sex sex, Religion religion) {
@Button
Object confirm() {
return Message.builder()
.text("Confirmed: " + this)
.build();
}
}
// Parent form
@Route("/page3")
public class Page3 {
String name;
int age;
@Section("Address")
Subform1 subform1; // → rendered as a subform with a toolbar
@Section("Preferences")
Subform2 subform2; // → rendered as a subform with a button bar
@Toolbar
Object save() {
return Message.builder()
.text("Saved page: " + this)
.build();
}
}

Page3 renders as a single page that contains:

  • Its own toolbar with the save action.
  • A section titled “Address” containing a subform with address and city fields and a save toolbar button scoped to Subform1.
  • A section titled “Preferences” containing a subform with sex and religion fields and a confirm button scoped to Subform2.

When the user clicks a button inside a subform, the action is dispatched to the nested object, not to the parent. The nested object receives the current values of its own fields and executes the annotated method.

This means the parent form does not need to know about or delegate to the child’s actions. Each subform manages its own behaviour independently.


The same rules that apply to top-level forms apply inside a subform:

AnnotationRendered as
@ToolbarButton in the subform toolbar (top area)
@ButtonButton in the subform footer (bottom area)

If the nested type has data fields but no @Button or @Toolbar annotations, it is rendered as an embedded field group without any chrome:

public record Address(String street, String city, String zip) {}
public class CustomerForm {
String name;
Address address; // → fields embedded inline, no subform chrome
}

Use a subform when the nested object has its own actions. Use an embedded group when it is pure data with no actions.