Form Scripting (Client-side)

This documentation is valid from yuuvis® Momentum, Version 2020 Autumn.

If you configure custom forms for objects, you can additionally use executable scripts to, e.g., validate data, change data, change field properties - such as "read-only" or "mandatory" - and show context-related messages. Form-related scripts enhance your options by adding further functionalities to support your use cases and processes the best. 

Table of Contents

Introduction

In yuuvis® Momentum client, you can use executable scripts to:

  • validate data
  • change data, for example by calculating a value based on other values
  • change field properties, such as read-only and visibility
  • show context-related messages.

Some things to note about script properties:

  • Client scripts in yuuvis® Momentum architect are written in JavaScript (ECMAScript).
  • Client scripts execute in the user's browser using the browser's native JavaScript runtime.
  • You can define client scripts by object type and form situation.

Note: When field data changes are made using a script (i.e., without user action) when loading the form, the Save option is not available. In contrast, you can disable fields to protect against user changes.

Script Scope 

The relevant object is given to each client script under the name 'scope.' This object provides the API so the scripts can access the object fields and their properties.

Properties of 'scope'

Name

Description

apiSupplies access to the plug-in API, with 'session' (user information), 'dms' (object details, search via DMS-Service), 'http' (connection to any service), 'config', 'util' (helper functions) and 'agent' properties.
dataSupplies all object fields defined in the object or process activity. The fields offer read-only access using the technical name. Available for releases 2017-09-27 (3.22.x) or later.
modelSupplies the flattened form model and all object fields defined on the form. The fields can be accessed with the technical name. The form groups cannot be accessed in this way.
situation

Supplies the current form model situation. Scripts can respond to the relevant situation. Possible values are 'CREATE' (create), 'SEARCH' (search) and 'EDIT' (edit index data).

objectIdSupplies the ID of the current DMS object if available (available since version 6.4).

scope.data

A value of a system property of an object can be read using scope.data['system:<property name>']. These are some of the available system properties. You can find more in the browser console when looking for the currently loaded form script and hovering over 'scope' and looking for section 'data':

Name

Description

system:objectIdUnique ID of the object
system:objectTypeIDTechnical name of the object type, e.g., 'tenMytenant:customer'
system:baseTypeIdSpecifies whether an object is of type 'folder' or 'document'
system:creationDateThe date and time of cration in the format 'yyyy-mm-ddThh-mm-ss.xxxZ'
system:createdByThe title of the user that created the object in the format <name>,<surname> (<loginname>)
system:lastModificationDateThe date and time of last modification in the format 'yyyy-mm-ddThh-mm-ss.xxxZ'
system:lastModifiedBy

The title of the last user that edited the object in the format <name>,<surname> (<loginname>)

system:versionNumberCurrent version number of the object
system:secondaryObjectTypeIdsThis array lists all secondary object types that are part of the current object instance
system:tenantThe tenant name as used when created
system:traceId

Trace ID of the object (todo: write about the meaning)

system:parentId

This ID is given if a document object is filed into a folder. 


Evaluate on data and getUser()
// assign the current objectId to a reference field and the current userId to an organisation field:
scope.model['tenMytenant:reference'].value = scope.data['system:objectId'];
scope.model['tenMytenant:user'].value = scope.api.session.getUser().id;              

For scope.api please refer to "Client Scripting API".

scope.situation

For object types, you can create a default form to be used for the CREATE, EDIT and SEARCH situations. In each situation, any included scripts are active.
If a general form is used, but different data management is necessary, it is possible to check which situation is given.
For example, how to deactivate a form script to be used in the 'SEARCH' situation.


Check situation

Check situation
if( scope.situation == 'CREATE' ) return;      // another situation value is EDIT
// ... additional script code

scope.model

This section describes how to access all form elements of objects or processes.

General object field properties

The following table describes object field properties that can be accessed with 'scope.model'.

Column "Binding"

  1. RO (ReadOnly): ReadOnly properties can only be read. Changes to the values of RO properties do not affect the interface. 
  2. RW (ReadWrite): ReadWrite properties can also be written. Changes to the values of RW properties affect the interface.

Each field has the following properties:

NameDescriptionBinding
name

The normalized name of the fields. Normalized means the simple field name is lower case. The name must not contain special characters except one ':'. This name is used to map the fields to the 'model.'

RO*
labelThe display name of the type in the current user locale. Used as a field identifier.RO
descriptionA field description. Can be used in tooltips for example.RO
type

Describes the data type of the field. The possible values here are documented in the description of field data types. Other field attributes may exist, depending on the data type.

RO
readonlyIf the read-only property is set to 'true,' the user cannot change the field value.RW**
requiredFlags a field as mandatory. If this property is set, the user must make an entry.RW
valueThe current value of the field.RW
cardinality

In case of 'multi' instead of 'single' (equal 'undefined') a list of values can be maintained. A JavaScript array is then always expected in 'value.'

Not every data type supports the 'multiselect' property.

RO

*RO (ReadOnly): ReadOnly properties can only be read. Value changes of RO properties do not affect the interface.

**RW (ReadWrite): ReadWrite properties can also be written. Value changes of RW properties affect the interface.

The following example validates dynamic field properties for required fields and write permissions.

Example: onChange handler for form validation and user input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

// Abbreviate with 'm' in the FormModel of the scope.
// You could just use 'scope.model' everywhere instead of 'm'.
var m = scope.model;
// We want to know if the active state changes
// To do so, we register an onchange handler function
m['tenMytenant:aktiv'].onchange=updateActiveState;
// The logic should also run when there are changes in the area, since we want to
// control the error state of the script here.
m['tenMytenant:area'].onchange=updateActiveState;
// When the weekly hours change, we want to know this for our calculation
m['tenMytenant:weekhours'].onchange=updateWeekdays;

// Here is the logic implementation for what happens when the active state changes

function updateActiveState() {

  // Employee initials + status + employee number are required fields,
  // when the employee is active.
  m['tenMytenant:emplshort'].required      = active;
  m['tenMytenant:status'].required         = active;
  m['tenMytenant:personnelno'].required     = active;
    
  // The regulations cannot be changed as long as the employee is active.
  m['tenMytenant:unbefristet'].readonly        = active;
  m['tenMytenant:zielvereinbarung'].readonly   = active;
  m['tenMytenant:altersvorsorge'].readonly     = active;
    
  // Example for a deliberately set validation error with relevant error message
  // take into account that the error message will be offered for non read-only fields!
  if( active && (!m['tenMytenant:area'].value || m['tenMytenant:area'].value=='') ) {
    m['tenMytenant:area'].error = {msg:'Ein aktiver Mitarbeiter muss einem Bereich zugeordnet sein.'};
  } else {
    // If the validation error does not occur, we may have to reset a previously set error:
    if (scope.user) {
        m['tenMytenant:area'].error = null;
    }
    m['tenMytenant:aea'].error = null;
  }
}
// Here we calculate the weekdays
function updateWeekdays() {
    m['tenMytenant:weekdays'].value=m['tenMytenant:weekdays'].value / 8;
}

// Since the active/not active logic should already apply during form initialization,
// we call the function here.
updateActiveState();

Data types

The following table gives an overview of the possible data types per field. The JavaScript data type lists what is expected as the 'value' of an element.

If the 'multiselect' property is set, then the JavaScript data type is an array of the data type.


Name

Description

JavaScript data type

Multi-selection

STRINGAny text. See also datatype: STRING.StringYes
NUMBERNumber and floating point number. See also datatype: NUMBER.NumberYes
BOOLEANSimple 'on/off' or 'true/false' value.BooleanNo
DATETIMEA date or a date with time value. DateYes
ORGANIZATIONA string field for saving user IDs. The title of the user is shown, or the ID if no title exists.StringYes
IDA string field for saving object IDs. The IDs can be restricted to the specified object types defined in the classification tag 'id:reference[object type 1, object type 2, ...]. The title of the object is shown, or the ID if no title exists, or '!******' if the object cannot be accessed.StringYes
CATALOGA string field for saving elements of a given array in the classification tag 'catalog[value1, value2, ...]'StringYes
TABLEA table with columns of the above data typesTableNo

STRING

For fields of type 'string' the following properties are given:

NameDescriptionBinding
maxlenThe maximum number of characters permitted as a value in this field.RO
minlenThe minimum number of characters permitted as a value in this field.RO
classification

If available, a specific type of text field is described.

  • 'email' to handle this as e-mail input field
  • 'url' to handle this as web address input field
  • 'phone' to handle this as phone number input field
  • 'id:organization' to handle this as user input field
  • 'id:reference [myObjectTpyte,...]' to handle this as object-reference input field
  • catalog[new,draft,review,released,rejected] to handle this as catalog input field
RO

*RO (ReadOnly): ReadOnly properties can only be read. Changes to values of RO properties do not affect the interface.

**RW (ReadWrite): ReadWrite properties can also be written. Changes to values of RW properties affect the interface.

NUMBERS

For fields of type integer and decimal the following properties are given:

NameDescriptionBinding
maxvalueThe maximum number permitted as a value in this field.RO
minvalueThe minimum number permitted as a value in this field.RO
classification

If 'digital' is set the numbers are formatted with a thousand separator, e.g. for Din EN 1,569,345.43 and DE 1.569.345,43

RO

*RO (ReadOnly): ReadOnly properties can only be read. Changes to values of RO properties do not affect the interface.

**RW (ReadWrite): ReadWrite properties can also be written. Changes to values of RW properties affect the interface.

DATE

Sample Script: Specifying a date in the future

When determining a deadline for working on the next process step, you want to make sure the date is not in the past.
Example: Validate a date in the future

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

var m=scope.model;
// The name of the date field is Deadline
var deadline=m['myTenant:deadline'];
 
 
// Register the onchange handler
deadline.onchange=updateDeadlineState;
 
 
// Next is the logic for what we want to happen if the state changes
function updateDeadlineState() {
    
  if( isBeforeToday( deadline.value ) ) {
    deadline.error = {msg:'Please select a date in the future.'};
  } else {
    // If the validation error does not occur, we may have to delete a previously-set error
    deadline.error = null;
  }
}
function isBeforeToday( pDate ) {
    
  // Todo: rework not available moment to a different function:
    var date = new Date();
    var today = moment(date).startOf('day');
    return moment(pDate).isBefore(today);
   
}

DATETIME

Important to know:

// allowed:
scope.model['tenMytenant:datetime'].value = new Date()
scope.model['tenMytenant:datetime'].value = moment().toDate()
 
// not allowed, sets the date one day into the past after save:
scope.model['tenMytenant:datetime'].value = moment();

TABLE

As of version 2.4, tables are supported.

Expects a JavaScript 'Object Array' as a 'value.' The properties of each object are defined by the column elements of the table. See the following example.

Manipulating table data
Example: Manipulating table data
/**
 * Example script: Manipulating table data based on index data change.
 */
// Add change listener to field 'myTenant:number'
scope.model.['myTenant:number'].onchange=function() {
     
    // The current user input on field with the internal name 'myTenant:number'
    var num = scope.model.['myTenant:number'].value;
     
    // Shortcut to access the table
    var table = scope.model.['myTenant:changes'];
     
    if(num>0 ) {
        // If 'myTenant:number' is set (greater than 0) we automatically fill the table
        if( !table.readonly ) {
            // The user may not modify the table
            table.readonly=true
        }
    } else {
        // If 'myTenant:number' is not set (less than 0) we let the user fill the table
        if( table.readonly ) {
            // The user may modify the table
            table.readonly=false
        }
    }
    // automatically fills the table if 'myTenant:number' is greater than 0
    // and add num rows to the table
    if(num>0) {
        // Clean up the table by setting a empty array
		table.value.length=0;   // changes of a single cell must be done in an array variable first, 
								// and then this array has to be pushed back to the table.
        for( i=0; i<num; i++ ) {
            // For each 'myTenant:number' add a row
            // Each row is an object defined by the internal names of the column elements.
            table.value.push(
                {
                    'myTenant:accepted': i%2,                              // Boolean
                    'myTenant:created': new Date() ,        // Date
                    'myTenant:activedate': new Date(),     // Another date
                    'myTenant:author': 'Marie Curie '+i,                       // String: Author name with i postfix
                    'myTenant:prio': i+.42,                                    // Decimal
                    'myTenant:company' : 'OSVH'                                // Codesystem - the 'data' values must be used.
                }
            )
        } // end for num
    } // end if num>0
}
Callback for tables
NameDescription
onroweditThis callback is called when the user starts to edit a table row. Like 'onchange,' the first parameter contains the field element (the table itself). The second parameter contains a 'row' object, which describes the row the user wants to edit.
The 'row' object

The 'row' object is transferred as the second parameter during the 'onrowedit' callback for table fields.

NameDescriptionBinding
indexRow index: '-1' for newly created rows. The first row has the value '0'.RO*
copyEnabledControls whether the "Copy and create as new row" function is enabled. You can only edit this synchronously inside the onrowedit.RW**
deleteEnabledControls whether the "Delete row" feature is enabled. You can only edit this synchronously inside the onrowedit.RW
saveEnabledControls whether the "Save row" feature is enabled. You can only edit this synchronously inside the onrowedit.RW
persistedIs 'false' if the edited file was newly created. The property remains 'false' for new rows until the index data is saved. You can use this property to differentiate between a row that has been saved or newly created by the user during the current index data editing.RO
modelProvides access to the model of the current row. With this model, the script can access and modify the values and element properties of the current row.RW

*RO (ReadOnly): ReadOnly properties can only be read. Value changes of RO properties do not affect the interface.

**RW (ReadWrite): ReadWrite properties can also be written. Value changes of RW properties affect the interface.

See how the 'row' object is used in the following example.

Sample script: Table row scripting
Example: Table row scripting (onrowedit)
// Example for row edit
// The 'scope.model.['myTenant:notices']' element is a table type with a few columns.
// This example script tries to achieve that only new rows can be edited. New rows are rows that the user has not saved yet.
scope.model.['myTenant:notices'].onrowedit=function(table,row) {
    // 'table' is the element, the callback event was triggered.
    // In this case the same as scope.model.notices. 'row' is the row object.
 
    // Show some information as a toast notification - just for demonstration
    scope.api.util.notifier.info("Editing table row","A row at index "+row.index+" is being edited.");
 
    // First we check whether the row is a new, not yet persisted, row, or an existing row
    if(!row.persisted) {
        // This is a row created by the user.
        // He is allowed to change it. So all table columns are set to be editable
        // by setting all row fields' readonly property (the column elements) to false.
        Object.keys(row.model).forEach( function(e) { e.readonly=false });
         
        // Delete/save is enabled by default, but the user is not allowed to copy the existing row as a new one.
        // So we switch off the copy function.
        row.copyEnabled = false;
    } else {
        // This is an already-saved row, so cell values are no longer editable, we have set them all to read only.
        Object.keys(row.model).forEach( function(e) { e.readonly=true });
         
        // Copy/delete/save are all disabled. The user can only 'cancel' the row editing. Nothing else.
        row.copyEnabled = false;
        row.deleteEnabled = false;
        row.saveEnabled = false;
    }
}
onChange handler for table cells

You define table rows in the form model (scope.model). Here you can set properties for each row. When the user edits a row, the system creates a temporary copy of the table elements. This copy can be accessed by a form script using the second parameter in the onchange Handler. Changes to the model are immediately visible in the table editing dialog. Changes to the properties of rowmodel are discarded when row editing has ended.

Example: onChange handler for a table cell
scope.model.['myTenant:mytable'].onrowedit = function(table, row){
	// On this element we register a value change handler
	row.model.['myTenant:number'].onchange = function(el, rowmodel) {	
		// The rowmodel provides access to the other elements
		rowmodel.['myTenant:number'].readonly = ( el.value == 42 );
	}
}
Manipulating row cells when one cell has been changed
var countries = scope.model.['myTenant:addresslist'].onrowedit=function(table,row){
	row.model.['myTenant:country'].onchange=function() {
		if(row.model.['myTenant:country'].value === 'Germany'){
			row.model.['myTenant:zip'].required=true;		// depending on value of country set a zip code as mandatory
		} else {
			row.model.['myTenant:zip'].required=false;
		}
	}
}
Manipulating multiple row cells

If we deal with more complex table value manipulation, we need to create a copy of the values. Once the values are written, the table is populated and changes will be lost. Once all the cell manipulations are done, we can assign the manipulated values to the table values.

// see Manipulating table data
...
let tableCopy = JSON.parse(JSON.stringify(['myTenant:mytable'].value)); //create a copy
for ( i = 0; i < num; i++ ) {
	if (i === 0) {
			tableCopy[i].['myTenant:accepted'] = !i%2,                               // Boolean
            tableCopy[i].['myTenant:created'] = new Date(),    // Date
            tableCopy[i].['myTenant:activedate'] = new Date(), // Another date
			tableCopy[i].['myTenant:author'] = 'Pierre Curie '+i,                    // String: Author name with i postfix
            tableCopy[i].['myTenant:prio'] = i+.00,                                  // Decimal
            tableCopy[i].['myTenant:company'] = 'OSVH'                               // Catalog values must be used.
	} else {
    	// For each number add a row
	    // Each row is an object defined by the internal names of the column elements.
    	tableCopy.push({
            	'myTenant:accepted': i%2,                              // Boolean
	            'myTenant:created': new Date(),    // Date
            	'myTenant:activedate': new Date(), // Another date
        	    'myTenant:author': 'Marie Curie '+i,                   // String: Author name with i postfix
    	        'myTenant:prio': i+.42,                                // Decimal
	            'myTenant:company': 'OSVH'                             // String: Element of a catalog
    	    });
	}
	// do even more fancy data manipulations
} // end for num
table.value = tableCopy;
...
// see Manipulating table data


scope.api.<more>

Please refer to the Client Scripting API documentation for more features you may use for specific requirements like notifying users, getting information about the current user, or calling other services via http.