Композитные компоненты
Композитный компонент — это компонент, состоящий из других компонентов. Как и фрагменты экранов, композитные компоненты позволяют переиспользовать некоторую компоновку и логику презентации. Мы рекомендуем использовать композитные компоненты в следующих случаях:
-
Функциональность компонента может быть реализована комбинацией существующих визуальных компонентов. Если вам требуются какие-либо нестандартные возможности, используйте Универсальный 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
В этом случае компонент из дополнения имеет более низкий приоритет и не будет зарегистрирован. Это означает, что вам нужно предоставить полную информацию о компоненте |
Создание 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
должна выглядеть следующим образом: