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 @Order annotation to handle the order of registering components. The order of providing ComponentRegistration beans is very important because components with the same name will be filtered if they have lower priority. For example, we have two configurations:

  • configuration from some add-on:

    @Bean
    @Order(200)
    protected ComponentRegistration newButton() {
        return ComponentRegistrationBuilder.create(Chart.NAME)
                .withComponentClass(WebChart.class)
                .withComponentLoaderClass(ChartLoader.class)
                .build();
    }
  • project configuration with component overriding the WebChart component:

    @Bean
    @Order(100)
    protected ComponentRegistration newButton() {
        return ComponentRegistrationBuilder.create(Chart.NAME)
                .withComponentClass(MyWebChart.class)
                .withComponentLoaderClass(ChartLoader.class)
                .build();
    }

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 MyWebChart component: name, tag (if it is not the same as name), component class, and loader class.

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:

composite component