3.5.14. 组合组件
组合组件是由其它多个组件组合的组件。跟界面 fragment 类似,组合组件也是一种可重用组件,能复用展示布局和逻辑。下列情况我们建议使用组合组件:
-
组件功能可以使用现存的通用 UI 组件以组合的方式来实现。如果需要非标准功能,可以封装 Vaadin 组件或者 JavaScript 库来创建自定义组件,或者使用通用 JavaScriptComponent。
-
组件相对比较简单,并不会加载或者保存数据。否则的话,考虑创建界面 fragment。
组合组件的类必须继承 CompositeComponent
基类。组合组件必须以一个单一组件为内部组件树的基础 - 称为根组件。根组件可以通过 CompositeComponent.getComposition()
方法获取。
内部组件通常在 XML 描述中通过声明式的方式创建。因此,组件类必须要有 @CompositeDescriptor
注解,用来指定相应描述文件的路径。如果注解值不是以 /
开头的话,会从组件类的包内加载该文件。
需要注意,界面中的内部组件,必须有唯一的 ID,以避免监听器和注入时发生混乱。推荐使用一些带前缀的标识符,如 |
另外,内部组件树也可以在 CreateEvent
监听器内通过编程的方式创建。
当框架完成组件的初始化之后,会发出 CreateEvent
事件。此时,如果组件使用了 XML 描述,则会进行加载并通过 getComposition()
方法返回根组件。这个事件可以用来添加更多的任何组件初始化,或者用来创建内部组件(不使用XML)。
下面我们示范如何创建 Stepper (步进)组件,并通过点击控件旁边的上下按钮来编辑输入框的整数值。
我们假设项目的包结构以 com/company/demo
为基础。
- 组件布局描述
-
在
web
模块创建带有组件布局的 XML 描述文件com/company/demo/web/components/stepper/stepper-component.xml
:<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 - 一组带有默认方法的借口,实现了标准通用 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 - 如果在 XML 中设置了 step
属性,则进行加载。
- 注册组件
-
为了在框架中注册组件及其加载器,在
web
模块创建com/company/demo/ui-component.xml
文件:<?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
-
如果需要在界面 XML 描述中使用组件,则 XSD 是必须的。在
web
模块的com/company/demo/ui-component.xsd
文件定义:<?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 | - 命名空间引用了组件的 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; } } }
重启应用程序服务并打开界面。带有我们组合步进组件的表单如下: