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:

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"
   }
} 

          
  1. When the form opens, the done checkbox value defaults to false. The expression runs, returns true, and the signature is hidden.

  2. 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)"
   }
} 

          
  1. When you open the form to create a new record, quantity and price have no value. The expression runs, returns 0, and total is set to 0.

  2. When you enter a quantity of 3 and price of 5, the expression runs, returns 15, and total is set to 15.

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

  4. Later, if you return to the form to edit the record, the expression doesn't run and quantity shows the saved value 12.

  5. When you enter a new quantity of 4, the expression runs, returns 20, and total 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:

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

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

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

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

General Notices