Customizing CRUD and listings
Mateu generates a full CRUD UI from your model.
Most real applications need to refine that default UI:
- hide fields
- adjust layouts
- customize lists
- add actions
- restrict what users can do
This section shows how to do that progressively, without breaking the model-driven approach.
Golden rule
Section titled “Golden rule”Customize the model first. Customize the orchestrator only when the model is not enough.
1. Start with default CRUD
Section titled “1. Start with default CRUD”@UI("/products")public class Products extends AutoCrud<Product> {
final ProductStore repository;
public Products(ProductStore repository) { this.repository = repository; }
@Override public CrudStore<Product> store() { return repository; }}public record Product( String id, String name, BigDecimal price, ProductStatus status) implements Identifiable {}Records vs classes for Identifiable
Section titled “Records vs classes for Identifiable”Identifiable requires a single method String id(). Java records satisfy this automatically — the compiler generates id() from the record component. Regular classes with Lombok’s @Data do not: Lombok generates getId(), not id(). If you use a class, add the override explicitly:
// ✅ record — id() generated automaticallypublic record Product(String id, String name) implements Identifiable {}// ✅ class — must add id() manually@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class Product implements Identifiable { String id; String name;
@Override public String id() { return id; }}Prefer records for row/entity types when they are immutable. Use classes when you need Lombok’s @Builder combined with @NoArgsConstructor (e.g. for JPA or in-memory stores that mutate the entity after construction).
This already gives you:
- list
- view
- edit
- create
2. Restrict capabilities
Section titled “2. Restrict capabilities”Add class-level annotations to AutoCrud (or any Crud subclass) to remove capabilities:
| Annotation | Effect |
|---|---|
@ReadOnly | Shorthand for @NotCreatable @NotEditable @NotDeletable — removes all write operations |
@NotCreatable | Hides the New button |
@NotEditable | Hides the Edit button in the detail view |
@NotDeletable | Hides the Delete button |
@NotNavigable | Hides the View button column — list rows are not clickable |
Common combinations:
| Intent | Annotations |
|---|---|
| Read-only catalogue with detail view | @ReadOnly |
| Simple read-only list (no detail) | @ReadOnly @NotNavigable |
| List you can add to but not click into | @NotNavigable |
| List you can edit but not create | @NotCreatable |
@UI("/products")@ReadOnlypublic class ProductCatalogue extends AutoCrud<Product> { ... }3. Control field visibility
Section titled “3. Control field visibility”Hide fields depending on context.
public record Product(
@HiddenInList String id,
String name,
BigDecimal price,
@HiddenInEditor ProductStatus status
) implements Identifiable {}Common annotations
Section titled “Common annotations”@HiddenInList@HiddenInView@HiddenInEditor@ReadOnly— on a field: makes that field read-only in forms
4. Control editing
Section titled “4. Control editing”@EditableOnlyWhenCreatingString id;@NotNullBigDecimal price;Mateu uses standard validation annotations:
@NotNull@NotEmpty@Email- etc.
5. Improve list rendering
Section titled “5. Improve list rendering”Status as badge
Section titled “Status as badge”@Status(defaultStatus = StatusType.NONE, mappings = { @StatusMapping(from = "ACTIVE", to = StatusType.SUCCESS), @StatusMapping(from = "DISCONTINUED", to = StatusType.DANGER)})ProductStatus status;This renders the field as a visual badge instead of plain text.
Labels and display
Section titled “Labels and display”@Overridepublic String toString() { return name;}Used in:
- lists
- lookups
- references
Button labels
Section titled “Button labels”Every built-in CRUD button label can be replaced by overriding the matching method on your AutoCrud (or any Crud) subclass:
| Method | Default label |
|---|---|
newLabel() | New |
saveLabel() | Save |
cancelLabel() | Cancel |
deleteLabel() | Delete |
editLabel() | Edit |
addAnotherLabel() | Add another — offered by the toast confirming a create (not on the view of an existing record) |
backToListLabel() | Back to list |
importLabel() | Import |
historyLabel() | History |
@UI("/products")public class Products extends AutoCrud<Product> {
@Override public String newLabel() { return "Add product"; }
@Override public String deleteLabel() { return "Remove"; }
private final ProductStore store;
public Products(ProductStore store) { this.store = store; }
@Override public CrudStore<Product> store() { return store; }}This is the simplest way to localise or rephrase the CRUD chrome without touching the generated screens.
6. Customize forms
Section titled “6. Customize forms”Layout
Section titled “Layout”@FormLayout(columns = 2)public record Product( String id, String name, BigDecimal price, ProductStatus status) implements Identifiable {}Grouping fields
Section titled “Grouping fields”@Section("General")String name;
@Section("Pricing")BigDecimal price;Consecutive fields sharing the same @Section name are grouped into one titled card.
Styling
Section titled “Styling”@Style("max-width:600px;margin:auto;")public class Products {}The record page
Section titled “The record page”A record’s page (/{id}) is the read-only form of the view class — for an AutoCrud<T>, T.
@FoldoutDetail turns it into an overview plus folding panels. For any other content, the view class
implements DetailContentSupplier and returns the component to show instead — for instance an
item overview, with the key information pinned beside tabs:
public class Student implements Identifiable, DetailContentSupplier { String id; String name; String email;
@Override public String id() { return id; }
@Override public Component detailContent(HttpRequest httpRequest) { return new StudentOverview(this); }}
class StudentOverview extends ItemOverview {
Component keyInfo;
@Panel(title = "Personal data") Component personal;
StudentOverview(Student student) { keyInfo = new Text("Email: " + student.email); personal = new Text("Name: " + student.name); }}Only the content changes. The header (title, subtitle, Status badges, KPIs) and the toolbar (Back to
list, Edit, @Toolbar and @ViewToolbarButton buttons) are still the CRUD’s, and the editor and the
creation form keep their regular forms although they are the same class. A button inside the content
reaches the CRUD like the toolbar’s do: "edit", "cancel-view", or "action-on-view-<method>" for
a @ViewToolbarButton method. Return null to keep the default form for a given record. The record
is what store().findById (or an overridden view(id, httpRequest)) returns, so load there whatever
else the content needs. The same hook applies to an editable view
in view mode.
7. Add actions
Section titled “7. Add actions”Page-level action
Section titled “Page-level action”@Buttonpublic Object publish() { return new Message("Published");}Mutating state
Section titled “Mutating state”@Buttonpublic Object discount() { price = price.multiply(BigDecimal.valueOf(0.9)); return List.of(new Message("Discount applied"), new State(this));}8. Customize list behavior
Section titled “8. Customize list behavior”Override fetchRows() directly on your AutoCrud subclass to control how the listing is populated — push filtering to the database, sort server-side, or apply pagination:
@UI("/products")public class Products extends AutoCrud<Product> {
final ProductStore repository; final ProductJpaRepository jpa;
public Products(ProductStore repository, ProductJpaRepository jpa) { this.repository = repository; this.jpa = jpa; }
@Override public CrudStore<Product> store() { return repository; }
@Override public ListingData<Product> fetchRows( String searchText, Product filters, Pageable pageable, HttpRequest httpRequest) { return ListingData.of(jpa.findByNameContainingIgnoreCase( searchText != null ? searchText : "")); }}Typical customizations:
- filtering
- sorting
- search behavior
- pagination
9. When the model is not enough
Section titled “9. When the model is not enough”Use:
Callable<?>→ dynamic UIroutes.yamlentries → custom pages- embedded orchestrators → master-detail
Example:
Callable<?> stats = () -> new HorizontalLayout( new KPI("Revenue", "10k"), new KPI("Orders", "120"));10. Anti-patterns
Section titled “10. Anti-patterns”Overusing custom pages
Section titled “Overusing custom pages”Do not create custom pages too early. Start with the model and the CRUD. Add custom pages only when the model-driven approach cannot express what you need.
Putting logic in the frontend
Section titled “Putting logic in the frontend”When your Java backend serves the UI, logic lives on the server: keep logic, validation, and state in your Java model. The UI is a projection of that model, not an independent stateful application.
11. Summary
Section titled “11. Summary”Customization in Mateu follows this progression:
- Model (annotations)
- Capability restrictions (
@ReadOnly,@NotNavigable,@NotCreatable,@NotEditable,@NotDeletable) - Validation
- Layout
- Actions
- Adapter
- Custom UI (Callable / Route)
Stay in the model as long as possible.
Move to more advanced techniques only when needed.
- Listing layout — understand how Mateu picks between table, list, cards, and master-detail, and how to override it
- Listing row actions — add per-row contextual actions with
ColumnActionandColumnActionGroup - Filtered orchestrators — add a dedicated filter model without leaving the auto variants
- Full control with Crud — explicit separate models for filters, rows, views, and forms
- Golden example: Orders, Customers and Order lines — see all of these techniques applied in a realistic business UI