Create Detail View
Use this skill when creating a create/edit view for one entity.
Two render-time killers that compile clean
Both produce valid Java/XML — compileJava is green — and then throw
when the view is opened. compileJava is BLIND to *-view.xml; only a
Jmix-aware inspection catches them statically, and the mechanical checks
do NOT cover these two. With no inspection, get them right BY
CONSTRUCTION from the WRONG/RIGHT examples below:
An enum attribute is NEVER
entityComboBox.entityComboBoxis for ENTITY references; binding it to an enum (with or without a made-upenumClassattribute) throwsIllegalStateException: Range is enumerationat render. There is noenumClassattribute onentityComboBox. For a Jmix enum property use a plain<comboBox>or<select>— Jmix auto-populates it from the enum:<!-- WRONG: <entityComboBox property="category" enumClass="...SomeEnum"/> --> <comboBox id="categoryField" property="category"/> <!-- enum: just bind the property -->itemsQueryMUST wrap its JPQL in a nested<query>element, and that query MUST reference:searchString(the combo passes it for type-ahead). Raw CDATA directly under<itemsQuery>throwsGuiDevelopmentException: Nested 'query' element is missing; a query that ignores:searchStringthrowsDevelopmentException: Parameter 'searchString' is not used in the queryat dropdown fetch. See "Reference Fields" below for the correct shape.
Read-only open mode vs read-only descriptor
These are two DIFFERENT things. Do not conflate them.
Read-only OPEN MODE is set by the LIST view's open action:
list_read instead of list_edit. list_read is an open MODE
(read-only at runtime), not a readOnly descriptor. Users opening an
existing row see a non-editable form; new entities still open in
writable mode through list_create.
Read-only DESCRIPTOR would mean every form field is hard-coded
readOnly="true" and the detail view has no save action — only
detail_close. This makes the view ALWAYS read-only, even for new
entities. It CANNOT create entities and CANNOT save edits.
"The list opens records in read mode" or "the detail view is opened
with the read action" — both phrasings call for read-only OPEN MODE
on the LIST side. The detail XML must still be a normal writable
descriptor with detail_saveClose and editable fields. Jmix flips
fields to read-only at runtime based on the open mode.
A detail view without a save action is broken for create flows. Always declare both:
<actions>
<action id="saveCloseAction" type="detail_saveClose"/>
<action id="closeAction" type="detail_close"/>
</actions>
and use editable form components (textField, comboBox,
entityComboBox, etc.) without hard-coded readOnly="true" unless
a specific attribute is permanently read-only in domain terms (e.g.
audit timestamps).
Steps
- Create Java controller under
view/<entityname>/. - Extend
StandardDetailView<Entity>. - Add
@Route(value = ".../:id", layout = MainView.class). - Add
@ViewController(id = "Entity.detail"). - Add
@ViewDescriptor(path = "entity-detail-view.xml"). - Add
@EditedEntityContainer("<entity>Dc"). - Create XML descriptor with instance container, loader,
dataLoadCoordinator, typed form fields,detail_saveClose, anddetail_close. - Configure reference fields with a verified data source: lookup action,
itemsContainer, oritemsQuery. - Use
InitEntityEventfor UI-only defaults ONLY. A required persistent default must be set at the ENTITY layer (field initializer /@PostConstruct/EntitySavingEvent) — NOT in anInitEntityEventalone, NOT in a service, NOT inEntityChangedEvent(it fires after persist and cannot satisfy@NotNull). Tests save viaDataManagerand bypass the view — seejmix-create-entity(required-field defaults). - Add message keys for the title and field labels.
- Grant access for roles that can open the detail view with
@ViewPolicy(viewIds = "Entity.detail")— declared on a method of a@ResourceRoleinterface (the annotation is@Target(METHOD)), not on the view controller. The annotation has novalue()member:@ViewPolicy("...")does not compile; useviewIds = "..."(orviewClasses = ...). Seejmix-create-resource-role. - Before finishing, compare every form field component against the Java property type.
Controller Template
@Route(value = "customers/:id", layout = MainView.class)
@ViewController(id = "Customer.detail")
@ViewDescriptor(path = "customer-detail-view.xml")
@EditedEntityContainer("customerDc")
public class CustomerDetailView extends StandardDetailView<Customer> {
}
XML Skeleton
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://customerDetailView.title"
focusComponent="form">
<data>
<instance id="customerDc" class="com.company.app.entity.Customer">
<fetchPlan extends="_base"/>
<loader id="customerDl"/>
</instance>
</data>
<facets>
<dataLoadCoordinator auto="true"/>
</facets>
<actions>
<action id="saveCloseAction" type="detail_saveClose"/>
<action id="closeAction" type="detail_close"/>
</actions>
<layout>
<formLayout id="form" dataContainer="customerDc">
<textField id="nameField" property="name"/>
</formLayout>
<hbox id="detailActions">
<button id="saveAndCloseButton" action="saveCloseAction"/>
<button id="closeButton" action="closeAction"/>
</hbox>
</layout>
</view>
Field Component Mapping
Choose form components by property type:
| Property type | Component |
|---|---|
String short text |
textField |
String long text |
textArea |
Integer |
integerField |
Long |
integerField or numberField according to project usage |
BigDecimal |
bigDecimalField |
Boolean |
checkbox |
LocalDate |
datePicker |
LocalDateTime |
dateTimePicker |
| Jmix enum | select or comboBox |
| Entity reference | entityComboBox or entityPicker |
For free-text fields with EAGER, LAZY, or TIMEOUT value updates, set
trimEnabled="false". Otherwise a client-originated update can trim the trailing
space or newline and rewrite the text before the user types the next word:
<textArea id="descriptionField" property="description"
valueChangeMode="TIMEOUT" trimEnabled="false"/>
If normalization is required, do it at an explicit commit or validation boundary.
Test a client-originated value ending in a space and a newline (or real typing),
not only server-side setValue, which bypasses this trimming branch.
For a Boolean property annotated with @NotNull, false is still a valid
value: the constraint rejects only null. A bound checkbox inherits the
required state from the entity metadata, but Vaadin treats an unchecked checkbox
as empty. The form then rejects false and the save action leaves the view open.
Declare required="false" explicitly so the checkbox can represent both Boolean
values without weakening the entity constraint:
<checkbox id="subscribedField" property="subscribed" required="false"/>
Do not expose technical fields (id, version) in user-facing forms. Hide parent/default fields only when they are initialized elsewhere.
Final XML Type Audit
After creating or editing the descriptor, inspect each field:
Integeris not atextField; useintegerField.BigDecimalis not atextField; usebigDecimalField.- Date/time properties use date/time picker components.
- Boolean properties use checkbox or the project's boolean component pattern. A
checkbox bound to an
@NotNull Booleanthat may befalsedeclaresrequired="false"explicitly. - Entity references use reference components, not text fields.
- Live free-text editors preserve whitespace while typing (
trimEnabled="false").
If an existing project uses a different compiled pattern for a type, follow the existing pattern and keep it consistent.
Reference Fields
For @ManyToOne and other entity references, pick the pattern by candidate-set characteristics:
entityComboBoxwithitemsQuery— PREFERRED. Lazy-loading: the JPQL runs against the database on every type-ahead, fetching only matching candidates. Use this for any non-trivial candidate set.entityComboBoxwithitemsContainer— for small, fixed sets that fit comfortably in memory and rarely change. Preloaded once at view open.entityPickerwith lookup and clear actions — when users need a full lookup screen. Use the literal standard action idsentity_lookupandentity_clear(do not invent ids).
itemsQuery — the correct shape
The combo passes a searchString parameter for type-ahead, so the
JPQL MUST like :searchString; ignoring it throws DevelopmentException: Parameter 'searchString' is not used in the query at dropdown fetch.
The JPQL must use the JPA/Jmix entity name, not the database table
name.
<entityComboBox id="refField" property="ref">
<itemsQuery class="com.company.app.entity.Ref">
<query>
<![CDATA[
select e from Ref e
where e.name like :searchString
order by e.name
]]>
</query>
</itemsQuery>
</entityComboBox>
itemsQuery does NOT auto-bind :container_* / :component_*
parameters — when the reference list depends on another component or
container, use itemsContainer with a regular loader instead (see
below).
itemsContainer — when the set is small or container-dependent
Declare a <collection> for the candidate entities and point the combo
at it:
<data>
<instance id="<entity>Dc" class="com.company.app.entity.<Entity>">
<fetchPlan extends="_base">
<property name="ref" fetchPlan="_instance_name"/>
</fetchPlan>
<loader id="<entity>Dl"/>
</instance>
<collection id="refsDc" class="com.company.app.entity.Ref">
<fetchPlan extends="_instance_name"/>
<loader id="refsDl">
<query><![CDATA[select e from Ref e order by e.name]]></query>
</loader>
</collection>
</data>
...
<entityComboBox id="refField" property="ref" itemsContainer="refsDc"/>
<dataLoadCoordinator auto="true"/> loads refsDc at open. Use this
when the candidate set is small enough to fit in memory, or when the
loader takes a :container_* / :component_* parameter that
itemsQuery cannot bind.
Before finishing, verify that saved reference entities can appear in the component data provider. If a field is required, do not leave a reference component without a working item source or lookup action.
To-many data grid
To show a to-many composition or association (Order.products, Customer.tags) on a detail
view, nest a property-bound <collection> INSIDE the <instance> container and
bind a dataGrid to it. The container infers its metaclass from the property, so
a class attribute here is a descriptor defect:
<instance id="orderDc" class="com.company.app.entity.Order">
<fetchPlan extends="_base">
<property name="products" fetchPlan="_instance_name"/>
</fetchPlan>
<loader id="orderDl"/>
<!-- property-bound: NO class, NO loader, NO query -->
<collection id="productsDc" property="products"/>
</instance>
<dataGrid id="productsDataGrid" dataContainer="productsDc" width="100%">
<columns>
<column property="name"/>
</columns>
</dataGrid>
The trap is that a TOP-LEVEL <collection> (a list view's own loader) REQUIRES
class. A nested property-bound collection takes property and nothing else — no class,
no loader, no query. The parent's fetch plan must include the property, or the
grid is empty (see jmix-configure-fetch-plan).
For a read-only grid, declare no list_create / list_edit / list_remove
actions on it.
Styling
Styling a field, a card, or a component you build in the controller — CSS classes,
component theme variants, getStyle().set(...), --aura-* / --lumo-* tokens —
is covered by jmix-style-ui. Read it before typing a CSS custom property: a
token the active theme does not define fails silently and passes every gate.
Asking for confirmation before saving
ValidationEvent can only REJECT a save; it cannot ask a question and continue.
The only hook that suspends a save and resumes it afterwards is
StandardDetailView.BeforeSaveEvent with preventSave() / resume():
@Autowired
private Dialogs dialogs;
@Subscribe
public void onBeforeSave(final BeforeSaveEvent event) {
if (!reissuesRelatedCodes()) {
return;
}
event.preventSave();
dialogs.createOptionDialog()
.withHeader(messageBundle.getMessage("confirmDialog.header"))
.withText(messageBundle.getMessage("confirmDialog.text"))
.withActions(
new DialogAction(DialogAction.Type.YES)
.withHandler(e -> event.resume()),
new DialogAction(DialogAction.Type.NO))
.open();
}
Note Dialogs.createOptionDialog() takes NO view argument, unlike
createInputDialog(View). Without preventSave() the save runs on regardless of what
a dialog is showing, so a home-made confirmation dialog does not hold anything back.
Cross-field validation
For cross-field/manual validation, add a @Subscribe handler on ValidationEvent and report failures via event.getErrors().add("..."); for programmatic checks (e.g. before a custom save) use the ViewValidation bean (validateUiComponents, showValidationErrors).
Do not rely solely on required or field validators for an invariant on a
secondary tabSheet tab. Inactive tab content is disabled, and field validation
skips disabled components. Keep those checks as UI affordances, but enforce the
save invariant in a ValidationEvent handler reading getEditedEntity(), not
only the currently enabled fields. Test saving without visiting the secondary
tab and after switching away from it; neither path may persist invalid state.
Forbidden
- Using list-view route or id patterns for detail views.
- Missing
detail_saveClose. - Using
textFieldfor numeric, date/time, boolean, or reference properties. - Reference fields without a working lookup action,
itemsContainer, or verifieditemsQuery. - Using
@Tablenames initemsQuery. itemsQuerywith unresolvedcontainer_orcomponent_parameters.- Hardcoded labels or titles.
- Hiding required fields without setting defaults elsewhere.
- A checkbox bound to an
@NotNull Booleanwithoutrequired="false"when uncheckedfalseis a valid value. - Missing view policy for dialog-opened detail views.
- A
classattribute on a nested property-bound<collection property="..."/>. <markdown>with an empty or absentcontentattribute (throws at view load), or a guessedio.jmix.flowui...Markdownimport.- CSS custom properties from a theme the app does not run (e.g.
--lumo-*in an Aura app) — seejmix-style-ui.