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, должна выглядеть так: