Client-side Form Scripting

This article describes the scripting functionality which can be used for data manipulation and validation in forms for objects and process tasks.

Introduction

In yuuvis® RAD 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 required
  • show context-related messages.

Some things to note about script properties:

  • Client scripts in yuuvis® RAD designer 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, and by process model and activity forms
  • You can save script functions in global scripts to reuse them or include them in client scripts

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'

NameDescription
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 release 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

Even when not defined in the form (object forms as well as process task forms), you can access object field data or process variables by using this part of the scope. Use the technical name of the object type field or process model variable to read its value, as shown in the following example.

Check situation
if (scope.data.status) {			// only fields not NULL are in the data object, so check for existence
	if (scope.data.status == 'active') { ... }
}

scope.situation

For object types, you can create a default form to be used for the situations CREATE, EDIT and SEARCH. 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 situation 'SEARCH.'


Check situation
if( scope.situation == 'SEARCH' ) return;      // other situation values are EDIT and CREATE
// ... additional script code

scope.model

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

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. This name is used to map the fields to the 'model.'

RO*
qname

The qualified name. Always <normalized type name>.'name'

Warning: The script leads to an error if qnames are not completely written in lower case

RO
labelThe display name of the type in the current user locale. Used as a field identifier.RO
descriptionThe field description is shown below the form field. 
Beginning with version 10.0 and hotfix 6 of version 9.16 LTS this field is RW
RO
RW
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 readonly 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
multiselect

If this property for fields is set up in the schema, lists 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.

Sample script: onChange handler for form validation and user input

Example: onChange handler for form validation and user input
// 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.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.bereich.onchange=updateActiveState;
// When the weekly hours change, we want to know this for our calculation
m.weekhours.onchange=updateWeekdays;
// Here is the logic implementation for what happens when the active state changes
function updateActiveState() {
  // Track the current active state - as an abbreviation.
  var active = m.active.value;
   
  // Employee initials + status + employee number are required fields,
  // when the employee is active.
  m.emplshort.required      = active;
  m.status.required         = active;
  m.personnelno.required     = active;
   
  // The regulations cannot be changed as long as the employee is active.
  m.unbefristet.readonly        = active;
  m.zielvereinbarung.readonly   = active;
  m.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.area.value || m.area.value=='') ) {
    m.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) {
	    scope.model.area.error = null;
	}
    m.aea.error = null;
  }
}
// Here we calculate the weekdays
function updateWeekdays() {
    m.weekdays.value=m.weekdays.value / 8;
}
// Since the active/not active logic should already apply during form initialization,
// we call the function here.
updateActiveState();

Data Types – Field Properties

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.

NameDescriptionJavaScript data typeMulti-selection
STRINGAny text. See also datatype: STRING.StringYes
NUMBERNumber and floating point number. See also datatype: NUMBER.NumberNo
BOOLEANSimple 'on/off' or 'true/false' value.BooleanNo
DATETIMEA date or a date with time value. See also datatype: DATETIME.DateNo
CODESYSTEMAn entry from a catalog. See also datatype: CODESYSTEM.String
Must correspond with the 'data' property in the codesystem. 
Yes
TABLEA table with values. See also datatype: TABLE.Object Array
Properties of each object are defined by the column elements of the table.
No
ORGANIZATIONUser or group in the organization tree. See also datatype: ORGANIZATION.String
Corresponds to the technical name of the object in the organization.
Currently user or group names. For users, this name corresponds to the user's login name.
 
Yes

Data Type-Specific Properties

STRING


NameDMS description (DMS only) *1Binding
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
  • 'selector' to handle this as a selection field that can be filled by scripting
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.

*1: Starting with release 4.3 and in case of BPM, regular expressions can be used for controlling minlen and maxlen, e.g.:

for at least 5 characters:
^.{5,}$

for maximum of 10 characters:
^.{,10}$

for at least 5 and maximum of 10 characters:
^.{5,10}$

for at least 5 letters:

^[a-zA-Z.]{5,}$ etc.

Dynamic lists (Selection)

As with catalog fields, string fields with the api-classification 'selector' are handled by displaying a selection list and selection dialog. You can set the list values using a script.

Note that in yuuvis® RAD designer, the classification is called 'Dynamic list' in English and 'Dynamische Liste' in German.

Important Note

Lists with more than 1,000 elements can lead to delays when opening the selection dialog.

In German only: Webinar on how to use microservices for dynamic lists.


// 1st example: fill a selection list simply
var costcenter = scope.model.costcenter;
costcenter.setList({
	config: {			// configuration with respect to the behavior of the catalog fields 
		allelementsselectable: true,     // use false to only allow to select subentries
		valueField: 'costvalue',         //optional, if not given, the name 'value' is used
		subEntriesField: 'subentries',   //optional, if not given, the name 'entries' is used
        descriptionField: 'descr'        //optional, if not given, the name 'description' is used
	},
	entries: [    // prepare a tree
		{
            costvalue: '4711',
            subentries: [
                {
                    costvalue: '1',
					descr: "Value 1"
                },
                {
                    costvalue: '2',
					descr: "Value 2"
                }
            ]
        },
        {
            costvalue: '4712',
            subentries: [
                {
                    costvalue: '3',
					descr: "Value 3"
                },
				{
					costvalue: '4',
					descr: "Value 4"
				}
            ]
		}
	]
});

// 2nd example: first prepare a flat list variable and a reference to it
var cclist = [];              // this prepares the list 'cclist' with values to be passed to the field 'costcenter' as list 'entries'
for (j = 1; j < 10; j++) {
	for (i = 0; i < 1000; i++) {
		cclist.push({
			value: j*1001000 + i + 1 + ''   // the empty string at the end results in a string format of the value
		});	
	};
};

var costcenter = scope.model.costcenter;
costcenter.setList({
	config: {
		allelementsselectable: true     // optional, default is 'true'; if a hierarchical list is set to 'false', only leaves are selectable
	},
	entries: cclist
});

// 3rd example: lists can be filled by HTTP Request (see also later chapter)
// find in object type 'accounts' where in its field 'costcenter' the value of the variable 'costcenter' is given 
// and generate a list with the value of the fields 'accountno' and 'description
scope.api.dms.getResult({costcenter:costcenter.value},'accounts').then(         // remark: this must be 'then' because 'await' is currently not supported
	function(result) {
		var acnolist = [];
			result.forEach(function(row) {
				if( row.data.accountno ) {
					acnolist.push({
						value: row.data.accountno+' '+row.data.acdescription
					})
				}
			});
			accountno.setList({
				config: {
					allelementsselectable: true
				},
				entries: acnolist
			};
	}),
	function(error) {
		scope.api.util.notifier.error('Failed',error);
	}
);

// 2.b example for tables:

scope.model.etlapositions.onrowedit=function(table,row) {
	row.model.etlacostcenter.setList({
		config: {
			allelementsselectable: true
		},
		entries: [
			{
            	value: '4711'
	        },
    	    {
        	    value: '4712'
			}
		]
	}
}

// 4th example: filling the description depending on the user settings for 'definition language' and different entries for the form situation similar to the standard catalog behavior
var qadynlist = scope.model.myfieldname; 
var myRequest={};							// for search situation take all entries
if ( scope.situation == 'CREATE' ) { 
	 myRequest={create:true};				// for create situation take only those entries where the field 'create' is checked
}
if ( scope.situation == 'EDIT' ) {
	 myRequest={edit:true};					// for edit situation take only those entries where the field 'edit' is checked
}
scope.api.dms.getResult(myRequest,'qadynlist').then(       // remark: this must be 'then' because 'await' is currently not supported
	function(result) {
		var dynlist = [];
		result.forEach(function(row) {
			if( row.data.qavalue ) {
				dynlist.push({
					value: row.data.qavalue,
					description: row.data[scope.api.session.getUser().schemaLocale]    // this feature is available beginning with release 2017-09-13 (3.21.x)
				});
			};
		});
		qadynlist.setList({
			entries: dynlist
		});
	},
	function(error) {
		scope.api.util.notifier.error('Could not values',error);
	}
);

// 5th example: fill dynamic list with favorites data

scope.api.http.get('/service/user/favorites','/rest-ws').then(            // remark: this must be 'then' because 'await' is currently not supported
	function(result) {
	    var favslist=[];
		_.forEach(result.data, function(row) {          // row gets all relevant objects
			favslist.push({
				value: row.title
			});
		});
		scope.model.favoritslist.setList({
			config: {
				allelementsselectable: true
			},
			entries: favslist
		});
	},
	function(error) {
		scope.api.util.notifier.error('Failed to load favorites',error+'');
	}
)

NUMBER


NameDescription (DMS only)Binding
scaleNumber of decimal places. For integer fields, this is 0.RO*
precisionTotal number of digits without separators.RO

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

The value is not available in the 'SEARCH' situation.


DATETIME

Expects a JavaScript 'Date' as value.

NameDescription (DMS and BPM)Binding
withtimeIs 'true' or 'false.' If 'true,' a time in seconds is expected in addition to the date.RO*

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

The value is not available in the 'SEARCH' situation.


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
var m=scope.model;
// The name of the date field is Deadline
var deadline=m.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 ) {
   
  // 
	var date = new Date();
	var today = moment(date).startOf('day');
	return moment(pDate).isBefore(today);
  
}

DATE

Important to know:
// allowed:
scope.model.datumfeld.value = new Date()
scope.model.datumfeld.value = moment().toDate()

// not allowed, sets the date one day into the past after save:
scope.model.datumfeld.value = moment();

The value is not available in the 'SEARCH' situation.

CODESYSTEM

Expects a JavaScript 'String' as value. The string must be the same as given in the 'data' property of the codesystem.

Note: Scripts must set valid catalog values until the client can evaluate them.

Reducing the selectable CODESYSTEM elements by filtering

Entry properties:

NameDescriptionBinding
allowedonnewIf 'true', the entry is visible within the situation of type CREATE.RO*
allowedonupdateIf 'true', the entry is visible within the situation of type EDIT.RO
dataQualified name of the entry.RO
defaultrepresentationUI name of the codesystem entry. The UI representation is built using the representation pattern of the codesystem.RO
idEntry ID.RO
labelLocalized label of this entry.RO
orderIndex value of the entry within the entry list.RO
typeType of entry. Possible values are LEAFENTRY for leaf nodes and INNERENTRY for inner tree nodes.RO

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

You can filter CODESYSTEM entries by providing a filter callback function. The function is called once for each entry in the given CODESYSTEM. The callback function is passed to the CODESYSTEM object by calling the 'applyFilter()' function. The entry object is passed to the callback function as an input value. The function returns 'true' if the current entry passed the filter (otherwise 'false').

scope.model.klasse.applyFilter(function(entry){
  return entry.data === 'Employee';
}) 

To remove the filter: 

scope.model.klasse.applyFilter(null)

For CODESYSTEM fields in tables, the filter callback function can be bound in the 'onrowedit' callback function, as shown in the following example.

scope.model.mytablefield.onrowedit = function(table, row){
	row.model.mycodesystemfield.applyFilter(function(entry){
		return entry.data === 'foo';
	}) 
};

Beginning with version 6.16 this new filter is given to disable tree entries:

scope.model.class.applyDisablingFilter(function(entry){
  return entry.defaultrepresentation !== 'Employee';   // entry is disabled when condition is true
}); 

ORGANIZATION

Expects a Javascript 'String' as value. The string is the technical name of the organization object, and may be user or group names. For users, the technical name is the user login name.

Reducing the Selectable ORGANIZATION Elements by Filtering 

To filter autocomplete values, use the setFilter function to pass a filter config object.

The filter config object can have the following attributes:

NameDescription
type

the type of the organization unit ('USER' or 'GROUP') to be displayed

groupsan array of group names
rolesan array of role names
activeonlywhether or not to display only active users/groups
Example
// set filter
scope.model.myOrgaField.setFilter({
	type: 'USER',                            // show only user elements
	groups: ['Employees', 'Accounting'],     // show only members of the listed groups
	roles: ['Trainer', 'Reviewer'],           // show only members of the listed roles
	activeonly: false						// controls whether or not only active users are listed
});

// remove filter
scope.model.myOrgaField.setFilter(null);

In the example above, the filter shows only users that are in the groups 'Employees' or 'Accounting' and are either in the role 'Trainer' or 'Reviewer'. To remove a filter, set it to null.

ID References

In the case of reference fields based on type ID a user is supported by a dialog that offers a search for possible objects to be selected. If only objects of the same context are allowed to be referenced this simple trick will solve this requirement:

Performe filtering on context objects
if(scope.context){
	scope.model.myidreferencefield.contextId = scope.context.id;
}
Reducing the Selectable ID Reference Elements by Filtering 

This feature can be used beginning with version 6.14.

To filter autocomplete values, use the setQueryFilters function to pass a filter config object.

The query filters are already documented as "filters" in this article: Search Service API#Request(HTTPbody)

Filter example
scope.model.responsibles.setQueryFilters({
	"personalakte.name": {
		o: 'eq',
		v1: 'Engel'
	}
})

Beginning with version 8.4., the filter for reference fields of abstract object types can be extended by an array of concrete object type names to restrict the filter by these object types:

Filter example
scope.model.referenceddocs.setQueryFilters({
	"basisdocument.status": {
		o: 'eq',
		v1: 'active'
	}
}, ["contract", "statementofearnings"])

TABLE

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

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

NameDescriptionBinding

No additional properties defined


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.

Sample script: Manipulating table data
Example: Manipulating table data
/**
 * Example script: Manipulating table data based on index data change.
 */
// Add change listener to field number
scope.model.number.onchange=function() {
     
    // The current user input on field with the internal name number
    var num = scope.model.number.value;
     
    // Shortcut to access the table
    var table = scope.model.changes;
     
    if(num>0 ) {
        // If number is set (greater than 0) we automatically fill the table
        if( !table.readonly ) {
            // The user may not modify the table
            table.readonly=true;
            // Copy the current values to a property using lodash
            table.old_value=_.map(table.value, _.clone);
        }
    } else {
        // If number is not set (less than 0) we let the user fill the table
        if( table.readonly ) {
            // Copy the old values back to values using lodash
            table.value=_.map(table.old_value, _.clone);
            // The user may modify the table
            table.readonly=false;
            // Cleanup
            delete table.old_value;
        }
    }
    // automatically fills the table if 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 number add a row
            // Each row is an object defined by the internal names of the column elements.
            table.value.push(
                {
                    accepted: i%2,                              // Boolean
                    created: moment().add(i,'day').toDate(),        // Date
                    activedate: moment().add(i,'day').toDate(),     // Another date
                    author: 'Marie Curie '+i,                       // String: Author name with i postfix
                    prio: i+.42,                                    // Decimal
                    company : 'OSVH'                                // Codesystem - the 'data' values must be used.
                }
            )
        } // end for num
    } // end if num>0
}


Callback for Table 
NameDescription
onrowedit

This 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.

You can access the current value of the table using scope.model.mytable.value.

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 row. 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.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.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 a 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.mytable.onrowedit = function(table, row){
	// On this element we register a value change handler
	row.model.number.onchange = function(el, rowmodel) {	
		// The rowmodel provides access to the other elements
		// 'number2' is another element of type 'NUMBER' that is contained in the table element
		// For the sake of example this field is set to readonly, if the user inputs a special number in 'elementNumber'

		rowmodel.number2.readonly = ( el.value == 42 );
	}
}
rowmodel: Applying a Filter to an Organization Element that is Used as a Table Element
Example: Apply filter to organization objects in table rows
scope.model.mytable.onrowedit = function(table, row){
	// On this element we register a value change handler.
	row.model.mybool.onchange = function(el, rowmodel) {
		// The rowmodel provides access to the other elements
		// 'myorg' is another element of type 'ORGANIZATION' that is contained in the table element.
		// We apply different filters, depending on the current value of 'myBool'
		if( rowmodel.mybool.value ) {
			rowmodel.myorg.setFilter({
				type: 'USER',              // show only user elements
				groups: ['Employees'],     // show only members of this group
			)};
		} else {
			rowmodel.myorg.setFilter({
				type: 'USER',              // show only user elements
				groups: ['Accounting'],    // show only members of this group
			)};	
		}
	}
}
Manipulating Row Cells when one Cell Has Been Changed
var countries = scope.model.addresslist.onrowedit=function(table,row){
	row.model.country.onchange=function() {
		if(row.model.country.value === 'Germany'){
			row.model.zip.required=true;		// depending on value of country set a zip code as mandatory
		} else {
			row.model.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 gets populated and changes will get 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(table.value)); //create a copy
for ( i = 0; i < num; i++ ) {
	if (i === 0) {
			tableCopy[i].accepted = !i%2,                               // Boolean
            tableCopy[i].created = moment().add(i+5,'day').toDate(),    // Date
            tableCopy[i].activedate = moment().add(i+5,'day').toDate(), // Another date
			tableCopy[i].author = 'Pierre Curie '+i,                    // String: Author name with i postfix
            tableCopy[i].prio = i+.00,                                  // Decimal
            tableCopy[i].company = 'OSVH'                               // Codesystem - the 'data' 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({
            	accepted: i%2,                              // Boolean
	            created: moment().add(i,'day').toDate(),    // Date
            	activedate: moment().add(i,'day').toDate(), // Another date
        	    author: 'Marie Curie '+i,                   // String: Author name with i postfix
    	        prio: i+.42,                                // Decimal
	            company: 'OSVH'                             // Codesystem - the 'data' values must be used.
    	    });
	}
	// do even more fancy data manipulations
} // end for num
table.value = tableCopy;
...
// see Manipulating table data


Handling an Empty List as Erroneous (not Null)
scope.model.addresslist.onchange=function(){

	if(scope.model.addresslist.value.length > 0){
		scope.model.addresslist.error = null;
    } else if (scope.model.addresslist.value.length == 0) {
		scope.model.addresslist.error = {msg:'At least one address must be given.'};
	}
}

var init = function(){
	if(scope.model.addresslist.value === null || scope.model.addresslist.value.length < 1){
		scope.model.addresslist.error = {msg:'Mindestens eine Adresse muss angegeben werden.'};
	}
}

init()


Scripts for BPM Forms

For scripts that modify or change elements, it does not matter whether the form is a BPM activity form or a DMS index data form. However, the BPM data types must be mapped to DMS data types.

Following is a list of differences between field properties of DMS and BPM forms: 

  • qname
    The qualified name qname is not supported.

  • required
    The property required is not supported, but can be used by scripts.

  • name
    The name of the element is the technical name of the data field bound by the parameter.

  • label
    The label stems from the localized name of the data field bound by the parameter.

Mapping BPM Data Types to DMS Data Types

BPM data typeDMS data typeDescription
String + IDSTRING
BooleanBOOLEAN
Date + LocalDateDATETIMELocalDate is withtime false. LocalDate therefore stands for a day without time.

Decimal + Long

NUMBERscale is not currently supported.
CodeSystemCODESYSTEM
All list typesTABLE

Reading and Setting Feasibility of Activity Actions

In some cases, individual activity actions should be deactivated if necessary data are not given. The following example toggles an action when a Boolean value is changed.

scope.model.checkbox.onchange = function(){
	scope.actions['1'].feasibility(!scope.actions['1'].feasibility());
};

'1' is a code which is set in yuuvis® RAD designer for each individual activity action.  

If feasibility = false, the action button is set to inactive.

Since actions cannot be added to the BPM start form, the feasibility method cannot be used to, for example, deactivate the OK button of the BPM start form. To deactivate the OK button, you can use the error setting for a field, as shown in the example below.

If ( scope.objects[0].data.status == ‘active’) {
   Scope.model.responsibility.error = {msg: ‘Customer already set to active‘};
}



Setting a field error state

You can set an error state for fields with invalid values using 'element.error'. The system then displays an error message for the field and deactivates the Save button.

Example

...
if( active && (!model.area.value || model.area.value=='') ) {
    model.area.error = {msg:'An active employee has to be assigned to an area.'};
} else {
  // if no validation error has occurred an error set before has to be reset
  model.area.error = null;
}
...

scope.objects

BPM Start Form

When manually starting a process, you may have to set fields, depending on the index data of the objects for which you are starting the process. Using scope.objects, the object data can be accessed for reading, as shown in the following examples.

var objectcount = 0
if ( scope.objects ) {					// relevant if a process is started for one or more objects
	objectcount = scope.objects.length
}
scope.model.objectcount.value = objectcount;

if ( objectcount == 1) {
	scope.model.dmsdataboolean.value = false;
	scope.model.dmstype.value = scope.objects[0].type;
	scope.model.dmsid.value = scope.objects[0].id;    // this and the following are object attributes of type string
	scope.model.dmstitle.value = scope.objects[0].title;
	scope.model.dmsdescription.value = scope.objects[0].label;
	scope.model.dmscreator.value = scope.objects[0].created.by.title;
	scope.model.dmsmodifier.value = scope.objects[0].modified.by.title;
	scope.model.dmcreated.value = scope.objects[0].created.on;        // type ISO-String
	scope.model.dmsmodified.value = scope.objects[0].modified.on;     // type ISO-String
	if (scope.model.dmstype.value = 'myobjecttype') {
		scope.model.dmsdataname.value = scope.objects[0].data.myname;           // this and the following are object index fields
		scope.model.dmsdatadate.value = scope.objects[0].data.mydate;
		scope.model.dmsdatadatetime.value = scope.objects[0].data.mydatetime;
		scope.model.dmsdatainteger.value = scope.objects[0].data.myinteger;
		scope.model.dmsdatadecimal.value = scope.objects[0].data.mydecimal;
		scope.model.dmsdatacatalog1.value = scope.objects[0].data.mycatalog;
		scope.model.dmsdatausergroup.value = scope.objects[0].data.myusergroup;
		scope.model.dmsdataboolean.value = scope.objects[0].datamy.boolean
	} 
}

// usually, all form fields (scope.model) mapped to object field data are set to read-only by form design. The following fields are controlled by a Boolean field of the first object:  

if ( scope.model.dmsdataboolean.value == true ) {
	scope.model.value1.required = true;
	scope.model.value3.readonly = true
} else {
	scope.model.value1.readonly = true;
	scope.model.value3.required = true
}

And the result looks like this:

The scope.objects[i] offers the following attributes:

ObjectAttributeSub-attributeSub-attributeTypeDescription
scope.objects[i]id

stringID of the object

title

stringTitle of the object

description

stringDescription of the object

hascontent

booleanIs TRUE if a content file exists

iscontextfolder

booleanIs TRUE if the object is of type context folder

isactiveversion

booleanIs TRUE if the object version is the active one

typeid
stringID of the object type


name
stringTechnical name of the object type


label
stringDescription of the object type

createdbyidstringID of the object creator



titlestring'Lastname, firstname' of creator



namestringLogin name of creator


on
dateDate of creation

modifiedbyidstringID of the object's last editor



titlestring'Lastname, firstname' of creator



namestringLogin name of creator


on
dateDate of last modification

data<technical name>
anyTechnical names of all index data fields


BPM Task Form

The following object attributes of the process file can be accessed and read using the scope.objects array.

scope.model.dmsaddtime.value = scope.objects[0].addtime,		// time at which the object was added to the process file; example: 1521534418927
scope.model.dmscreator.value = scope.objects[0].creator;		// string, login name of the user who added the object; example: 'johnson'
scope.model.dmstype.value = scope.objects[0].type;			// string, type of added object as configured in yuuvis® RAD designer; example: 'invoice'
scope.model.dmstitle.value = scope.objects[0].title;			// string, title format of the added object as configured in yuuvis® RAD designer; example: 'Invoice-no: 4711'
scope.model.dmsdescription.value = scope.objects[0].description;	// string, description format of the added object as configured in yuuvis® RAD designer; example: 'Delivered by: MyCompany'
scope.model.dmsid.value = scope.objects[0].id;					// string with the added object ID; example: '4AAB835352294DE0B359A05160B6C1BC'
scope.model.dmsiconid.value = scope.objects[0].iconid;		// string with the icon ID of added object; example: '8ED1E037E4D7468AB69EB8A04C2EA080'

Note: If you need the DMS object of this ID (scope.objects[0].id), you have to request the REST-WS endpoint ../dms/{id}.

scope.api

This section describes all the API functions that can be used for client plugin development.

scope.context

The scope.context offers the following attributes for objects where a context folder is available:

NameTypeDescription
idStringThe ID of the context object.
titleStringThe title of the context object.
typeNameStringThe name of the type of the context object.


Using Global Scripts

Form scripts are defined by type and situation. The scripts are independent of each other and cannot share functionality. If you need to use generally applicable functions or the project-specific logic of multiple form scripts, you can add global scripts and include them in local scripts.

Creating Global Scripts in yuuvis® RAD designer

You can add new scripts in yuuvis® RAD designer, in the Scripts section. Give the scripts a technical name.

In a global script, you can define which functions or values are publicly available. The result returns a Javascript object with 'exports'.

This is a short global sample script:

Small global script
return {
	exports : {
		markRequired: function(element) { element.required=true; }
	}
}


The script above provides a simple function which marks a single element as a required field. This script is stored under the name 'FormUtils'.

Note about creating global scripts

When a global script provides many functions, it becomes difficult to keep an overview of the 'exports'. Therefore, we recommend to keep function declarations separate from the implementation. Example

return {
	exports : {
		markRequired: markAsRequired
	}
}
function markAsRequired(element) { element.required=true; }

The "markAsRequired" function is exported as "markRequired".

Functions not listed in the exports block can only be used within the global script.

Using Global Scripts in Form Scripts

Global scripts are stored in the system using the unique technical name assigned in yuuvis® RAD designer. A form script which uses the 'FormUtils' script above, can then be written as follows.

Form script using a global script
return {
	uses : ['FormUtils'],
	init: function(util) {
		util.markRequired(scope.model.vorname);
    }
}

With 'uses', the form script defines which global scripts should be used. The technical name of the global script is used here. When multiple global scripts are used, you can use a list of scripts as a string array. Exports from global scripts are passed as parameters, injected in the init function defined in the script, and can be used there. The end result of this example is that the field named 'vorname' is marked as a required field.

Info

You can achieve the same result for this simple case with the following script.

scope.model.vorname.required=true;

This is just a short example. Scripts that are used multiple times typically have many and more complex functions. You can find a more comprehensive script in the sample scripts below.

Sample Global Scripts

Using General Help Functions in Form Scripts

This script is saved as a general (global) script under the name 'Utilities'.

Example: Global Form Utilities
//
// Example: Global Form Utilities
//
return {
    // Here we define the public exports. The exports are available to users of this global script.
    // You could implement the functions here, but instead we only declare them.
    // This is for readability and also adheres to the John Papa Style Guide for angular factories. See also https://github.com/johnpapa/angular-styleguide#style-y052
    exports : {
        toUpper : toUpper,
        toLower : toLower,
        forEachElement : forEachElement,
        hasAlias : hasAlias,
        onChangeOf : onChangeOf
    }
}
// Element value toUpperCase
function toUpper(el) {
    if( el.type==='STRING' && el.value) {
        el.value=el.value.toUpperCase();
    }
}
// Element value toLowerCase
function toLower(el) {
    if( el.type==='STRING' && el.value) {
        el.value=el.value.toLowerCase();
    }
}
// Goes through each element and calls the callback - optional filter
function forEachElement(callback, filter) {
    _.forEach(scope.model, function(el) {
        if( filter ) {
            if( filter(el) ) {
                callback(el);
            }
        } else {
            callback(el);
        }
    });
}
// Checks whether an alias exists
function hasAlias(el, searchalias) {
    var aliasFound = false;
    if( el.aliases ) {
        _.forEach(el.aliases, function(el) {
            if (el.indexOf(searchalias)==0) {
                aliasFound=true;
            }
        });
    }
    return aliasFound;
}
// Sets an onchange handler for an array of elements
function onChangeOf(elements, changecallback) {
    _.forEach(elements, function(element) {
        element.onchange = changecallback;
    });
}

Calculation Function Example

This script is saved with the name 'Calculator'.

Example: Simple calculations
//
// Example: Simple calculations
//
return {
    exports : {
        sum : buildSum,
        mul : buildMul
    }
}
function buildSum(elements) {
    var sum = 0;
    _.forEach(elements, function(el) {
        sum += el.value;
    });
    return sum;
}
function buildMul(elements) {
    if( !elements || elements.length==0 ) {
        return 0;
    }
     
    var sum = 1;
    _.forEach(elements, function(el) {
        sum *= el.value;
    });
    return sum;
}

Using Global Scripts

The following form script uses the global scripts defined above. This script could be assigned to a form in the EDIT situation.

Example: Form script
return {
    uses : ['Utilities','Calculator'],
    init: function(util,calc) {
         
        // We track the model
        var m = scope.model;
         
        // Set some test values
        m.name.value='Feuerstein';
        m.vorname.value='Herbert';
         
        // Example: All values UPPPERCASE
        util.forEachElement(util.toUpper);
         
        // Example: Set certain fields with certain aliases to read-only.
        util.forEachElement(
            function(el) { el.readonly=true},                                   // This is the executed action.
            function(el) { return util.hasAlias( el, 'extract.OS:Title') }      // Filter: true when the alias exists.
        );
         
        // Example: If last or first name changes, change title.
        util.onChangeOf([m.name,m.vorname], updateTitle);
         
        // Example: Calculation with help function from the global script
        var sizeElements=[m.width,m.height];
        util.onChangeOf(sizeElements, updatePixel)  // If width or height changes -> updatePixel
        // Funktion: Pixel ausrechnen
        function updatePixel() {
            m.pixel.value=calc.mul(sizeElements);   // Pixel is the multiplication of the size fields.
        }
         
        // Function: Update title
        function updateTitle() {
            m.titel.value='Passfoto von "'+m.vorname.value+' '+m.name.value+'"';
        }
         
        // Execute function so that values are current.
        updateTitle();
        updatePixel();
         
    }
}


Tips & Tricks

How to Avoid Rounding Errors

Calculating with decimals can lead to rounding errors, e.g.:

Setting up a decimal with 4 decimal places, then the following does not work well:

scope.model.resultvalue.value = Math.round (scope.model.value1.value - scope.model.value2.value)

in concret numbers: (100.3900 – 100.0010) leads to 0.3900

but this:

scope.model.resultvalue.value = Math.round ((scope.model.value1.value - scope.model.value2.value) * 10000) / 10000

now it's correct: (100.3900 – 100.0010) leads to 0.3890

Therefore, use as many '0' for multiplication and division as decimal places are configured.

And: take more than one decimal place!

International Values

Using forms in an international context means setting values in the language of the current user, as shown in the following example.

var task = {				// use the two-character DIN values for languages as attribute 
	de: 'Bitte, ...',
	en: 'Please, ...',
	ru: 'Пожалуйста, ...'
}
scope.model.Task.value = task[scope.api.session.getUser().schemaLocale];      // give the value depending on the user language setting for 'Definition' = schemalocale

Value Changes With Action

When changes are made to the field data by the script (not user action) while the form is loading, no "Save" action is offered. You can also disable fields to protect them from changes by users.

Debugging Scripts

You can use a browser to debug scripts. The developer tools included in the most common browsers (Microsoft Internet Explorer, Google Chrome and Mozilla Firefox) are sufficient to inspect scripts and fix bugs.

User-defined scripts are registered under a defined name in the browser environment and can be found by the name.

Script names in browsers

Global scripts use the prefix 'Global', backslash as delimiter, the technical name of the script and the extension .js. For example, if the technical name of the script is 'MyUtilities', then the name of the global script in the browser is 'Global/MyUtilities.js'.Form scripts use the prefix 'Formscript', backslash as delimiter, the technical name of the type, the situation and the extension .js. For example, if the typename is 'invoice' and the situation is EDIT, then the name of the script in the browser is 'Formscript/invoice_EDIT.js'.

Chrome

Start the Chrome debugger by pressing F12 on your keyboard. The user-defined scripts are found under Source > no domain. For more information, refer to Debugging Javascript (external link).

Firefox

Start the Firefox debugging tools by pressing F12. The scripts are listed in Debugger > Sources. You can use Firefox's search function to find the scripts by name. For more information, refer to Debugger Tools im Firefox (external link).

Internet Explorer

Start the IE debugger by pressing F12, and then click Debugger. Since IE does not decode the script names, scripts are hidden in the source code tree view under 'dynamic scripts'. You can find them using the search function, by searching on the prefix 'Global/' to find all loaded global scripts, and 'Formscript/' to find all form scripts.

Example: Messages
var m=scope.model;
m.unlimited.onchange=function() {
	if( m.unlimited.value ) {	
		scope.api.util.notifier.success( 
			 m.firstname.value +' ' +m.familiyname.value+' is now employed without time limit.',		// Message
			'Personal information changed'   // optional: use a short title
		);
	}
}