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 the properties of the elements will change the form.

In the example below, an element has properties label, type, and placeholder defined with 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 reflect the current result of the expression. These expressions execute when the form is openned and when any dependant 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. As the form is opened, the done checkbox value is defaulted to false. The expression executes, resolves to true, and the signature is hidden.

  2. The user ticks the checkbox and its value changes to true. The expression executes again, resolves to false, and the signature is displayed.

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. As the form is opened to create a new record, quantity and price have no value. The expression executes, resolves to 0, and total gets a value of 0.

  2. The user enters a quantity of 3 and price of 5, the expression executes, resolves to 15, and total gets a value of 15.

  3. Now the user changes total to 12. It appears out of sync with the expression, but is valid. The user saves the record and leaves the form.

  4. Later the user goes back to the form to edit the record. The expression is not executed and quantity will show the saved value 12.

  5. The user enters a new quantity of 4, the expression executes, resolves to 20, and total gets a value of 20.

type, multiple, and parent Not Supported

type, multiple, and parent are static properties to ensure server and client data match when syncing. Instead show and hide different elements of the required types.

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 at least one. Only the referenced element properties are monitored for changes to execute the expression.

  2. Must be pure.

    An expression with the same inputs must always output the same value. An expression must not change the values of other element properties, interact with the DOM, or perform asyncronous operations 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 must return data of the correct type or unexpected results will occur.

  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 to keep things simple, so they must be valid. For example, do not use hyphenated names, 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 not 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 readable, perform well, and be testable. Divide complex expressions into smaller units that can be developed, reused, and debugged separately.

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/NextService/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/NextService/utils/topography.js"
      "topodata": "/SuiteScripts/NextService/regional-topography.json"
   }
} 

          

Debugging

Use the browser Developer Tools when troubleshooting expressions.

Logs

Expressions produce Console logs when they initialize, update a property, or have errors. Logs can be enabled 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 are vulnerable to the inherant nuances of JavaScript in a 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 will not redownload unless an import id changes.

General Notices