LSPS documentation logo
LSPS Documentation
Creating a UI Component

Important: This section deals with implementing custom form components for the ui module forms. Information on how to implement a custom forms component is available here.

You can implement a custom ui form component either in Java or in the Expression Language: When implementing in the Expression Language, both the implementation and declaration of the component are stored in a custom definition file in a GO-BPMN Module. If you want to use a custom Vaadin implementation, you will need to first implement the component in Java and then create its declaration in a GO-BPMN Module.

Before you implement a custom UI component, make sure to get familiar with execution levels, since ui forms are automatically created on the screen level and might use the View Model component to operate on additional screen levels to isolate transient form data.

UI Component in Java

To define and declare a custom form component, do the following:

  1. Create the custom component class in your application (in the default application, it is recommended to implement custom form components in a dedicated package <YOUR_APP_PACKAGE>.vaadin.<MY_PACKAGE> in the <YOUR_APP>-vaadin project). The implementing class must meet the following:
    • It implements the com.whitestein.lsps.vaadin.ui.components.UIComponent interface.

      Important: It is not recommended to extend the form components of the Standard Library since their methods might change.

    • It defines a constructor with UIComponentData input parameter:

      The constructor should set the UIComponentData values. Component data are a wrapper of the component record that also holds values of the record fields.

    • It implements the getComponentData() method, which returns the component data.
    • It implements the getWidget() method, which returns the Vaadin component to be rendered on the client. This method is useful if you want to use multiple Vaadin components to render a single LSPS custom component. Generally, you want the method to return this.
    • It implements the refresh() method.

      An example Label Component implementation

      import com.vaadin.ui.AbstractComponent;
      import com.vaadin.ui.Label;
      import com.whitestein.lsps.vaadin.ui.UIComponentData;
      import com.whitestein.lsps.vaadin.ui.components.UIComponent;
      import com.whitestein.lsps.vaadin.util.Variant;
       
      public class MyLabel extends Label implements UIComponent {
       
        private final UIComponentData uic;
       
        public MyLabel(UIComponentData uic) {
          this.uic = uic;
        }
       
        @Override
        public UIComponentData getComponentData() {
          return uic;
        }
       
        @Override
          public void refresh() {
          String suffixText = getProperty("suffix");
          String content = getProperty("content");
          suffixText = getLocalizedString(suffixText);
          content = getLocalizedString(content);
          setValue(content + " " + suffixText);
        }
       
        private String getLocalizedString(String string) {
          return uic.getScreen()
              .getContextHolder()
              .getAppConnector()
              .getLocalizer()
              .getLocalizedString(string, this);
        }
       
        private String getProperty(String propertyName) {
          return Variant //variant allows to set the scope of closure and handle any null values;
              .definitionOf(this) //get the record of MyLabel (wrapped in Variant)
              .getPropertyValue(propertyName).closure() //get text property value and cast to closure
              .inScope(this)//set the scope of the closure
              .call().string().valueOrNull(); //execute; cast result to string, and get value (unwrap the variant to get the value)
        }
       
        @Override
        public AbstractComponent getWidget() {
          return this;
        }
      }
  2. If you need additional Vaadin components, add the respective Vaadin add-on to the generated application:
    1. Create a GWT XML in <YOUR_APP>-vaadin-war/src/main/resources/com/whitestein/lsps/vaadin/webapp directory
    2. Enable automatic compilation of the your widget sets: open the <YOUR_APP>-vaadin-war/pom.xml file and configure the maven Vaadin plugin.
    3. In the pom file, uncomment the vaadin-client-compiler dependency.
    4. Open the LspsUI Java file and modify the @Widgetset annotation, to reference your widget set, for example @Widgetset("com.whitestein.lsps.vaadin.webapp.MyWidgetSet")
    5. Open the <YOUR_APP>-vaadin-war/pom.xml and add maven dependency to the Vaadin component jar file.
  3. In the Modeling perspective, reflect the component implementation as records:
    1. Create the component record which extends the UIComponent record which implements the LSPS reflection of the Vaadin component: In the example, we extended Vaadin's Label, which is implemented by the UIOutputText and this is reflected as the ui::OutputText: Hence we imported the OutputText record since this reflects the Vaadin Label and created the subrecord UICustomLabel that will reflect our MyLabel class.
    2. Add any additional fields which the user needs to populate for your component (suffix defined as a closure in our example). Make sure these are handled in your implementation properly.
      customcomponentdatamodel.png
      Custom component records
    3. Create a custom component definition in a custom component definition file.

      Set the Implementation property of the custom component definition to Data Type and enter the name of the component record.

    4. In the Properties area, define the component properties that will be available for editing in the Properties view:
      • Property Name: name of the underlying record field
      • Display Name: name displayed in the Properties view
      • Type: data type of the property (needed if the Implementation is defined as an expression)
      • Edit Style: child component edit style
        • EXPRESSION: Property is edited as an expression in the component.
        • DYNAMIC_EXPRESSION: Property is edited as an expression in the component and automatically wrapped as a non-parametric closure (the parameter is processed as { -> <parameter_value> }).
        • COMPONENT: Property is handled as a child component.
        • COMPONENT_LIST: Property can be inserted multiple times as a child component.
      • Mandatory: whether the property value must be specified
      • Displayed in Editor: if set to true, the value of the property is displayed in the Form editor

        Note: If the custom component extends a non-abstract UIComponent, it is rendered as its UIComponent parent and the Displayed in Editor setting is ignored.

        customComponentDefinition.png
        A custom component definition with no additional fields
  4. Connect the record with its implementation: in the LspsUIComponentFactory class, uncomment the createComponent() method and modify it to return your component when the respective record is requested.
    public class LspsUIComponentFactory extends UIComponentFactoryImpl {
     
      public LspsUIComponentFactory(LspsAppConnector connector) {
        super(connector);
      }
     
      @Override
      protected UIComponent createComponent(UIComponentData componentData) {
        final String type = componentData.getDefinition().getTypeFullName();
        if (type.equals("customUIComponent::UICustomLabel")) {
          return new CustomUiComponent(componentData);
        }
        return super.createComponent(componentData);
      }
    }
  5. Build and deploy your application.

You can now use the custom component in your ui definition. Consider distributing the components as part of a Library.

uicompindefinition.png
Custom component in form definition (inherited properties are in the Detail tab)

UI Component in the Expression Language

To create a custom component implemented in the Expression Language, do the following:

  1. In your module, create a record which extends a ui::UIComponent record. Add any additional fields which the user needs to populate when they will use the component.
  2. Create a custom component definition in a custom component definition file.
  3. In the Properties area, define the component properties that will be available for editing in the Properties view:
    • Property Name: name of the underlying record field
    • Display Name: name displayed in the Properties view
    • Type: data type of the property (needed if the Implementation is defined as an expression)
    • Edit Style: child component edit style
      • EXPRESSION: Property is edited as an expression in the component.
      • DYNAMIC_EXPRESSION: Property is edited as an expression in the component and automatically wrapped as a non-parametric closure (the parameter is processed as { -> <parameter_value> }).
      • COMPONENT: Property is handled as a child component.
      • COMPONENT_LIST: Property can be inserted multiple times as a child component.
    • Mandatory: whether the property value must be specified
    • Displayed in Editor: if set to true, the defined value of the property is displayed in the graphical depiction of the component in the Form editor

      Note that only one property can be displayed in the component graphical depiction.

      If the custom component extends a non-abstract UIComponent, it is rendered as its UIComponent parent and the Displayed in Editor setting is ignored.

      customComponentasExp.png
      A custom component with a parameter
  4. Set the Implementation property of the custom component definition to Expression and enter an expression that returns a custom component.
    //create checkbox:
    def ui::CheckBox cb := new ui::CheckBox();
    def Boolean checked;
    //bind checkbox to variable checked:
    cb.binding := &checked;
    //activate immediate mode for checkbox:
    cb.triggerProcessingOnChange := true;
    //handle value change of checkbos: refresh the checkbox list to have all boxes unchecked:
    cb.listeners := { new ValueChangeListener(
      refresh -> { a -> {checkBoxList}},
      handle -> {a -> if not checked then *(checkBoxList.binding) := {}; end; }
      )
    };
    //return checkbox:
    cb;

UI Component with a Custom Event

To create a custom event for your custom form component, you need to do the following:

  1. In the <YOUR_APP>-vaadin project, create the class of the event:
    • It must implement the UIEvent.
    • The constructor must have as its second parameter the relevant data type.

      The parameter can be based on the record related to the component record.

  2. In the event class, override the getEventProperties() method so it returns a hashmap of the custom event properties.
    package com.whitestein.colorpicker.vaadin.util;
     
    import java.util.HashMap;
    import java.util.Map;
     
    import com.vaadin.shared.ui.colorpicker.Color;
    import com.whitestein.lsps.lang.Decimal;
    import com.whitestein.lsps.lang.exec.RecordHolder;
    import com.whitestein.lsps.vaadin.ui.components.UIComponent;
    import com.whitestein.lsps.vaadin.ui.events.UIEvent;
     
    public class UIColorPickEvent extends UIEvent {
     
      private final Color newColor;
     
      public UIColorPickEvent(UIComponent component, Color newColor) {
        super(component, colorpicker::ColorPickEvent);
        this.newColor = newColor;
      }
     
      private static RecordHolder toColor(UIComponent context, Color color) {
        final RecordHolder c =
          context.getComponentData().getScreen().getScreenContext().getNamespace()
            .createRecord("colorpicker::Color");
        c.setProperty("r", new Decimal(color.getRed()));
        c.setProperty("g", new Decimal(color.getGreen()));
        c.setProperty("b", new Decimal(color.getBlue()));
        c.setProperty("a", new Decimal(color.getAlpha()));
        return c;
      }
     
      @Override
      //creates java hashmap -> fieldname to value; then creates the uicolorpickevent recordholder;
      protected Map<String, ?> getEventProperties(UIComponent component) {
        final Map<String, Object> result =
           new HashMap<String, Object>(super.getEventProperties(component));
        result.put("color", toColor(component, newColor));
        return result;
      }
    }
  3. Implement your custom component: Make sure the UIComponentData is defined as a class variable, so you can use it in the getComponentData method.

    1. Create the custom listener.

    The listener should override the method that creates the event on the component, in the example colorChanged(ColorChangeEvent e) so the event is transformed into a custom event. It enters the event queue of the event-processing cycle when UIComponents.fireAndProcess() method is called.

    1. Register the component in the LspsAppComponentFactory class: uncomment the createComponent method and add the constructor call for your component that is called when the respective Record is requested.
      package org.eko.ekoapp.vaadin.util;
       
      import org.eko.ekoapp.vaadin.components.UIText;
       
      import com.whitestein.lsps.vaadin.LspsAppConnector;
      import com.whitestein.lsps.vaadin.ui.UIComponentData;
      import com.whitestein.lsps.vaadin.ui.UIComponentFactoryImpl;
      import com.whitestein.lsps.vaadin.ui.components.UIComponent;
       
      public class MyComponentFactory extends UIComponentFactoryImpl {
       
          public MyComponentFactory(LspsAppConnector connector)
                  throws NullPointerException {
              super(connector);
          }
       
          @Override
          protected UIComponent createComponent(UIComponentData componentData) {
              String type = componentData.getComponentDefinition().getType()
                      .getFullName();
              if (type.equals("customComponentModule::TextComponentRecord")) {
                  return new UIText(componentData);
              }
              return super.createComponent(componentData);
          }
      }
    2. Create listeners and context for the component (UIComponents.afterCreate(this)).
      public UIColorPicker(UIComponentData data) {
        this.data = data;
        ColorChangeListener listener = new ColorChangeListener() {
       
          @Override
          public void colorChanged(ColorChangeEvent event) {
            final Color newColor = event.getColor();
            UIComponents.fireAndProcess(new UIColorPickEvent(UIColorPicker.this, newColor));
          }
        };
        //registered to vaadin's color picker
        addColorChangeListener(listener);
        UIComponents.afterCreate(this);
      
    3. Define the refresh() method for the component.
      @Override
      public void refresh() {
        Variant.RecordVariant color = Variant.definitionOf(this).getPropertyValue("color").closure()
            .inScope(this).call().record();
        color.checkType("colorpicker::Color").checkPresent();
        setColor(toColor(color));
      }
       
      private static Color toColor(Variant.RecordVariant color) {
        return new Color(color.getPropertyValue("r").decimal().get().intValue(),
            color.getPropertyValue("g").decimal().get().intValue(),
            color.getPropertyValue("b").decimal().get().intValue(),
            color.getPropertyValue("a").decimal().or(new Decimal(255)).intValue());
        }
    4. Implement the getComponentData() method.
      package com.whitestein.colorpicker.vaadin.util;
       
      import com.vaadin.shared.ui.colorpicker.Color;
      import com.vaadin.ui.ColorPicker;
      import com.vaadin.ui.components.colorpicker.ColorChangeEvent;
      import com.vaadin.ui.components.colorpicker.ColorChangeListener;
      import com.whitestein.lsps.lang.Decimal;
      import com.whitestein.lsps.vaadin.ui.UIComponentData;
      import com.whitestein.lsps.vaadin.ui.components.UIComponent;
      import com.whitestein.lsps.vaadin.ui.events.UIEvent;
      import com.whitestein.lsps.vaadin.util.UIComponents;
      import com.whitestein.lsps.vaadin.util.Variant;
       
      public class UIColorPicker extends ColorPicker implements UIComponent {
       
        private final UIComponentData data;
       
        public UIColorPicker(UIComponentData data) {
          this.data = data;
          ColorChangeListener listener = new ColorChangeListener() {
       
            @Override
            public void colorChanged(ColorChangeEvent event) {
              final Color newColor = event.getColor();
              UIComponents.fireAndProcess(new UIColorPickEvent(UIColorPicker.this, newColor));
            }
          };
          //registered to vaadin's color picker
          addColorChangeListener(listener);
          UIComponents.afterCreate(this);
        }
       
        @Override
        public void refresh() {
          Variant.RecordVariant color = Variant.definitionOf(this).getPropertyValue("color").closure()
              .inScope(this).call().record();
          color.checkType("colorpicker::Color").checkPresent();
          setColor(toColor(color));
        }
       
        private static Color toColor(Variant.RecordVariant color) {
          return new Color(color.getPropertyValue("r").decimal().get().intValue(),
              color.getPropertyValue("g").decimal().get().intValue(),
              color.getPropertyValue("b").decimal().get().intValue(),
              color.getPropertyValue("a").decimal().or(new Decimal(255)).intValue());
        }
       
        @Override
        public UIComponentData getComponentData() {
          return data;
        }
      }
  4. Create the records for your event.
  5. Declare your component in PDS:
    1. Create a data type model that reflects the components, events, and any related data types defined in your application (in the example, the color picker, color, and color-change listener). Note that the data types must have the correct super types:
      • A Listener type must have ui::Listener or its subtype as its super type.
      • A Component type must have ui::UIComponent or its subtype as its super type.
      • An Event type must have ui::Event or its subtype as its super type. It contains the field source that holds the component that produced the event and a field with the data the implementation requires.
    2. Create the custom component definition: Use the component record as its implementation.
    3. When using the component, define the listener as an expression.
      new colorpicker::ColorPickListener(
        refresh -> {a->{PICKER}},
        handle -> {e:ColorPickEvent-> color:=e.color; debugLog({->"Color was picked. " + e})}
      )