Композитные компоненты

Композитный компонент — это компонент, состоящий из других компонентов. Как и фрагменты экранов, композитные компоненты позволяют переиспользовать некоторую компоновку и логику презентации. Мы рекомендуем использовать композитные компоненты в следующих случаях:

  • Функциональность компонента может быть реализована комбинацией существующих визуальных компонентов. Если вам требуются какие-либо нестандартные возможности, используйте Универсальный JavaScriptComponent.

  • Компонент относительно прост и не загружает/сохраняет данные самостоятельно. В противном случае рассмотрите возможность создания фрагмента экранов.

Класс композитного компонента должен расширять базовый класс CompositeComponent. Композитный компонент должен иметь единственный компонент в качестве корня внутреннего дерева компонентов. Корневой компонент можно получить методом CompositeComponent.getComposition().

Внутренние компоненты удобно определять декларативно в XML. В этом случае класс компонента должен иметь аннотацию @CompositeDescriptor, задающую путь к файлу дескриптора. Если значение аннотации не начинается с символа /, файл дескриптора загружается из файла, находящегося в том же пакете, что и класс компонента.

Альтернативой является создание дерева внутренних компонентов программно, в обработчике события CreateEvent.

CreateEvent посылается фреймворком, когда он заканчивает инициализацию компонента. В этот момент, если компонент использует XML-дескриптор, он загружен, и метод getComposition() возвращает корневой внутренний компонент. Данное событие можно использовать как для дополнительной инициализации компонента, так и для создания внутренних компонентов без XML.

Ниже описывается пошаговое создание компонента Stepper, предназначенного для редактирования целочисленного значения в поле ввода нажатием на кнопки вверх/вниз рядом с полем.

Создание дескриптора компоновки компонента

Создайте XML-дескриптор с компоновкой компонента:

<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 определяет содержимое дескриптора компонента.
2 Один корневой компонент.
3 Любое количество вложенных компонентов.
4 Укажите имена стилей, которые будут определены позже в разделе Собственный стиль. Помимо пользовательских стилей, определенных в проекте, используются следующие предопределенные стили: v-component-group, icon-only.

Создание класса реализации компонента

Создайте класс реализации компонента в том же пакете:

@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 Аннотация @CompositeDescriptor указывает путь к дескриптору компоновки компонента, который находится в том же пакете, что и класс.
2 Класс компонента наследуется от CompositeComponent, параметризованного типом корневого компонента.
3 Компонент StepperField реализует интерфейс Field<Integer>, ак как он предназначен для отображения и редактирования целочисленных значений.
4 Набор интерфейсов с дефолтными методами для реализации стандартной функциональности компонента пользовательского интерфейса.
5 Имя компонента, используемое для регистрации этого компонента для распознавания фреймворком.
6 Поля, содержащие ссылки на внутренние компоненты.
7 Свойство компонента, задающее значение изменения при нажатии на кнопки вверх/вниз. Свойство имеет публичные getter/setter методы и может быть назначено в XML экрана.
8 Инициализация компонента производится в слушателе события CreateEvent.
9 У родительского компонента прослушиваются изменения свойства editable. Далее обновляется свойство editable компонента.

Вы можете автоматически связать бины Spring с классом реализации компонента, например:

protected MessageTools messageTools;

@Autowired
public void setMessageTools(MessageTools messageTools) {
    this.messageTools = messageTools;
}

Создание загрузчика компонентов

Создайте загрузчик компонента для того, чтобы компонент можно было использовать в XML-дескрипторах экранов:

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 Класс загрузчика должен наследоваться от AbstractComponentLoader, параметризованного классом компонента. В нашем случае, так как компонент реализует Field, необходимо воспользоваться более специфичным базовым классом AbstractFieldLoader.
2 Создание компонента по его имени.
3 Загрузка свойства step из XML, если оно указано.

Регистрация компонента

Чтобы зарегистрировать компонент и его загрузчик во фреймворке, создайте класс конфигурации Spring с аннотацией @Configuration для добавления или переопределения компонентов UI:

@Configuration
public class ComponentConfiguration {

    @Bean
    public ComponentRegistration stepperField() { (1)
        return ComponentRegistrationBuilder.create(StepperField.NAME)
                .withComponentClass(StepperField.class)
                .withComponentLoaderClass(StepperFieldLoader.class)
                .build();
    }
}
1 Определите объявление бина ComponentRegistration.

Приведенный выше код регистрирует новый компонент StepperField с:

  • именем: StepperField.NAME;

  • классом: StepperField.class;

  • именем XML-тега: StepperField.NAME;

  • классом загрузчика: StepperFieldLoader.class;

Теперь фреймворк сможет распознать новый компонент в XML-дескрипторах экранов приложения.

Используйте аннотацию Spring @Order для обработки порядка регистрации компонентов. Порядок предоставления бинов ComponentRegistration очень важен, потому что компоненты с одинаковыми именами будут отфильтрованы, если имеют более низкий приоритет. Например, рассмотрим две конфигурации:

  • конфигурация из некого дополнения:

    @Bean
    @Order(200)
    protected ComponentRegistration newButton() {
        return ComponentRegistrationBuilder.create(Chart.NAME)
                .withComponentClass(WebChart.class)
                .withComponentLoaderClass(ChartLoader.class)
                .build();
    }
  • конфигурация проекта с компонентом, переопределяющим компонент WebChart:

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

В этом случае компонент из дополнения имеет более низкий приоритет и не будет зарегистрирован. Это означает, что вам нужно предоставить полную информацию о компоненте MyWebChart: имя, тег (если он не совпадает с именем), класс компонента и класс загрузчика.

Создание XSD компонента

XSD требуется для использования компонента в XML-дескрипторах экранов.

Создайте файл app-ui-component.xsd в том же каталоге, что и дескриптор компоновки компонента:

<?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 Наследование всех базовых свойств поля.
2 Определение атрибута для свойства step.

Собственный стиль

Теперь давайте добавим собственные стили, указанные ранее в атрибуте stylename, для улучшения визуального представления компонента.

Создайте собственную тему и добавьте несколько стилей CSS:

@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);
    }
  }
}

Использование композитного компонента

Пример использования созданного компонента в экране приложения:

<?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 Namespace ссылается на XSD компонента.
2 Композитный компонент, связанный с атрибутом rating сущности.

Перезапустите сервер приложения и откройте экран. Форма с созданным композитным компонентом Stepper должна выглядеть следующим образом:

composite component