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

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

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

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

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

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

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

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

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

Далее предполагается, что проект имеет базовый пакет com/company/demo.

Дескриптор компоновки компонента

Создайте XML-дескриптор компоновки в файле com/company/demo/web/components/stepper/stepper-component.xml модуля web:

<composite xmlns="http://schemas.haulmont.com/cuba/screen/composite.xsd"> (1)
    <hbox id="rootBox" width="100%" expand="valueField"> (2)
        <textField id="valueField"/> (3)
        <button id="upBtn"
                icon="font-icon:CHEVRON_UP"/>
        <button id="downBtn"
                icon="font-icon:CHEVRON_DOWN"/>
    </hbox>
</composite>
1 - XSD определяет возможное содержимое дескриптора
2 - единственный корневой компонент
3 - любое количество вложенных компонентов
Класс имплементации компонента

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

package com.company.demo.web.components.stepper;

import com.haulmont.bali.events.Subscription;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.data.ValueSource;
import com.haulmont.cuba.web.gui.components.*;

import java.util.Collection;
import java.util.function.Consumer;

@CompositeDescriptor("stepper-component.xml") (1)
public class StepperField
        extends CompositeComponent<HBoxLayout> (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)

    public StepperField() {
        addCreateListener(this::onCreate); (8)
    }

    private void onCreate(CreateEvent createEvent) {
        valueField = getInnerComponent("valueField");
        upBtn = getInnerComponent("upBtn");
        downBtn = getInnerComponent("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() { (9)
        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 addValidator(Consumer<? super Integer> validator) {
        valueField.addValidator(validator);
    }

    @Override
    public void removeValidator(Consumer<Integer> validator) {
        valueField.removeValidator(validator);
    }

    @Override
    public Collection<Consumer<Integer>> getValidators() {
        return valueField.getValidators();
    }

    @Override
    public boolean isEditable() {
        return valueField.isEditable();
    }

    @Override
    public void setEditable(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 void removeValueChangeListener(Consumer<ValueChangeEvent<Integer>> listener) {
        valueField.removeValueChangeListener(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();
    }
}
1 - аннотация @CompositeDescriptor указывает путь к дескриптору компоновки компонента, который находится в том же пакете что и класс.
2 - класс компонента наследуется от CompositeComponent, параметризованного типом корневого компонента.
3 - компонент реализует интерфейс Field<Integer>, так как он предназначен для отображения и редактирования целочисленных значений.
4 - набор интерфейсов с дефолтными методами для реализации стандартной функциональности Generic UI компонента.
5 - имя компонента, используемое для регистрации в файле ui-component.xml (см. ниже).
6 - поля, содержащие ссылки на внутренние компоненты.
7 - свойство компонента, задающее значение изменения при нажатии на кнопки вверх/вниз. Свойство имеет публичные getter/setter методы и может быть назначено в XML экрана.
8 - инициализация компонента производится в слушателе события CreateEvent.
Загрузчик компонента

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

package com.company.demo.web.components.stepper;

import com.google.common.base.Strings;
import com.haulmont.cuba.gui.xml.layout.loaders.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, если оно указано.
Регистрация компонента

Для регистрации компонента и его загрузчика во фреймворке, создайте файл com/company/demo/ui-component.xml в модуле web:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<components xmlns="http://schemas.haulmont.com/cuba/components.xsd">
    <component>
        <name>stepperField</name>
        <componentLoader>com.company.demo.web.components.stepper.StepperFieldLoader</componentLoader>
        <class>com.company.demo.web.components.stepper.StepperField</class>
    </component>
</components>

Добавьте следующее свойство в com/company/demo/web-app.properties:

cuba.web.componentsConfig = +com/company/demo/ui-component.xml

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

XSD компонента

XSD требуется для использования компонента в XML-дескрипторах экранов. Определите ее в файле com/company/demo/ui-component.xsd модуля web:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns="http://schemas.company.com/demo/0.1/ui-component.xsd"
           attributeFormDefault="unqualified"
           elementFormDefault="qualified"
           targetNamespace="http://schemas.company.com/demo/0.1/ui-component.xsd"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:layout="http://schemas.haulmont.com/cuba/screen/layout.xsd">

    <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 компонента.
Использование компонента

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

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        xmlns:app="http://schemas.company.com/demo/0.1/ui-component.xsd" (1)
        caption="msg://caption"
        messagesPack="com.company.demo.web.components.sample">
    <data>
        <instance id="fooDc" class="com.company.demo.entity.Foo" view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <form id="form" dataContainer="fooDc">
            <column width="250px">
                <textField id="nameField" property="name"/>
                <app:stepperField id="ageField" property="limit" step="10"/> (2)
            </column>
        </form>
    </layout>
</window>
1 - namespace ссылается на XSD компонента.
2 - композитный компонент, соединенный с некоторым атрибутом limit сущности.
Собственный стиль

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

Сначала измените корневой компонент на CssLayout и назначьте стили внутренним компонентам. Кроме стилей, определенных в проекте (см. ниже), здесь используются следующие предопределенные стили: v-component-group, icon-only.

<composite xmlns="http://schemas.haulmont.com/cuba/screen/composite.xsd">
    <cssLayout id="rootBox" width="100%" stylename="v-component-group stepper-field">
        <textField id="valueField"/>
        <button id="upBtn"
                icon="font-icon:CHEVRON_UP"
                stylename="stepper-btn icon-only"/>
        <button id="downBtn"
                icon="font-icon:CHEVRON_DOWN"
                stylename="stepper-btn icon-only"/>
    </cssLayout>
</composite>

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

@CompositeDescriptor("stepper-component.xml")
public class StepperField
        extends CompositeComponent<CssLayout>
        implements ...

Сгенерируйте расширение темы в проекте (см. здесь как это сделать в Studio) и добавьте следующий код в файл modules/web/themes/hover/com.company.demo/hover-ext.scss:

@mixin com_company_demo-hover-ext {

  .stepper-field {
    display: flex;

    .stepper-btn {
      width: $v-unit-size;
      min-width: $v-unit-size;
    }
  }
}

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

stepper final