LSPS documentation logo
LSPS Documentation
Creating a Forms Component

Important: This feature is experimental and its API might change in future releases. To create fully supported forms in this LSPS version, use the ui module of the Standard Library. If you decide to use this feature, you might want to read about the differences between forms and ui.

When creating a custom form component, you will need to create the following:

  • Declaration of your form component as a Record
  • Implementation of your form component
    • in the Expression Language: you will create a form component that will extend an existing form component available in your libraries.
    • in Java as a Vaadin component: you will implement the component as a Vaadin component in your LSPS Application freely and deploy the implementation as part of the LSPS Application. You can implement a custom forms component in Java or in the Expression Language.

In addition to a custom form component, you can also implement a custom Grid renderer: a Grid renderer allows you to define how the data in a cell of a Grid Column is rendered.

Creating a Custom Form Component in the Expression Language

To create a custom form component based on an existing component, use the expression in a custom component definition: Like this, you can, for example, define a custom component that returns a Vertical Layout with a set of components inside.

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

  1. In a module, create a Record which has a subtype of the forms::FormComponent record as its parent: Pick a component that is the closest to what you require, and add the additional fields for new properties of the components.
    exampleFormRecord.png
    Example record of a custom form component
  2. Create a custom form component definition file: right-click your Module and go to New > Custom Form Component Definition.
  3. In the Custom Form Components section of the editor, click Add to create a new form component definition.
  4. In Custom Form Component Details define the component properties:
    • Name: name of the form component
    • Icon path: relative workspace path to the icon that should be used in the palette
    • Properties: properties that will be available for editing in the Properties view of the component
      • 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
  5. In the Expression section enter an expression that returns a custom component.
customComponentasExp.png
A custom component definition and the component record constructor
The component is now available in the palette of form definitions editor. Note that if you want to use it in other modules, make sure to they import your Module.

Creating a Custom Form Component in Java

If an existing form component does not cut it, you can either import an existing Vaadin component or implement your custom Vaadin component from scratch and import your implementation:

  1. Switch to Java perspective.
  2. If required, create the Vaadin implementation in a package in the <YOUR_APP>-vaadin project.
  3. Create a Java class that will connect the LSPS component to its Vaadin implementation:
    • The class must inherit from the FormComponent class or its subclass so you can register it with the component factory.

      You can either inherit directly from the FormComponent or alternatively from the LSPS components, typically prefixed with W: For the forms::TextArea record, the implementation is com.whitestein.lsps.vaadin.forms.WTextArea class. This makes sure your connector class implements all the inherited methods of your component.
    • The class must implement the createWidget() method, which returns the Vaadin implementation.
    • Optionally, the class can implement the getWidget() method, which calls the getWidget() of the extended LSPS class and casts the returned component to the custom Vaadin component.
      customWColorPicker.png
      Example connector LSPS class
      import com.vaadin.shared.ui.colorpicker.Color;
      import com.vaadin.ui.ColorPicker;
      import com.whitestein.lsps.lang.Decimal;
      import com.whitestein.lsps.lang.exception.ValidationException;
      import com.whitestein.lsps.lang.exec.RecordHolder;
      import com.whitestein.lsps.vaadin.forms.FormComponent;
       
      public class WColorPicker extends FormComponent {
       
        @Override
        protected ColorPicker createWidget() {
       
          return new ColorPicker();
        }
       
        @Override
        public ColorPicker getWidget() {
          return (ColorPicker) super.getWidget();
        }
       
        public void setColor(RecordHolder colorHolder) {
          //Decimal since it is mapped to lsps integer (red property in color)
          Decimal red = (Decimal) colorHolder.getProperty("red");
          Decimal green = (Decimal) colorHolder.getProperty("green");
          Decimal blue = (Decimal) colorHolder.getProperty("blue");
       
          Color color = new Color(red.intValue(), green.intValue(), blue.intValue());
          getWidget().setColor(color);
        }
       
        public RecordHolder getColor() {
          Color color = getWidget().getColor();
          RecordHolder colorHolder = getNamespace().createRecord("forms::Color");
          colorHolder.setProperty("red", new Decimal(color.getRed()));
          colorHolder.setProperty("green", new Decimal(color.getGreen()));
          colorHolder.setProperty("blue", new Decimal(color.getBlue()));
          return colorHolder;
        }
      }
      
  4. Once the Vaadin implementation is ready, create the definition of the custom form component so you can use it in forms definitions, do the following:
    1. In a GO-BPMN module, create a Record that will represent your Vaadin component:
      • The supertype of the Record must be the forms::FormComponent record or its child Record: this will typically be the same component as your Vaadin implementation extends. If your Vaadin component extends the com.vaadin.ui.CustomComponent class as in the case of composite components, your record should have forms::FormComponent as its super type.

        It is not recommended to add any additional record fields since the record serves to reflect a Vaadin component in LSPS; Vaadin components are intended for presentation, not for business logic.

        formscustomcomponentdatamodel.png
        Custom component record
      • The Record must implement the methods of its interfaces, and override inherited methods as applicable, and define methods that call their Vaadin implementation: Such methods will use call() to call the method on its Vaadin implementation. When implementing a layout component, that is, components that can hold child components, your record should implement the HasChildren interface.
          ColorPicker {
         
            public void setColor(Color color){
              //calls the setColor(color) method on the Vaadin implementation:
               call("setColor", [color]);
            }
         
            public Color getColor(){
              //calls the getColor(color) method on the Vaadin implementation:
                call("getColor",[]) as Color;
            }
         
            public void refresh() {
              getColor();  // make sure that at least ObjectReference is set as a property
              call(#"refresh", null)
          }
        }
        
    2. To include the component in the palette of the form editor, create a custom component definition in a Custom Form Component definition file in your Module.
      creatingcustomformcomponentfile.png
      Creating custom form component definition file
    3. In the definition file, create a new custom component declaration:
      1. Set the Component Type to the component record.
      2. Define properties that will be edited in the Properties view of your custom component in the Properties area:
        • 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.
          • 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
        • Property displayed in editor: if set to true, the value of the property is displayed in the Form editor (only one property can be displayed in the component graphical depiction).
      3. In the Expression field, define an expression that will return the instance of the record, typically the constructor of the record that takes the defined properties, such as new MyLabel(text). Implement handling of the properties in the Expression.
        definingcustomformcomponent.png
        Custom form component definition with a property
  5. In your LSPS Application, create the LSPS implementation that will connect the LSPS record and its Vaadin implementation and vice versa: Modify the MyFormComponentFactory class to return the LSPS implementation when the Record of your component is requested.
       @Override
      public FormComponent create(Variant.RecordVariant def) {
        final String type = def.getTypeFullName();
        //modified code (returns the WColorPicker for the record ColorPicker):
        if (type.equals("colorpicker::ColorPicker")) {
          return new WColorPicker();
        }
        return super.create(def);
      }
    
  6. Rebuild and deploy your application.

You can now use the custom components in your forms and distribute it to other users as part of a library.

customFormsInFormDefinition.png
Custom component in a form definition

Saving of a Custom Form Component

In the LSPS Application, the user can save a to-do or document with a ui form for later editing. To include data of your custom component when a to-do or a document is saved, you need to include the data on save and recover it when required:

  1. To save presentation data which is not part of the component record, do the following:
    1. In your connector class, in the example, WColorPicker, create constant-property variables for each property values to be saved.
      private static final String STATE_CURRENT_COLOR = "ColorPicker_currentColor";
      
    2. Override writeInternalState() so it saves these properties in the internal state of the component record.
      @Override
      protected void writeInternalState(Map<String, Object> state) {
        super.writeInternalState(state);
        state.put(STATE_CURRENT_COLOR, getWidget().getColor());
      }
      
    3. Override restoreInternalState() so it applies the properties on restore:
      @Override
      protected void restoreInternalState(Map<String, Object> state) {
        super.restoreInternalState(state);
        Color color = (Color) state.get(STATE_CURRENT_COLOR);
        if (color != null) {
          getWidget().setColor(color);
        }
      }
      
  2. If applicable, to save the child components of the custom component, do the following:
    1. Add a list of the components to writeInternalState():
      @Override
      protected void writeInternalState(Map<String, Object> state) {
        super.writeInternalState(state);
       
        writeChildComponents(state);
      }
      protected void writeChildComponents(Map<String, Object> state) {
        state.put(STATE_COMPONENTS, getComponents());
      }
      public ListHolder getComponents() {
        final List<Object> children = new ArrayList<>(getWidget().getComponentCount());
        for (FormComponent child : getChildren()) {
          children.add(form.getDef(child).get());
        }
        return form.getContext().getExecutionContext().getNamespace().createList(children);
      }
    2. Create empty instances of the child components in restoreInternalState() so the saved data has a component which it can populate.
      protected void restoreInternalState(Map<String, Object> state) {
        super.restoreInternalState(state);
       
        restoreChildComponents(state);
       
      }
      protected void restoreChildComponents(Map<String, Object> state) {
        final LspsContextHolder context = form.getContext();
       
        ListHolder children = (ListHolder) state.get(STATE_COMPONENTS);
       
        for (Object child : children) {
          RecordHolder recordHolder = (RecordHolder) child;
          Variant.RecordVariant record = Variant.wrap(recordHolder, context).record();
          form.createComponent(record);
       
          addComponent(recordHolder);
        }
      }

Creating a Custom Grid Renderer

Columns of the Grid component define a renderer which is used to render the data in each row of the Column. While a variety of renderers are available by default, you can define your own renderer if necessary.

To implement your custom Grid renderer, do the following:

  1. In a GO-BPMN module, create a record that will represent your renderer: The supertype of the record must be the forms::Renderer record or its child record.

    customrendererdatamodel.png
    Custom renderer record
  2. Switch to Java perspective to work with the Java part of the implementation:

    1. Create the Vaadin component implementation in <YOURAPP>.vaadin.util.<RENDERER> in the <YOUR_APP>-vaadin project. In the example, we are using the already available ProgressBarRenderer.
    2. Create a Java class that will connect the LSPS renderer to its Vaadin renderer. The class must extend your Vaadin renderer.

    Example renderer class:

    public class WProgressBarRenderer extends ProgressBarRenderer {
     
      private final WGrid grid;
     
      public WProgressBarRenderer(WGrid owner, RecordVariant rendererDef) {
        grid = owner;
        rendererDef.checkSubtypeOf("gridModule::ProgressBarRenderer");
      }
    }
  3. In your Application User Interface, modify the createRenderer() method of MyFormComponentFactory class to return the LSPS implementation when the renderer record is requested:
    public Renderer<?> createRenderer(WGrid owner, Variant.RecordVariant rendererDef) {
      //add the if with the renderer record and the call to return the progress bar via the connecting renderer class:
      if (rendererDef.getTypeFullName().equals("gridModule::ProgressBarRenderer")) {
        return createProgressBarRenderer(owner, rendererDef);
      } else {
        return super.createRenderer(owner, rendererDef);
      }
    }
     
    protected Renderer<?> createProgressBarRenderer(WGrid owner, Variant.RecordVariant rendererDef) {
      return new WProgressBarRenderer(owner, rendererDef);
    }
    
  4. If your renderer passes parameters to its Vaadin counterpart, make sure the data types of the parameters are compatible. You can check the compatibility of data types in Data Type Mapping in the Expression Language documentation. If the data types of the parameters passed from LSPS to the renderer implementation and vice versa are not compatible, do the following:
    1. Implement the converter as a class that implements the Vaadin Converter interface. for your renderer.
      //This is example implementation of a converter of 
      public class DoubleToDecimalConverter implements Converter<Double, Decimal> {
       
        /**
         * serialVersionUID
         */
        private static final long serialVersionUID = 1L;
       
        @Override
        public Decimal convertToModel(Double value, Class<? extends Decimal> targetType, Locale locale) throws com.vaadin.data.util.converter.Converter.ConversionException {
          return value == null ? null : new Decimal(value);
        }
       
        @Override
        public Double convertToPresentation(Decimal value, Class<? extends Double> targetType, Locale locale) throws com.vaadin.data.util.converter.Converter.ConversionException {
          return value == null ? null : new Double(value.toString());
        }
       
        @Override
        public Class<Decimal> getModelType() {
          return Decimal.class;
        }
       
        @Override
        public Class<Double> getPresentationType() {
          return Double.class;
        }
      }
      
    2. Implement the createConverterForRenderer() method in the MyFormComponentFactory() class.
      @Override
      public Converter<?, ?> createConverterForRenderer(WGrid owner, Variant.RecordVariant rendererDef) {
        if (rendererDef.getTypeFullName().equals("gridModule::ProgressBarRenderer")) {
          //return new StringToDecimalConverter();
          return new DoubleToDecimalConverter();
        } else {
          return super.createConverterForRenderer(owner, rendererDef);
        }
      }
      
  5. Rebuild and deploy your application as required.

You can now use the custom renderer in your grid columns. Consider distributing the renderer as part of a Library.

customGridColumnRenderer.png