Composite Components
A composite component is a component consisting of other components. Like screen fragments, composite components allow you to reuse some presentation layout and logic. We recommend using composite components in the following cases:
-
The component functionality can be implemented as a combination of existing visual components. If you need some non-standard features, use Generic JavaScriptComponent.
-
The component is relatively simple and does not load or save data itself. Otherwise, consider creating a screen fragment.
The class of a composite component must extend the CompositeComponent
base class. A composite component must have a single component at the root of the inner components tree. The root component can be obtained using the CompositeComponent.getComposition()
method.
Inner components are usually created declaratively in an XML descriptor. In this case, the component class should have the @CompositeDescriptor
annotation, which specifies the path to the descriptor file. If the annotation value does not start with /, the file is loaded from the component’s class package.
Alternatively, the inner components tree can be created programmatically in a CreateEvent
listener.
CreateEvent
is sent by the framework when it finishes the initialization of the component. At this moment, if the component uses an XML descriptor, it is loaded, and the getComposition()
method returns the root component. This event can be used for any additional initialization of the component or creating the inner components without XML.
Below, we demonstrate the creation of the Stepper
component, which is designed to edit integer values in the input field by clicking on up/down buttons located next to the field.
Creating Component Layout Descriptor
Create an XML descriptor with the component layout:
<composite xmlns="http://jmix.io/schema/ui/composite"> (1)
<cssLayout id="stepper_rootBox"
width="100%"
stylename="v-component-group stepper-field"> (2)
<textField id="stepper_valueField"
datatype="int"/> (3)
<button id="stepper_upBtn"
icon="font-icon:CHEVRON_UP"
stylename="stepper-btn icon-only"/> (4)
<button id="stepper_downBtn"
icon="font-icon:CHEVRON_DOWN"
stylename="stepper-btn icon-only"/>
</cssLayout>
</composite>
1 | XSD defines the content of the component descriptor. |
2 | A single root component. |
3 | Any number of nested components. |
4 | Specify names of styles, which will be defined later in Custom Styling. Besides custom styles defined in the project, the following predefined styles are used: v-component-group, icon-only . |
Creating Component Implementation Class
Create the component implementation class in the same package:
@CompositeDescriptor("stepper-component.xml") (1)
public class StepperField
extends CompositeComponent<CssLayout> (2)
implements Field<Integer>, (3)
CompositeWithCaption, (4)
CompositeWithHtmlCaption,
CompositeWithHtmlDescription,
CompositeWithIcon,
CompositeWithContextHelp {
public static final String NAME = "stepperField"; (5)
private TextField<Integer> valueField; (6)
private Button upBtn;
private Button downBtn;
private int step = 1; (7)
private boolean editable = true;
private Subscription parentEditableChangeListener;
public StepperField() {
addCreateListener(this::onCreate); (8)
}
private void onCreate(CreateEvent createEvent) {
valueField = getInnerComponent("stepper_valueField");
upBtn = getInnerComponent("stepper_upBtn");
downBtn = getInnerComponent("stepper_downBtn");
upBtn.addClickListener(clickEvent -> updateValue(step));
downBtn.addClickListener(clickEvent -> updateValue(-step));
}
private void updateValue(int delta) {
Integer value = getValue();
setValue(value != null ? value + delta : delta);
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
@Override
public boolean isRequired() {
return valueField.isRequired();
}
@Override
public void setRequired(boolean required) {
valueField.setRequired(required);
getComposition().setRequiredIndicatorVisible(required);
}
@Override
public String getRequiredMessage() {
return valueField.getRequiredMessage();
}
@Override
public void setRequiredMessage(String msg) {
valueField.setRequiredMessage(msg);
}
@Override
public void setParent(Component parent) {
if (getParent() instanceof EditableChangeNotifier
&& parentEditableChangeListener != null) {
parentEditableChangeListener.remove();
parentEditableChangeListener = null;
}
super.setParent(parent);
if (parent instanceof EditableChangeNotifier) { (9)
parentEditableChangeListener = ((EditableChangeNotifier) parent).addEditableChangeListener(event -> {
boolean parentEditable = event.getSource().isEditable();
boolean finalEditable = parentEditable && isEditable();
setEditableInternal(finalEditable);
});
Editable parentEditable = (Editable) parent;
if (!parentEditable.isEditable()) {
setEditableInternal(false);
}
}
}
@Override
public boolean isEditable() {
return editable;
}
@Override
public void setEditable(boolean editable) {
if (this.editable != editable) {
setEditableInternal(editable);
}
}
private void setEditableInternal(boolean editable) {
valueField.setEditable(editable);
upBtn.setEnabled(editable);
downBtn.setEnabled(editable);
}
@Override
public Integer getValue() {
return valueField.getValue();
}
@Override
public void setValue(Integer value) {
valueField.setValue(value);
}
@Override
public Subscription addValueChangeListener(Consumer<ValueChangeEvent<Integer>> listener) {
return valueField.addValueChangeListener(listener);
}
@Override
public boolean isValid() {
return valueField.isValid();
}
@Override
public void validate() throws ValidationException {
valueField.validate();
}
@Override
public void setValueSource(ValueSource<Integer> valueSource) {
valueField.setValueSource(valueSource);
getComposition().setRequiredIndicatorVisible(valueField.isRequired());
}
@Override
public ValueSource<Integer> getValueSource() {
return valueField.getValueSource();
}
@Override
public void addValidator(Validator<? super Integer> validator) {
valueField.addValidator(validator);
}
@Override
public void removeValidator(Validator<Integer> validator) {
valueField.removeValidator(validator);
}
@Override
public Collection<Validator<Integer>> getValidators() {
return valueField.getValidators();
}
}
1 | The @CompositeDescriptor annotation specifies the path to the component layout descriptor, which is located in the class package. |
2 | The component class extends CompositeComponent parameterized by the type of the root component. |
3 | The StepperField component implements the Field<Integer> interface because it is designed to display and edit an integer value. |
4 | A set of interfaces with default methods to implement standard Jmix UI component functionality. |
5 | Name of the component which is used to register this component to be recognized by the framework. |
6 | Fields containing references to inner components. |
7 | Component’s property, which defines the value of a single click to up/down buttons. It has a public getter/setter methods and can be assigned in screen XML. |
8 | Component initialization is done in the CreateEvent listener. |
9 | Listen to parent’s editable state changes and update component’s editable state accordingly. |
You can autowire Spring beans to the component implementation class, for example:
protected MessageTools messageTools;
@Autowired
public void setMessageTools(MessageTools messageTools) {
this.messageTools = messageTools;
}
Creating Component Loader
Create the component loader which is needed to initialize the component when it is used in screen XML descriptors:
package ui.ex1.components.stepper;
import com.google.common.base.Strings;
import io.jmix.ui.xml.layout.loader.AbstractFieldLoader;
public class StepperFieldLoader extends AbstractFieldLoader<StepperField> { (1)
@Override
public void createComponent() {
resultComponent = factory.create(StepperField.NAME); (2)
loadId(resultComponent, element);
}
@Override
public void loadComponent() {
super.loadComponent();
String incrementStr = element.attributeValue("step"); (3)
if (!Strings.isNullOrEmpty(incrementStr)) {
resultComponent.setStep(Integer.parseInt(incrementStr));
}
}
}
1 | Loader class must extend AbstractComponentLoader parameterized by the class of the component. As our component implements Field , use a more specific AbstractFieldLoader base class. |
2 | Create the component by its name. |
3 | Load the step property value from XML if it is specified. |
Component Registration
To register the component and its loader with the framework, you should create a Spring configuration class with the @Configuration
annotation for adding or overriding UI components:
@Configuration
public class ComponentConfiguration {
@Bean
public ComponentRegistration stepperField() { (1)
return ComponentRegistrationBuilder.create(StepperField.NAME)
.withComponentClass(StepperField.class)
.withComponentLoaderClass(StepperFieldLoader.class)
.build();
}
}
1 | Define the ComponentRegistration bean declaration. |
The code above registers the new StepperField
component with:
-
name:
StepperField.NAME
; -
class:
StepperField.class
; -
XML tag name:
StepperField.NAME
; -
loader class:
StepperFieldLoader.class
;
Now the framework will recognize the new component in XML descriptors of application screens.
Use the Spring
In this case, the component from the add-on has lower priority and will not be registered at all. It means that you should provide complete information for the |
Creating Component XSD
XSD is required to use the component in screen XML descriptors.
Create the app-ui-component.xsd
file in same directory as the component layout descriptor:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns="http://schemas.company.com/ui.ex1/0.1/app-ui-components.xsd"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
targetNamespace="http://schemas.company.com/ui.ex1/0.1/app-ui-components.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:layout="http://jmix.io/schema/ui/layout">
<xs:element name="stepperField">
<xs:complexType>
<xs:complexContent>
<xs:extension base="layout:baseFieldComponent"> (1)
<xs:attribute name="step" type="xs:integer"/> (2)
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
</xs:schema>
1 | Inherit all base field properties. |
2 | Define an attribute for the step property. |
Custom Styling
Now let’s apply some custom styles specified earlier in the stylename attribute to improve the component look.
Create a custom theme and add some CSS styles:
@import "../helium/helium";
@mixin helium-ext {
@include helium;
.stepper-field {
display: flex;
.stepper-btn {
width: $v-unit-size;
min-width: $v-unit-size;
border: 1px solid var(--border-color);
}
}
}
Using Composite Component
The following example shows how the component can be used in a screen:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://jmix.io/schema/ui/window"
caption="msg://compositeComponentScreen.caption"
xmlns:app="http://schemas.company.com/ui.ex1/0.1/app-ui-components.xsd"> (1)
<data>
<instance id="orderDc" class="ui.ex1.entity.Order">
<fetchPlan extends="_base"/>
<loader id="orderDl"/>
</instance>
</data>
<layout>
<form dataContainer="orderDc">
<dateField property="dateTime"/>
<textField property="amount"/>
<app:stepperField id="ageField"
property="rating"
step="10"/> (2)
</form>
</layout>
</window>
1 | The namespace referencing the component’s XSD. |
2 | The composite component connected to the rating attribute of an entity. |
Restart the application server and open the screen. The form with our composite Stepper
component should look as follows: