Free component

With the help of a 'Free component', individual components for the form renderer can be developed if the Lobster Data Platform / Orchestration modules are no longer sufficient, or if custom creative components are implemented in a form.
The path of such a component should only be taken if no other onboard solution can be found, or if third-party components are integrated.

►CAUTION◄ Programming skills and experience with HTML5 (HTML/CSS/Javascript) are the basic requirements!

Introduction

Free components, if licensed, can be developed in two ways:

  1. Directly via the 'Free component' in the 'Portal' section of the editor. Then via the Edit button in the corresponding properties section.

    images/download/attachments/167859116/image2023-1-23_13-23-55-version-1-modificationdate-1705571863000-api-v2.png

    images/download/attachments/167859116/image2023-1-23_13-24-42-version-1-modificationdate-1705571863002-api-v2.png

    Advantage: Direct development of the component in the manner of 'what you see is what you get'.
    Disadvantage: No possibility to reuse the developed component.
    Disadvantage: No 'single point of maintenance'. If a component is only copied from form-to-form, errors and extensions cannot be handled all together at a central point.
    Disadvantage: The component is compiled per form instance and cannot be cached globally.

  2. Persistent creation of components via the 'Free component' overview (rights must be available accordingly).

    images/download/attachments/167859116/image2023-1-23_13-31-18-version-1-modificationdate-1705571863004-api-v2.png

    images/download/attachments/167859116/image2023-1-23_13-32-42-version-1-modificationdate-1705571863006-api-v2.png

    Advantage: The components can be reused. The maintenance and further development of components happens from one point.
    Advantage: Components appear directly in the 'Free component' section in the form editor and can be dragged onto the form.
    Advantage: The component code can be cached globally. The compilation step does not have to be performed again for each form instance.
    Disadvantage: Changes to a component are not directly visible. Recommendation: open a portal editor and load the latest changes by switching between test and edit mode.

The editor for the configuration differs for both types only in minor details and is therefore described here only once.

Structure of a 'Free component'

A free component basically consists of three parts:

  1. View (HTML)
    Defines the basic layout of the component with HTML code.

  2. Style (CSS)
    Defines the look and feel of the basic layout with CSS code.
    This CSS code can also be loaded from a file via the Settings tab (load stylecode from URL). In this case, proprietary keywords (like :host) are also interpreted before including the file content.

  3. Controller (Javascript)
    Implements the program logic and behaviour of the component with Javascript and forms the interface between the component and the form renderer.
    ►NOTE◄ Since Lobster_pro does no longer support older browsers (s.a. IE11), all Javascript code can be written in ES6 syntax.

Therefore, the editor is divided into these three parts.

Editor for 'Free component'

Display in the shape editor

images/download/attachments/167859116/image2023-1-23_13-15-28-version-1-modificationdate-1705571862995-api-v2.png

images/download/attachments/167859116/image2023-1-23_13-16-29-version-1-modificationdate-1705571862997-api-v2.png

Tip: Additional settings can be made and examples loaded via the Settings tab.
In the Resources tab, external resources (mainly localization entries) can be defined, which will be exported in case of Exports of the component. (For details see the Lokalisierung section).

►NOTE◄ This chapter does not go into detail about technical terms relating to the parts mentioned, as a strong knowledge of web applications is assumed. Likewise, comments on the code are given exclusively in English.

Controller

The implementation of the component logic is done in the controller part of the editor with Javascript. As an agreement, the controller class must be returned to Lobster Data Platform / Orchestration (return Classname). This also allows the use of custom support classes within the controller code.
The controller is based on the following interface, whereby implementation of the individual methods is optional.

Controller Interface: IFreeComponent
interface IFreeComponent {
/**
* Will be called on initialization and once the readonly flag of the element has changed
*
* @param {boolean} readonly True if readonly has changed to true, false if it has changed to false
*/
readOnlyChanged( readonly: boolean ): void;
 
/**
* Will be called on initialization and once the enabled flag of the element has changed
*
* @param {boolean} disabled True if the component has been disabled, false if it has been enabled
*/
disabledChanged( disabled: boolean ): void;
 
/**
* Will be called when the element gets initialized, even before the view part of the component is ready ( see onViewInit() ).
* Therefore view based context properties like hostElement are not initialized, yet.
* In case the component has script dependencies defined, initialize() will be called once all dependencies are loaded without an error.
 * This method might return a PromiseLike object to do some asynchronous initialization as well (e.g. retrieving data from a web service etc.)
*
* @param {FreeComponentContext} context The predeclared view context
* @returns A PromiseLike object or void
*/
initialize( context: FreeComponentContext ): PromiseLike<void>;
 
/**
* Will be called once the entire component (controller) is destroyed.
* The controller is NOT destroyed in case of invisibility!
*/
onDestroy(): void;
 
/**
* Will be called once the element loads data from its data field.
* @param {Object} value The populated value
* @param {boolean} isNew Determines whether the load was due to initialization (in case of true, a default value may be loaded if value is null or undefined)
* @returns true if the value of the element has actually changed, false else
*/
populateValue( value: Object, isNew: boolean ): boolean;
 
/**
* Will be called once the internal value of the element will be changed.
* This is independend of the elements data field and may occur due to user changes or behaviour actions.
* The changed value is not necessarily the populated value and is to be synchronized once retreiveValue() is called
*
* @param {Object} value The changed value
* @returns True if the value of the element has actually changed, false else
*/
changeValue( value: Object ): boolean;
 
/**
* Retrieves the value of this element
*
* @returns The value of this element
*/
retrieveValue(): Object;
 
/**
* Retrieves a dummy value of this element. Needed for the data structure export in the form editor
*
* @returns Any dummy value which is of the same type as the elements value
*/
retrieveDummyValue(): Object;
 
/**
* Returns a list of event names supported by this component.
* Use the context (given to onViewInit()) to dispatch events. (e.g. this.context.dispatchEvent('MY_EVENTNAME', "event value") )
* It is recommended to prefix the event names, if it is not a default event like 'CHANGED', to prevent collisions between event names.
* Warning: Do not dipsatch CHANGED events on retrieveValue(), changeValue() or populateValue()!
* Important: To notify the form renderer about value changes triggered by the view of this component, call this.context.notifyValueChange( value )!
*/
getEventNames(): string[];
 
/**
* Returns a list of action names which are handled by this component.
* The handling of actions is implemented in the handleAction() method.
*
* @see handleAction()
*/
getActionNames(): string[];
 
 
/**
* Returns a list of behaviour names which are handled by this component.
* The handling of behaviours is implemented in the handleBehaviour() method.
*
* @see handleBehaviour()
*/
getBehaviourNames(): string[];
 
   /**
* Will be called once the component needs to handle a specific action.
* @see getActionNames()
*
* @param {FreeComponentAction} a The action which should be handled by this component (action name: a.name, action value: a.value)
*/
handleAction( a: FreeComponentAction ): void;
 
/**
* Will be called once the component needs to handle a specific behaviour.
* @see getActionNames()
*
* @param {FreeComponentBehaviour} a The behaviour which should be handled by this component (behaviour name: b.name, behaviour value: b.value)
*/
handleBehaviour( b: FreeComponentBehaviour ): Promise< BehaviourResult >;
 
   /**
* Will be called once the view part of this component has been inserted into the DOM and is ready for use.
* In case the component has style dependencies defined, onViewInit() will be called once all style sheets are loaded without an error.
 *
* @param {FreeComponentContext} context The view context of this component. It offers angular support and access to the host DOM element
*/
onViewInit( context: FreeComponentContext ): void;
 
/**
* Will be called once the view part of this component is destroyed and removed from the DOM.
* This would also apply if the component is not rendered by the form renderer (e.g. invisible, not displayed because in closed expandable or tab etc.)
* Should disconnect view listeners and release resources.
* onViewInit() will be called once the view has been created again.
*/
onViewDestroy(): void;
 
   /**
* Sets/Removes the focus from the component
*
* @param focus True if the focus should be set, false if it should be removed
*/
setFocus( focus: boolean ): void;
 
/**
* Will be called once the hint of the element has changed
*
* @param hint The hint text (usually displayed underneith the component as visual text)
*/
hintChanged( hint: string ): void;
 
/**
* May be used to override the html part of the component.
* Implementing this is not the common case
*/
getViewHTML(): string;
}
BehaviourResult
interface BehaviourResult {
/**
* The result to indicate if the true or false actions should be executed
*/
result: boolean;
/**
* The value, which should be passed to the actions
*/
value: any;
}

Initialization

If script dependencies (Dependencies tab) have been defined, the controller class will not be compiled until all these dependencies have been loaded without errors (see also Importieren von externen Bibliotheken und Modulen).
The first initialization step "initialize( context )" of the component is called, even if the component itself is not visible. Here additional data can be obtained and preparatory steps for the part that is detached from the view can be performed. The method can optionally return a Promise, which delays the initialization of the component (and thus the form) until it is resolved.

As soon as the component is drawn visibly, the renderer calls the method "onViewInit( context )" and passes the view context (type: FreeComponentContext), the structure of which is documented below.

FreeComponentContext
interface FreeComponentContext {
/**
* The element which appends the free component view as inner html, available after onViewInit was called
   */
hostElement: HTMLElement;
 
/**
* Indicates whether the element is rendered in form editing mode
*/
isEditorMode: boolean;
 
/**
* The generated view id to resolve the binding between hostElement node and controller instance, available after onViewInit was called
   */
viewId: string;
 
/**
* Indicates whether the component view is destroyed.
* If this is set to true, the view context is invalidated and must not be used anymore
*/
isDestroyed: boolean = false;
 
/**
* The reference to the style code of the component in the DOM, available after onViewInit was called
   */
internalStyleRef: string;
 
/**
* Queries the host element for a specific element that matches the query.
* Using this method ensures that only an element within the component view is selected.
*
* @param query A DOM element query (e.g. ".container ~ span")
* @see elements(query)
*/
public element( query: string ): HTMLElement;
 
/**
* Queries the host element for specific elements that match the query
* Using this method ensures that only elements within the component view are selected.
*
* @param query A DOM element query (e.g. "div > .headline")
* @see element(query)
*/
public elements( query: string ): HTMLElement[];
 
/**
* Dispatches any event defined by its name and value
*
* @param eventName The name of the event
* @param value The value passed by the event
* @param changeKind (optional) Defines the kind of interaction which triggered the event (default is "MANUAL_CHANGED")
*/
dispatchEvent( eventName: string, value: any, changeKind = CHANGE_KINDS.MANUAL_CHANGED );
 
/**
* Notifies the form renderer that the value of the component has changed through the view part.
* Hint: Do not call notifyValueChange() in changeValue(), populateValue() or retrieveValue()!
*
 * @param value The changed value
* @param changeKind (optional) Defines the kind interaction which triggered the event (default is "MANUAL_CHANGED")
*/
notifyValueChange( value: any, changeKind = CHANGE_KINDS.MANUAL_CHANGED );
 
/**
* Returns a localized value from the registered template resources
*
* @param resourceIdentifier The identifier of the resource registered in the template resources
* @param defaultValue An optional default value which will be returned if the resource was not found
* @param args (optional) Arguments to replace placeholders in the resource string ({0}, {1}, ...)
* @param localeCode (optional) The locale code (defaults to the current session locale)
*/
public rs( resourceIdentifier: string, defaultValue?: string, args?: any[], localeCode?: string ): string;
 
/**
* Can be used to bootstrap an isolated angular context.
* This is only recommended if the component is not instanciated a lot per form e.g. in a "repeatable element" container
*
* #### Example
* ```javascript
* context.requestAngularPlatformRef().bootstrapModule( CustomAngularModule );
* ```
*/
public requestAngularPlatformRef(): PlatformRef;
 
/**
* Marks the host view of the component for checks.
* This is usually not necessary.
*/
public markForCheck(): void;
}

Data and values of a 'Free component'/Changed trigger

If the component is to write its data into the defined data field and be read via element links, the data interface must first be implemented.
The "populateValue()", "changeValue()" and "retrieveValue()" methods are used to read and write form data related to the component's data field. Purely visual components that do not have a value do not need to implement these methods.

Method

Explanation

populateValue( value, isNew )

Called when the value of the component is set via the element data (data field).
isNew is true when the data is loaded into the component for the first time (this may be used to set a default value, if value is falsy).
The method returns true or false to indicate whether the value of the component has changed (true). Only if true is returned here, the "Changed" event will be triggered.

changeValue( value )

Called when the value of the component is to be set independently of the data field.
e.g. by the action 'Set value'.
The method returns true or false to indicate whether the value of the component has changed.

retrieveValue( )

Returns the value of the component.

retrieveDummyValue( )

Supplies dummy data e.g. for the structure export function if the editor is not in test mode.

►IMPORTANT◄ The above methods for setting the value are called exclusively by the form renderer.
An internal change of the value (e.g. by the input of a text by the user) is signaled exclusively by the "notifyValueChange()" method of the view context (FreeComponentContext).

In order to be able to react to the "Changed" behaviour trigger as a configurator, the "CHANGED" event must be explicitly offered by the component (see following chapter).

Events, behaviour and actions

Apart from reading and writing the component value, communication between other elements and the 'Free component' takes place exclusively via events, behaviour and actions.
Which events the component offers is defined in the method getEventNames( ). In the localization, these can also be defined by the bundle "en.lobster.fre.model.EventTypeEnum".

All events offered are consequently also available in the behaviour configuration in the form editor.

Example:

Controller

Behaviour configuration

getEventNames() {
return [ 'MY_CUSTOM_EVENT1', 'MY_CUSTOM_EVENT2' ];
}

images/download/attachments/167859116/image2023-1-23_13-34-40-version-1-modificationdate-1705571863009-api-v2.png

To trigger an event, the "dispatchEvent( )" method of the view context ( see onViewInit( context ) ) can be called.

Example:

onViewInit( context ) {
this.context = context;
}
 
someViewEventHandler() {
if( !this.context )
return;
 
this.context.dispatchEvent( 'MY_CUSTOM_EVENT1', /* some event data */ 12345 );
}

►NOTE◄ It is recommended to prefix proprietary events so that they do not collide with native event names.

The following native events, which are not automatically offered, are defined so far:

Event name

Description

Must be called by the controller?

CHANGED

The value of the component has changed.
►NOTE◄ This event is dispatched when context.notifyValueChange() is called, which should only be done when the view part of the component makes changes!

Automatically triggered via context.notifyValueChange().

FOCUS_OUT

The component has lost focus

Yes

CHANGED_AND_FOCUS_OUT

The component has lost focus and value since the focus was last changed.
►NOTE◄ Automatically detected by the program when FOCUS_OUT is triggered.

No

CLICKED

The component or one of its sub-parts was clicked on.

Yes

F[1-12]_PRESSED

The corresponding F key was pressed.

Yes

ENTER_PRESSED

The enter key was pressed

Yes

In summary, events control the output path of component communication.

Behaviours are another way to read data from the component. Similar to the events, the controller method "getBehaviourNames()" specifies which behaviours are handled by the component.
The behaviour action Free component behaviour (only compatible with 'Free components') can then be used to execute the desired behaviour of the component.
The data of the behaviour is available in the controller's behaviour handler.

Example:

Controller

Behaviour configuration

getBehaviourNames() {
return [ 'MY_BEHAVIOUR1', 'MY_BEHAVIOUR2' ];
}

images/download/attachments/167859116/image2023-1-23_13-36-28-version-1-modificationdate-1705571863011-api-v2.png

Tip: The listed behaviours can be customized in localization via the bundle 'freeComponentBehaviours'.


As soon as the Free component behaviour is executed, the "handleBehaviour( )" method of the controller is called and the behaviour is passed accordingly.

handleBehaviour( b ) {
switch( a.name ) {
case 'MY_BEHAVIOUR1':
// handle action
console.log( "MY_BEHAVIOUR1", a.value );
return Promise.resolve( { result: true, value: "My Behaviour 1" } );
break;
 
case 'MY_BEHAVIOUR2':
// handle action
return Promise.resolve( { result: false, value: "My Behaviour 2" } );
  break;
}
}

The data passed is of type 'FreeComponentBehaviour', the structure of which is defined as follows:

FreeComponentBehaviour
interface FreeComponentBehaviour {
/**
* The name of the behaviour
*/
name: string;
 
/**
* The behaviour input value
*/
value: any;
}


Actions on the other hand side represent the counterpart of the events and behaviours, i.e. the input path of the component. Similar to the events, the controller method "getActionNames()" determines which actions are handled by the component.
With the behaviour action Free component action (only compatible with 'Free components') the desired action can then be triggered at the component.
The data of the behaviour is available in the action handler of the controller.

Example:

Controller

Action configuration

getActionNames() {
return [ 'MY_ACTION1', 'MY_ACTION2' ];
}

images/download/attachments/167859116/image2023-1-23_13-38-37-version-1-modificationdate-1705571863013-api-v2.png

Tip: The listed actions can be customized in localization via the bundle freeComponentActions.

As soon as the 'Free component action' is executed, the "handleAction( )" method of the controller is called and the action is passed accordingly.

handleAction( a ) {
switch( a.name ) {
case 'MY_ACTION1':
// handle action
console.log( "MY_ACTION1", a.value );
break;
 
case 'MY_ACTION2':
// handle action
break;
}
}

The action passed is of the 'FreeComponentAction' type, whose structure is defined as follows:

FreeComponentAction
interface FreeComponentAction {
/**
* The name of the action
*/
name: string;
 
/**
* The action value (usually the value passed by the behaviour)
*/
value: any;
/**
* The kind of interaction which triggered the action (e.g. 'MANUAL_CHANGED' if the user triggered the behaviour)
*/
changeKind: string;
}

Clean up/destroy the component

If the component is no longer displayed (e.g. when collapsing an expandable container), the controller method "onViewDestroy( )" is called. Here all element event handlers and observers should be separated.
The view context was then also destroyed and should no longer be used. Tip: As soon as the component is displayed again, "onViewInit()" is called again.

If the entire component is no longer needed (e.g. when deleting a repeating element), "onViewDestroy( )" is called first and then "onDestroy( )". All 'cleaning up' steps should then be performed here.

View (HTML)

The view part of the component is defined with HTML code. This code is loaded from the host element as inner HTML. All rules specified by the HTML default and browser specific peculiarities apply here.
The only special case here is accessing the controller directly from the markup. For this purpose, the keyword "$controller" was introduced, which refers to the controller instance in the Javascript parts of the markup.

Example:

Example code

Result

Example View
<button onClick="$controller.onButtonClick(event)">Click me!</button>
Example Controller
class MyComponent {
constructor( logger ) {
this.logger = logger;
}
 
onButtonClick( e ) {
this.logger.warn( "Click event", e ); // logs into the browser console
e.target.setAttribute( "color", "warn" ); // sets the button color to "warn"
}
}
 
return MyComponent;

images/download/attachments/167859116/image2023-1-23_13-43-23-version-1-modificationdate-1705571863016-api-v2.png

The host element can be accessed via the view context after the view has been initialized ( see onViewInit( context ) ).

►NOTE◄ By inserting the view code via innerHTML, browsers usually do not execute script tags. These would then have to be added manually via DOM functions (Element.appendChild())..

►IMPORTANT◄ The markup of the component is hooked into the DOM of the application unchanged with respect to CSS classes, IDs and attributes. No encapsulation/scoping takes place here. So if an attribute is used in the view code, it will occur redundantly for each instance of the component in the DOM.

Negative example:

View Code
<div id="my-component-element"></div>
Controller (Javascript)
onViewInit( context ) {
// DANGER! This will fetch the first element with id "my-component-element" even if this is the second or nth instance of the component!
var containerEle = document.querySelector("#my-component-element");
}

In this negative example, three instances of the 'Free component' also have three elements in the DOM, each with the same "my-component-element" ID.
Therefore id attributes should be omitted.

Positive example:

View Code
<div class="my-component-element"></div>
Controller (Javascript)
onViewInit( context ) {
// SAFE: Fetches only the element with class "my-component-element" within the Free Component instance
var containerEle = context.element(".my-component-element");
}

The context functions (FreeComponentContext) element(query) and elements(query) can be used to ensure that only elements within the 'Free component' are selected.

Style (CSS)

With the help of Cascading Style Sheet (CSS) code, the view can be adapted stylistically as desired. All rules and restrictions of the CSS standard apply.
The only special case here is a direct selector on the host element. For this the keyword ":host" was introduced, which is replaced by a selector on the host element.
Using :host as a root selector is highly recommended to avoid collisions with existing stylesheets and to increase the performance of the CSS engine.

Example:

Example code

Result

Example View
<span class="heading">Hello World!</span>
Example Stylesheet
:host .heading {
display: block;
background-color: «_v( primaryColor )»;
color: «_v( primaryContrastColor )»;
padding: 0.5em 1em;
}

Tip: The Style Palette can be accessed directly via the placeholder syntax "«_v( style-property-name[, fallback-value] )»".

images/download/attachments/167859116/image2023-1-23_13-44-10-version-1-modificationdate-1705571863018-api-v2.png

Color indicator

The color indicator of the component is automatically defined via the 'indicator' attribute of the host element (context.hostElement.getAttribute('indicator')). The attribute is not present if no color indicator is set.

Indicator values are:

  • primary – primary color

  • secondary – secondary color

  • warn – warning indicator

  • error or invalid – error/invalid indicator

  • success – success indicator

Localization

With the help of the Resources tab, localized values from localization can be declared, which can then be used via their ID (1).
images/download/attachments/167859116/image2019-10-4_8-30-53-version-1-modificationdate-1705571862980-api-v2.png

The declaration of a resource consists of its already mentioned internal ID (1) and the specification of the localization entry via Resource bundle (2) and Resource name (3).

Why must resources be declared separately?
When exporting/importing a free component into another Lobster Data Platform / Orchestration system, it must be known which localization entries are used in the component so that they can be exported as well.
Since resource names can also be derived from the program here, it is impossible to automatically recognize the resources used. Therefore the explicit declaration.

In the component's controller, declared resources can then be used via the view context's "rs( ID, default, arguments )" method ( passed to onViewInit( context ) ):

onViewInit( context ) {
this.context = context;
var hintValue = this.context.rs( "hint" ); // loads the declared resource with ID "hint"
// ...
}

The 'rs' method supports the following parameters:

Parameter name

Optional

Explanation

resourceIdentifier: string

no

The ID of the resource declaration.

defaultValue: string

yes

A default value if the declared resource was not found.

args: any[ ]

yes

A list of arguments which are converted to a string and used for placeholders in the resource text in the appropriate order.
Placeholders in resources are defined via {i}, where 'i' is a consecutive number starting at 0. This number defines the index of the argument to be set.

localeCode: string

yes

Specifies the locale code of the desired language (e.g.: 'de_DE'). If no code is specified (recommended), then the language of the current user session is used.

Localizations can also be statically used directly in the view part (HTML). For this purpose, a placeholder syntax has been agreed upon, which is evaluated and replaced accordingly when the view is initialized.
Syntax for using resources directly in the markup: <[resourceIdentifier,defaultValue,arg0,arg1,arg2,arg3,...]> All parameters except 'resourceIdentifier' are optional.

Example:

<span class="heading" title="<[hint,no hint resource]>"><[mainHeading]></span>

Importing (external) libraries and modules

Within the controller code the global function "syncImport( importPath )" is available, which allows the import of already defined modules. However, this requires knowledge of the internal Lobster API, which is not the subject of this documentation.
Also globally available is the SystemJS framework, which can be accessed via the 'System' keyword.
External libraries, which can be loaded directly by embedding a JavaScript, are specified in the Dependencies tab. The component is not initialized until all dependencies are loaded.

images/download/attachments/167859116/image2023-1-23_13-47-1-version-1-modificationdate-1705571863020-api-v2.png

Since the order of the libraries to be imported is extremely important in most cases, the loading order can be influenced via the 'Up' and 'Down' functions (1).
In a similar way to the scripts, stylesheet files can be loaded as well. The "onViewInit( context )" method of the controller is only called when all specified stylesheets have been loaded without error.

►CAUTION◄ If strict content security policies are configured (system configuration WebAppManager.xml response headers), there will be problems when external libraries are included. These can also be entered as exceptions via the Content-Security-Policy HTTP header, but it may be that the used library itself does not follow strict content security policies, e.g. if it uses inline code execution (eval()/Function()). Here, either a suitable library must be selected or the security settings must be adjusted.

Logging

The constructor of the controller is passed the logger instance assigned to it. This offers the following log level methods:

  • trace( ... messageParts: Object[] )

  • debug( ... messageParts: Object[] )

  • info( ... messageParts: Object[] )

  • warn( ... messageParts: Object[] )

  • error( ... messageParts: Object[] )

  • fatal( ... messageParts: Object[] )

►NOTE◄ By default, the client logs at least the 'warn' level. For 'info', 'trace' and 'debug', the 'debug' parameter must be added to the browser URL (e.g.: http://pro.server.address/?debug).

It is recommended not to log via console.log( ), but to use the logger to get additional useful information such as the log source or timestamp and to keep a consistent console image.

Special features and tips

Debugging free components

The controller code of a free component can be debugged directly in the developer tools of the web browser (usually F12).
However, there is no direct or obvious entry point into the code. Therefore, a little trick can be used.

As soon as a console output is made in the controller code, the browser resolves the line of output inside the virtual component.
In most browsers, you can navigate directly to the corresponding location by clicking on this line specification. Here you can also use all debugger functions of the browser (including breakpoints).

Example based on the integrated sample code (Settings → Load example)

The first line of the controller code simply executes the statement "console.log( 'DEBUG' )", which writes an entry to the browser console without any detours.

images/download/attachments/167859116/image2020-11-5_13-56-25-version-1-modificationdate-1705571862987-api-v2.png

By clicking on the line reference, the debugger of the browser jumps directly into the controller code, to the place where "console.log()" was called.
As long as the client is not restarted, persistent breakpoints can be set here.

images/download/attachments/167859116/image2020-11-5_14-1-47-version-1-modificationdate-1705571862989-api-v2.png
The screenshot above shows a triggered breakpoint in line 16 of the controller code.

The appearance of the debugger varies from browser to browser, but usually offers the same functions.

Working with list types

Form modules and elements can process and return different list types. The following should be considered when developing a free component. For example, the data of a Repeatable element container initially returns an ArrayCollection, which implements the IList interface. For read-only processing of such a collection, it is recommended to simply use the toArray(false) method. The false parameter indicates that no copy of the internal buffer except the original is to be returned.

IList Interface (Collections)
/**
* The number of elements in the list
*/
length: number;
 
/**
* Adds an item to the list and returns the
* new length of the list or 0 if the item was not added
*
* @param item The item which will be added
* @returns The new length of the list or 0 if the item was not added
*/
addItem(item: T): number;
 
/**
* Inserts an item at the given index
*
* @param item The item which will be inserted
* @param index The insert position
* @returns The new length of the list or 0 if the item was not added
*/
addItemAt(item: T, index: number): void;
 
/**
* Returns the item at the given position
*
* @param index The index of the item
* @returns The item at the given position
*/
getItemAt(index: number): T;
 
/**
* Returns the index of the given item or -1
* if the item is not contained in this collection.
* How the items are compared is defined by the 'useEquals' parameter (see useEquals parameter)
*
* @param item The item
* @param start The start index at which to begin the search
* @param useEquals If true, the element will be searched by using Env.areEqual()
* @returns The index of the given item or -1 if it is not contained
*/
getItemIndex(item: T, start?: number, useEquals?: boolean): number;
 
/**
* Determines whether the given item is contained in this ArrayCollection or not.
* How the items are compared is defined by the 'useEquals' parameter.
* If it is false (default), the simple indexOf(item) >= 0 condition will be checked.
* If it is true, an item match is considered through Env.areEqual().
*
* @param item The item which will be checked
* @param useEquals If true, the element will be searched by using Env.areEqual()
* @returns True if the given item already exists in this ArrayCollection, else false
*/
contains(item: T, useEquals?: boolean): boolean;
 
/**
* Removes all items from this list
*/
clear(): void;
 
/**
* Removes the item at the given index and
* returns it
*
* @param The index of the item
* @returns The removed item
*/
removeItemAt(index: number): T;
 
/**
* Removes the given item from the collection
* and returns the index on which it was removed.
* If the returned index is -1 the item was not in the collection.
* How the items are compared is defined by the 'useEquals' parameter (see useEquals parameter)
*
* @param item The item which will be removed
* @param start The start index at which to begin the search
* @param useEquals If true, the element will be searched by using Env.areEqual()
* @returns The index on which the item was removed or -1 if it was not in the collection
*/
removeItem(item: T, start?: number, useEquals?: boolean): number;
 
/**
* Replaces the item at the given index with the
* given one and returns the replaced item
*
* @param item The item which will be set
* @param index The index of the replaced item
* @returns The replaced item
*/
setItemAt(item: T, index: number): T;
 
/**
* Replaces the item at the given index with the
* given one and returns the replaced item
*
* @param index The old ( current ) index of the item
* @param newIndex The new position
* @returns The length of the list or 0 if the indexes was out of range
*/
moveItem(index: number, newIndex: number): number;
 
/**
* Writes this list to a native array and returns it
*
* @param copy Indicates whether the returned array is a copy of the source or the source array itself (defaults to true)
* @returns An array representation of this list (as copy)
*/
toArray( copy?: boolean ): T[];
 
/**
* Iterates through the list and calls the given callback function with three parameters:
* - item The current item
* - index The current index
* - list The list itself
*
* If one of the callback calls returns a value that is not undefined, the forEach loop will break and return this value.
*
* @param callback A callback function which will be called for each element in the list
* @param thisArg An optional parameter to define the 'this' scope which will be set to call the callback
* @returns The return value that has been returned by any of the callback calls or undefined if either undefined was returned or no callback returned anything
*/
forEach(callback: (item: T, index: number, list: IList<T>) => void, thisArg?: Object): any;

►NOTE◄ In addition, help utilities are available in the controller code, which also facilitate and normalize the handling of lists. These utilities can be accessed via the FCUtils icon and are described in more detail in the Free component Utils subchapter.

Here, working with arrays and IList objects can be unified without having to pay attention to the exact type of the list.

Example: Normalizing list operations via FCUtils
FCUtils.isCollection( anyData ); // returns true if data is an IList or an array
FCUtils.toArray( collection ); // returns collection as array if collection is an IList or an array
FCUtils.getItemAt( collection , i ); // returns the item in collection at index i
FCUtils.setItemAt( collection , item, i ); // sets the item in collection at index i
FCUtils.removeItem( collection , item ); // removes a specific item from collection
FCUtils.removeItemAt( collection , i ); // removes the item at index i from collection
FCUtils.addItem( collection , item ); // adds item to collection
FCUtils.addItemAt( collection , item, i ); // adds item to collection at index i
FCUtils.forEach( collection, cb ); // iterates through the collection calling the given callback function for each item (cb(item, index))

Data provider

There are modules in the environment of the form renderer, which provide a so-called data provider (e.g. LocalListDataProvider). Such a provider supports data paging, which can be realized via the methods set/getOffset(), set/getMaxResults() and getOverallCount().
The method getItems() returns the entries of the currently loaded page. If getOverallCount() is greater than the number of these items, additional entries can be loaded via setOffset() followed by update(). All data providers implement the IListDataProvider interface:

IListDataProvider
/**
* Sets the item offset for the required page.
* e.g. Max results is 20 and page 2 is required, the offset would be set to 20
*
* @param value The item offset within the overall item set
* @returns This IListDataProvider instance
*/
setOffset(value: number): IListDataProvider<T>;
 
/**
* Returns the item offset for the required page.
* e.g. Max results is 20 and page 2 is required, the offset would be set to 20
*/
getOffset(): number;
 
/**
* Sets the maximum number of items per item page (set to 0 if paging is not required)
*
* @param value The maximum number of items per item page or 0 to turn off paging
* @returns This IListDataProvider instance
*/
setMaxResults(value: number): IListDataProvider<T>;
 
/**
* Returns the maximum number of items per item page (0 if paging is turned off)
*
* @returns The maximum number of items per item page (0 if paging is turned off)
*/
getMaxResults(): number;
 
/**
* Returns the overall number of items which would have been fetched with the current sort and filter configuration
* if paging was turned off
*
* @returns The overall number of items for the current sort and fitler configuration regardless of paging parameters
* @see getItemCount()
*/
getOverallCount(): number;
 
/**
* Returns the items of the current page as array
*/
getItems(): T[];
 
/**
* Updates the data by applying the filters, sorts and paging parameters.
* The result will be stored in the internal items array ( see getItems() )
*
* @returns A Promise resolving with this IListDataProvider instance once the data is available
*/
update(): Promise<IListDataProvider<T>>;