Dynamic Forms
Dynamic Forms are forms that change based on user input. They adapt to more effectively display or capture information. A form may need to:
-
Show or require new inputs when a box is checked.
-
Change options in a select based on a value entered into a text field.
-
Calculate a new value from a set of number fields.
Form layout and behavior is defined by a set of elements. Changing an element's properties will change the form.
In the example below, an element's label
, type
, and placeholder
properties have static values.
{
"actions": {
"label": "Actions Taken",
"type": "textarea",
"placeholder": "Enter details here..."
}
}
Defining properties as an Expression allows their values to change dynamically.
Using Expressions
Expressions are JavaScript formulas that resolve to a value. They tell a property what its value should be at any point in time.
Standard expression behavior
Most properties always show the latest value from their expression. These expressions run when the form opens and whenever any related element properties change.
In the example below, the sign
element uses the simple expression !done.value
to define when it is hidden.
{
"done": {
"label": "Is all work complete?",
"type": "checkbox",
"value": false
},
"sign": {
"label": "Customer Signature",
"type": "signature",
"hidden": "!done.value"
}
}
-
When the form opens, the
done
checkbox value defaults to false. The expression runs, returns true, and the signature is hidden. -
When the user checks the checkbox and its value changes to true, the expression runs again, returns false, and the signature appears.
value Expression Behavior
value is a special property because users may change it. It may not always reflect the current result of the expression. These expressions do not execute on form load for an existing record.
In the example below, the total element uses the expression (quantity.value || 0) * (price.value || 0)
to define a calculated value.
{
"quantity": {
"label": "Quantity",
"type": "number"
},
"price": {
"label": "Price",
"type": "number",
},
"total": {
"label": "Total",
"type": "number",
"value": "(quantity.value || 0) * (price.value || 0)"
}
}
-
When you open the form to create a new record,
quantity
andprice
have no value. The expression runs, returns 0, andtotal
is set to 0. -
When you enter a
quantity
of 3 andprice
of 5, the expression runs, returns 15, andtotal
is set to 15. -
Now, if you change
total
to 12, it looks out of sync with the expression, but it's valid. You save and leave the form. -
Later, if you return to the form to edit the record, the expression doesn't run and
quantity
shows the saved value 12. -
When you enter a new
quantity
of 4, the expression runs, returns 20, andtotal
is set to 20.
type, multiple, and parent Not Supported
type
, multiple
, and parent
are static properties to make sure server and client data match when syncing. Instead, show and hide different elements as needed.
Rules for Expressions
Expressions concisely define complex forms by putting a few ground rules in place:
-
Reference at least one element property.
You can access as many element properties as you want, but you need to use at least one. The expression only monitors the element properties you reference.
-
Must be pure.
An expression with the same inputs must always output the same value. An expression shouldn't change other element properties, interact with the DOM, or perform asynchronous actions like http requests.
-
Return values in the data type of the property.
All element properties have an expected data type. For example,
disabled
expects a Boolean. Expressions need to return the right data type or you'll get unexpected results. -
Use valid variable names for elements and properties.
At runtime, expressions are plain JavaScript functions. Element and property variable names are used as is, so make sure they're valid, for instance, don't use hyphens, use underscores instead.
What is in scope?
Expressions have access to all elements on the current form as plain objects. An expression in a table form only has access to the elements of that row. An expression outside a table form can't access the table's rows or columns.
Unique elements
User resource elements can be made available to all expressions by defining a URI, or unique resource identifier.
In the example below, an element with a uri
of ppe
is added to a user resource and contains data from a custom record search.
{
"ppe": {
"uri": "ppe",
"type": "datalist",
"options": {
"record": "customrecord_ppe",
"map": {
"id": "internalid",
"label": "name"
}
}
}
}
An element on a JSA uses the expression ppe.options
to assign options to ppeused
.
{
"ppeused": {
"label": "PPE Used",
"type": "select",
"multiple": true,
"options": "ppe.options"
}
}
this Keyword
An expression can refer to its own element using the this keyword instead of its name.
In the example below, the temp
element uses the expression this.value > 100 ? 'red' : ''
to apply a red style based on its own value.
{
"temp": {
"label": "Temperature",
"class": "this.value > 100 ? 'red' : ''",
"type": "number",
}
}
Global Scope
Expressions have access to everything in the Global Scope of the Browser.
In the example below, the speed
element uses the expression Math.round((distance.value || 0)/(time.value || 0))
to apply the round
function from the built-in object Math
to calculate a value.
{
"distance": {
"label": "Distance Travelled (m)",
"type": "number"
},
"time": {
"label": "Elapsed Time (s)",
"type": "number",
},
"speed": {
"label": "Average Speed (m/s)",
"type": "number",
"value": "Math.round((distance.value || 0)/(time.value || 0))",
"readonly": true
}
}
Advanced Techniques
Expressions should be clear, fast, and easy to test. Break up complex expressions into smaller, reusable, and testable parts.
Properties
Elements can have arbitrary properties with values or expressions.
In the example below, the weight
element has target
and tolerance
properties that store static values. The pass
property calculates a value other elements reference using the expression weight.pass
instead of repeating the calculation.
{
"weight": {
"label": "Weight",
"type": "number",
"target": 100,
"tolerance": 0.1,
"pass": "Math.abs(this.target - (this.value || 0)) < this.tolerance"
},
"fail1": {
"label": "Fail Reading 1",
"type": "number",
"hidden": "weight.pass"
},
"fail2": {
"label": "Fail Reading 2",
"type": "number",
"hidden": "weight.pass"
},
"fail3": {
"label": "Fail Reading 3",
"type": "number",
"hidden": "weight.pass"
}
}
Functions
Pure functions can be used to abstract and reuse logic. Include functions by registering one or more JavaScript files from the file cabinet in the import
Mobile configuration option.
In the example below, the distance
element uses an expression to call the haversine
function and calculate the distance between 2 points.
{
"latitude": {
"label": "Latitude",
"type": "number"
},
"longitude": {
"label": "Longitude",
"type": "number"
},
"distance": {
"label": "Distance",
"type": "number",
"readonly" true,
"latitude": -37.8541542,
"longitude": 145.1040064,
"value": "haversine(this.latitude, this.longitude, latitude.value, longitude.value)"
}
}
The functions are developed, tested, and version controlled in a JavaScript file named haversine.js
.
function isNumber(value) {
return !isNaN(parseFloat(value))
}
function toRadian(degree) {
return degree * Math.PI/180
}
function haversine(lat1, lon1, lat2, lon2) {
if (isNumber(lat1) && isNumber(lon1) && isNumber(lat2) && isNumber(lon2)) {
return 6362.4098345775 * (Math.acos(Math.sin(toRadian(lat1)) * Math.sin(toRadian(lat2)) + Math.cos(toRadian(lat1)) * Math.cos(toRadian(lat2)) * Math.cos(toRadian(lon1-lon2))) || 0)
}
}
The completed script is uploaded to the File Cabinet and registered as an import
with an unique ID.
{
"import": {
"customfunctions": "/SuiteScripts/FieldService/haversine.js"
}
}
Data Sets
Static JSON data can be used in expressions. Import data by registering one or more JSON files from the File Cabinet in the import
Mobile configuration option. The data will be available as Global variables named by their import id.
In the example below, the elevation
element uses an expression to pass topography data, imported as the topodata
global variable, to a function.
{
"elevation": {
"label": "Elevation",
"type": "number",
"readonly" true,
"latitude": -37.8541542,
"longitude": 145.1040064,
"value": "getElevation(topodata, this.latitude, this.longitude)"
}
}
The function calculates and returns the elevation at a coordinate and is imported in a JavaScript file named topography.js
.
function getElevation(topography, latitude, latitude) {
// Pure function with complex logic
}
The data is uploaded to the File Cabinet as a JSON file named regional-topography.json
and registered as an import
with the unique ID topodata
.
{
"import": {
"topomath": "/SuiteScripts/FieldService/utils/topography.js"
"topodata": "/SuiteScripts/FieldService/regional-topography.json"
}
}
Debugging
Use the browser Developer Tools when troubleshooting expressions.
Logs
Expressions write logs to the Console when they initialize, update a property, or have errors. You can turn logs on by setting the Mobile configuration log
option to true.
In the example below, the value
expression has an invalid operator x
. On form load, the expression logs its dependent properties. It fails to initialize and logs a syntax error.
{
"width": {
"label": "Width",
"type": "number"
},
"height": {
"label": "Height",
"type": "number",
},
"area": {
"label": "Area",
"type": "number",
"readonly": true,
"value": "(width.value || 0) x (height.value || 0)"
}
}
BIND INIT area value { width: ["value"], height: ["value"] }
BIND PARSE ERROR area value SyntaxError: Unexpected identifier
When using the expression below, it instead has a bad variable name HEIGHT
. It initializes, but fails when first executed and logs a reference error.
(width.value || 0) * (HEIGHT.value || 0)
BIND INIT area value { width: ["value"], HEIGHT: ["value"] }
BIND RUNTIME ERROR area value ReferenceError: HEIGHT is not defined
BIND SET area value undefined
When using the valid expression below, it initializes, executes, and logs the resulting value.
(width.value || 0) * (height.value || 0)
BIND INIT area value { width: ["value"], height: ["value"] }
BIND SET area value 0
Pitfalls
Expressions can be affected by the quirks of JavaScript in the browser.
-
It is possible to create infinite loops by having expressions that depend on eachother. Ensure dependency loops have an exit condition.
-
Float math can have unexpected results as all JavaScript numbers are IEEE 754 floating point numbers.
-
It is possible to have conflicts with already used names in the Global Scope.
-
Built in objects and functions may not behave the same across all browsers.
-
Imported script files are cached by the app and won't redownload unless the import ID changes.