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 implemented in the Expression Language, both the implementation and declaration of the component are stored in a custom definition file in a GO-BPMN Module.
  • When implementing in Java, you will:
    • create the component in Java and deploy it as part of your LSPS Application
    • 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 with the aim to isolate the data in the form so that a form can be discarded without leaving the data in an inconsistent state.

UI Component in Java

This section primarily describes how to implement a custom UI component in Vaadin 8 component.

Important: UI Vaadin 8 implementation is a new forms implementation and is not compatible with the previously used UI Vaadin 7 implementation. A list of differences between the implementations is available in the here. To keep using the UI Vaadin 7 implementation, set UIComponentFactoryV7Impl as the UIComponentFactory. Where applicable, an admonition with instructions on how to implement UI Vaadin 7 when you are using UIComponentFactoryV7Impl.

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

  1. If you require Vaadin components which are not available out-of-the-box, 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 the maven dependency to the Vaadin component jar file.
  2. Create the custom component class in your application in a dedicated package in the <YOUR_APP>-vaadin project):

    The implementing class must meet the following requirements:

    • It implements com.whitestein.lsps.vaadin.ui.components.UIComponent.
    • It extends a class that implements the com.vaadin.ui.<Component> class. Do not extend the form components of the Standard Library since their methods might change.

      For UI Vaadin 7, import and extend the v7 versions of the components, for example, use com.vaadin.v7.ui.Label instead of com.vaadin.ui.Label.

    • It defines a constructor with the input parameter UIComponentData, a wrapper of the component record: the constructor sets the UIComponentData values.
    • 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. If you want to return multiple Vaadin components as a single LSPS custom component, return it from this method.
    • It implements the refresh() method.

      An example Label Component implementation in UI Vaadin 8

      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;
        }
      }
  3. In the Modeling perspective, create the component record:
    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 and created the subrecord UICustomLabel that will reflect our MyLabel class.

      Important: Make sure your module imports the ui module of the Standard Library.

    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
  4. In a custom component definition file declare the component:
    1. Set the Implementation property of the custom component definition to Data Type and enter the name of the component record.
    2. In the Properties area, define the component properties that will be available for editing in the Properties view (in the case of the example Label, it is only the suffix property). For each property define the following:
      • 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
  5. Resolve the record of your form component to the implementation in your LSPS Application:
    1. In the connectors package of the vaadin project, create a factory class that extends UIComponentFactoryV8Impl.

      For UI Vaadin 7, extend UIComponentFactoryV7Impl

    2. Override the createComponent() method so it returns your component when the record from your form definition is requested.
      public class MyUIComponentFactoryV8Impl extends UIComponentFactoryV8Impl {
       
        public MyUIComponentFactoryV8Impl(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);
        }
      }
    3. In the DefaultLspsAppConnector class in the core package of the vaadin project, return your factory in getComponentFactory().
      public class DefaultLspsAppConnector extends LspsAppConnectorImpl {
       
        private static final long serialVersionUID = 6600639435905976895L;
       
        public DefaultLspsAppConnector(LspsUI ui) {
          super(ui);
        }
       
        @Override
        public UIComponentFactory getComponentFactory() {
          //returns the custom ui factory:
          return new MyUIComponentFactoryV8Impl(this);
        }
      
  6. Build and deploy your application.

You can now use the custom component in your ui definition.

Don't forget to upload the module with your ui component record and declaration along with the LSPS Application. 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:
    • Make it extend UIEvent.
    • Pass the data type as the second parameter of the event constructor.

      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. UIComponentData includes such data about the component as its parent component, its depth from the root component, etc.
    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.

    2. 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);
          }
      }
    3. 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);
      
    4. 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());
        }
    5. 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})}
      )