组合组件

组合组件是由其他多个组件组合而成。与 界面 fragments 类似,组合组件也是一种可重用组件,能复用一些展示布局和逻辑。下列情况我们建议使用组合组件:

  • 组件功能可以用已有的 可视化组件 以组合的方式来实现。如果需要非标准功能,请使用 通用 JavaScript 组件

  • 组件相对比较简单,本身并不会加载或者保存数据。否则的话,考虑创建界面 fragment。

组合组件的类必须继承 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-groupicon-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 一组带有默认方法的接口,实现了 Jmix 标准 UI 组件的功能。
5 组件名称,用来注册组件,以便框架识别。
6 包含内部组件引用的字段。
7 组件的属性,定义单击一次上/下按钮能改变的值。具有公共 getter/setter,并能在界面 XML 中设置。
8 组件初始化在 CreateEvent 监听器内完成。
9 监听父组件编辑状态的变化并更新组件的状态。

可以在组件的实现类中注入 Spring beans,示例:

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 如果在 XML 中设置了 step 属性,则进行加载。

组件注册

为了在框架中注册组件及其加载器,需要创建一个带 @Configuration 注解的 Spring 配置类,用于添加或覆盖 UI 组件:

@Configuration
public class ComponentConfiguration {

    @Bean
    public ComponentRegistration stepperField() { (1)
        return ComponentRegistrationBuilder.create(StepperField.NAME)
                .withComponentClass(StepperField.class)
                .withComponentLoaderClass(StepperFieldLoader.class)
                .build();
    }
}
1 定义 ComponentRegistration bean。

上面的代码注册了新的 StepperField 组件:

  • name: StepperField.NAME

  • class: StepperField.class

  • XML tag name: StepperField.NAME

  • loader class: 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

如果需要在界面 XML 描述中使用组件,则必须有 XSD 定义。

组件布局描述 所在目录创建 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 命名空间引用了组件的 XSD。
2 组合组件连接到实体的 rating 属性。

重启应用程序服务并打开界面。带有我们组合 Stepper 组件的表单如下:

composite component