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
donecheckbox 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,
quantityandpricehave no value. The expression runs, returns 0, andtotalis set to 0. -
When you enter a
quantityof 3 andpriceof 5, the expression runs, returns 15, andtotalis set to 15. -
Now, if you change
totalto 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
quantityshows the saved value 12. -
When you enter a new
quantityof 4, the expression runs, returns 20, andtotalis 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,
disabledexpects 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.