3.5.14. 组合组件

在线示例

组合组件是由其它多个组件组合的组件。跟界面 fragment 类似,组合组件也是一种可重用组件,能复用展示布局和逻辑。下列情况我们建议使用组合组件:

  • 组件功能可以使用现存的通用 UI 组件以组合的方式来实现。如果需要非标准功能,可以封装 Vaadin 组件或者 JavaScript 库来创建自定义组件,或者使用通用 JavaScriptComponent

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

组合组件的类必须继承 CompositeComponent 基类。组合组件必须以一个单一组件为内部组件树的基础 - 称为根组件。根组件可以通过 CompositeComponent.getComposition() 方法获取。

内部组件通常在 XML 描述中通过声明式的方式创建。因此,组件类必须要有 @CompositeDescriptor 注解,用来指定相应描述文件的路径。如果注解值不是以 / 开头的话,会从组件类的包内加载该文件。

另外,内部组件树也可以在 CreateEvent 监听器内通过编程的方式创建。

当框架完成组件的初始化之后,会发出 CreateEvent 事件。此时,如果组件使用了 XML 描述,则会进行加载并通过 getComposition() 方法返回根组件。这个事件可以用来添加更多的任何组件初始化,或者用来创建内部组件(不使用XML)。

下面我们示范如何创建 Stepper (步进)组件,并通过点击控件旁边的上下按钮来编辑输入框的整数值。

  1. 组件布局描述

  2. 组件实现类

  3. 组件加载器

  4. 注册组件

  5. 组件 XSD

  6. 使用组件

  7. 自定义样式

我们假设项目的包结构以 com/company/demo 为基础。

组件布局描述

web 模块创建带有组件布局的 XML 描述文件 com/company/demo/web/components/stepper/stepper-component.xml

  1. <composite xmlns="http://schemas.haulmont.com/cuba/screen/composite.xsd"> (1)
  2. <hbox id="rootBox" width="100%" expand="valueField"> (2)
  3. <textField id="valueField"/> (3)
  4. <button id="upBtn"
  5. icon="font-icon:CHEVRON_UP"/>
  6. <button id="downBtn"
  7. icon="font-icon:CHEVRON_DOWN"/>
  8. </hbox>
  9. </composite>
1- XSD 定义了组件描述的内容
2- 单一的根组件
3- 任何数量的内部组件

组件实现类

在同一个包内创建组件的实现类:

  1. package com.company.demo.web.components.stepper;
  2. import com.haulmont.bali.events.Subscription;
  3. import com.haulmont.cuba.gui.components.*;
  4. import com.haulmont.cuba.gui.components.data.ValueSource;
  5. import com.haulmont.cuba.web.gui.components.*;
  6. import java.util.Collection;
  7. import java.util.function.Consumer;
  8. @CompositeDescriptor("stepper-component.xml") (1)
  9. public class StepperField
  10. extends CompositeComponent<HBoxLayout> (2)
  11. implements Field<Integer>, (3)
  12. CompositeWithCaption, (4)
  13. CompositeWithHtmlCaption,
  14. CompositeWithHtmlDescription,
  15. CompositeWithIcon,
  16. CompositeWithContextHelp {
  17. public static final String NAME = "stepperField"; (5)
  18. private TextField<Integer> valueField; (6)
  19. private Button upBtn;
  20. private Button downBtn;
  21. private int step = 1; (7)
  22. public StepperField() {
  23. addCreateListener(this::onCreate); (8)
  24. }
  25. private void onCreate(CreateEvent createEvent) {
  26. valueField = getInnerComponent("valueField");
  27. upBtn = getInnerComponent("upBtn");
  28. downBtn = getInnerComponent("downBtn");
  29. upBtn.addClickListener(clickEvent -> updateValue(step));
  30. downBtn.addClickListener(clickEvent -> updateValue(-step));
  31. }
  32. private void updateValue(int delta) {
  33. Integer value = getValue();
  34. setValue(value != null ? value + delta : delta);
  35. }
  36. public int getStep() {
  37. return step;
  38. }
  39. public void setStep(int step) {
  40. this.step = step;
  41. }
  42. @Override
  43. public boolean isRequired() { (9)
  44. return valueField.isRequired();
  45. }
  46. @Override
  47. public void setRequired(boolean required) {
  48. valueField.setRequired(required);
  49. getComposition().setRequiredIndicatorVisible(required);
  50. }
  51. @Override
  52. public String getRequiredMessage() {
  53. return valueField.getRequiredMessage();
  54. }
  55. @Override
  56. public void setRequiredMessage(String msg) {
  57. valueField.setRequiredMessage(msg);
  58. }
  59. @Override
  60. public void addValidator(Consumer<? super Integer> validator) {
  61. valueField.addValidator(validator);
  62. }
  63. @Override
  64. public void removeValidator(Consumer<Integer> validator) {
  65. valueField.removeValidator(validator);
  66. }
  67. @Override
  68. public Collection<Consumer<Integer>> getValidators() {
  69. return valueField.getValidators();
  70. }
  71. @Override
  72. public boolean isEditable() {
  73. return valueField.isEditable();
  74. }
  75. @Override
  76. public void setEditable(boolean editable) {
  77. valueField.setEditable(editable);
  78. upBtn.setEnabled(editable);
  79. downBtn.setEnabled(editable);
  80. }
  81. @Override
  82. public Integer getValue() {
  83. return valueField.getValue();
  84. }
  85. @Override
  86. public void setValue(Integer value) {
  87. valueField.setValue(value);
  88. }
  89. @Override
  90. public Subscription addValueChangeListener(Consumer<ValueChangeEvent<Integer>> listener) {
  91. return valueField.addValueChangeListener(listener);
  92. }
  93. @Override
  94. public void removeValueChangeListener(Consumer<ValueChangeEvent<Integer>> listener) {
  95. valueField.removeValueChangeListener(listener);
  96. }
  97. @Override
  98. public boolean isValid() {
  99. return valueField.isValid();
  100. }
  101. @Override
  102. public void validate() throws ValidationException {
  103. valueField.validate();
  104. }
  105. @Override
  106. public void setValueSource(ValueSource<Integer> valueSource) {
  107. valueField.setValueSource(valueSource);
  108. getComposition().setRequiredIndicatorVisible(valueField.isRequired());
  109. }
  110. @Override
  111. public ValueSource<Integer> getValueSource() {
  112. return valueField.getValueSource();
  113. }
  114. }
1- @CompositeDescriptor 注解指定了组件布局的描述文件路径,这个文件也在同一包内。
2- 组件类继承了 CompositeComponent,使用根组件的类型作为参数。
3- 组件实现了 Field<Integer> 接口,因为组件要用来展示和编辑一个整数值。
4- 一组带有默认方法的借口,实现了标准通用 UI 组件的功能。
5- 组件名称,用来在 ui-component.xml 文件内注册组件,以便框架识别。
6- 包含引用内部组件的字段。
7- 组件的属性,定义单击一次上/下按钮能改变的值。具有公共 getter/setter,并能在界面 XML 中设置。
8- 组件初始化在 CreateEvent 监听器内完成。

组件加载器

创建组件加载器,当组件在界面 XML 描述中使用的时候需要用加载器进行初始化:

  1. package com.company.demo.web.components.stepper;
  2. import com.google.common.base.Strings;
  3. import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader;
  4. public class StepperFieldLoader extends AbstractFieldLoader<StepperField> { (1)
  5. @Override
  6. public void createComponent() {
  7. resultComponent = factory.create(StepperField.NAME); (2)
  8. loadId(resultComponent, element);
  9. }
  10. @Override
  11. public void loadComponent() {
  12. super.loadComponent();
  13. String incrementStr = element.attributeValue("step"); (3)
  14. if (!Strings.isNullOrEmpty(incrementStr)) {
  15. resultComponent.setStep(Integer.parseInt(incrementStr));
  16. }
  17. }
  18. }
1- 加载器累必须使用组件的类作为参数继承 AbstractComponentLoader。由于我们的组件实现了 Field,所以可以用更具体的 AbstractFieldLoader 作为基类。
2- 使用组件名称创建组件。
3- 如果在 XML 中设置了 step 属性,则进行加载。

注册组件

为了在框架中注册组件及其加载器,在 web 模块创建 com/company/demo/ui-component.xml 文件:

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

com/company/demo/web-app.properties 中添加下列属性:

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

现在框架能识别应用程序界面 XML 中包含的新组件了。

组件 XSD

如果需要在界面 XML 描述中使用组件,则 XSD 是必须的。在 web 模块的 com/company/demo/ui-component.xsd 文件定义:

  1. <?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2. <xs:schema xmlns="http://schemas.company.com/demo/0.1/ui-component.xsd"
  3. attributeFormDefault="unqualified"
  4. elementFormDefault="qualified"
  5. targetNamespace="http://schemas.company.com/demo/0.1/ui-component.xsd"
  6. xmlns:xs="http://www.w3.org/2001/XMLSchema"
  7. xmlns:layout="http://schemas.haulmont.com/cuba/screen/layout.xsd">
  8. <xs:element name="stepperField">
  9. <xs:complexType>
  10. <xs:complexContent>
  11. <xs:extension base="layout:baseFieldComponent"> (1)
  12. <xs:attribute name="step" type="xs:integer"/> (2)
  13. </xs:extension>
  14. </xs:complexContent>
  15. </xs:complexType>
  16. </xs:element>
  17. </xs:schema>
1- 继承所有基本的字段属性。
2- 为 step 定义属性。

使用组件

下面的示例展示了如何在界面中使用该组件:

  1. <?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2. <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
  3. xmlns:app="http://schemas.company.com/demo/0.1/ui-component.xsd" (1)
  4. caption="msg://caption"
  5. messagesPack="com.company.demo.web.components.sample">
  6. <data>
  7. <instance id="fooDc" class="com.company.demo.entity.Foo" view="_local">
  8. <loader/>
  9. </instance>
  10. </data>
  11. <layout>
  12. <form id="form" dataContainer="fooDc">
  13. <column width="250px">
  14. <textField id="nameField" property="name"/>
  15. <app:stepperField id="ageField" property="limit" step="10"/> (2)
  16. </column>
  17. </form>
  18. </layout>
  19. </window>
1- 命名空间引用了组件的 XSD。
2- 组合组件连接到实体的 limit 属性。

自定义样式

现在我们使用一些自定义的样式让组件变得更好看一些。

首先,将根组件改为 CssLayout 并为内部组件分配样式名。除了项目中定义的自定义样式(见下面)外,下面这些预定义的样式也会使用: v-component-groupicon-only

  1. <composite xmlns="http://schemas.haulmont.com/cuba/screen/composite.xsd">
  2. <cssLayout id="rootBox" width="100%" stylename="v-component-group stepper-field">
  3. <textField id="valueField"/>
  4. <button id="upBtn"
  5. icon="font-icon:CHEVRON_UP"
  6. stylename="stepper-btn icon-only"/>
  7. <button id="downBtn"
  8. icon="font-icon:CHEVRON_DOWN"
  9. stylename="stepper-btn icon-only"/>
  10. </cssLayout>
  11. </composite>

相应的调整一下组件的类:

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

生成主题扩展(参阅 这里 了解如何在 Studio 中操作)并在 modules/web/themes/hover/com.company.demo/hover-ext.scss 文件添加如下代码:

  1. @mixin com_company_demo-hover-ext {
  2. .stepper-field {
  3. display: flex;
  4. .stepper-btn {
  5. width: $v-unit-size;
  6. min-width: $v-unit-size;
  7. }
  8. }
  9. }

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

stepper final