Render content dynamically in mobile extensions

How to use dynamic content for pages or components.

Overview

Dynamic Content is a mechanism that allows mobile extention developers to generate the UI Definition dynamically based on context preferences.

This can be useful when a form needs to display custom fields dynamically, based on some special settings.

Another example when the Dynamic Content component is useful, is when organizations want to show different layouts of mobile forms for different groups of users.

The way the dynamic content works is based on the data returned from the server. This is for offline and online mode.

Before working with dynamic content in your extension, make sure you are familiar with the following files and concepts:

When the UI Definition is defined for a Form, it is static. The instanceData and staticData is dynamic based on what you defined to be returned from the server.

Therefore, the dynamic content mechanism is based on the dynamic mechanism of the instanceData and staticData (online mode data as well) to return the corresponding dynamic content.

Dynamic content at the page level

Dynamic content at the page level allows developers to create dynamic page content based on the data being returned. This allows for the creation of numerous pages, dynamically, based on the instanceData or staticData.

To define the use of dynamic content on a page level, do the following:

// Root of ui_def
{
  ...
  "content": {
    "type": "dynamicContent",
    "contentDataExpression": DataExpression
  },
  "extras": {
    "localesDataExpression": DataExpression
  }
}

The content type must be defined as dynamicContent and the location of the dynamic content must be specified in the contentDataExpression.

Put any extra localization inside extras.localesDataExpression if your locale files do not yet contain the localized keys for the dynamic content.

When the user opens the mobile extension, it will start getting the dynamic content as defined in contentDataExpression, and then render it as normal.

The data from the instanceData/staticData should look something like this:

// instanceData/staticData
{
  "__dynamic": {
    "dynamicLocales": {
      "en": { .... }
    },
    "dynamicUIContent": {
      "page1": {....},
      "page2": {....}
    }
  }
}

Dynamic content at the component level

Dynamic content at the component level allows mobile extension developers to create dynamic component content based on the data returned from the server. This allows for the creation of numerous components, dynamically, based on the instanceData or staticData.

To define the use of dynamic content on a component level, do the following:

// Root of a page
{
  ...
  "items": [
  ..., /* Others fixed items */
  {
    "type": "dynamicContent",
    "contentDataExpression": DataExpression
  }]
}

You will need to define the component type as dynamicContent and the location of the dynamic content must be specified in the contentDataExpression. If developers want to include complex fields like look-ups or picklist/multi-picklist, they may need to handle the options separately to make sure that the dynamic component can show the right options.

Put any extra localization inside extras.localesDataExpression if your locale files do not yet contain the localized keys for the dynamic content.

When the user opens the mobile extension, it will start getting the dynamic content as defined in contentDataExpression, and then render it as normal.

The data from the instanceData/staticData should look something like this:

// instanceData/staticData
{
  "__dynamic": {
    "dynamicLocales": {
      "en": { .... }
    },
    "dynamicUIContent": [
      {...} // Component 1,
      {...} // Component 2,
    ]
  }
}

Dynamic locales

When using dynamic content, it is mandatory to include the locale keys. These are required for all the components. Because the content is dynamic, so the locales in this case has to be dynamic as well. The dynamic locales are returned in the same way as with the UI; that is, returned from the instance or static data.

To define extra locales to be consumed, do the following:

// Root of UI Def
{
  "extras": {
    "localesDataExpression": DataExpression
  } 

No matter how many dynamicContent you return, there must always be dynamic locales content defined here. All dynamic locales must be in one single object.

Online mode

For online mode, because we don’t support instanceFetch as a default approach, the form properties (formDataExpression) must be used to fetch the data for dynamic content.

The formDataExpression is always triggered when a user first opens a form. The dynamic content will start rendering after the formDataExpression has finished running.

Example

Consider a scenario where a developer wants to build a mobile form that shows all custom fields from a custom object.

To create a mobile form that displays custom fields from a custom object, do the following steps:

  1. Prepare InstanceData / StaticData.

As mentioned above, the dynamicContent is based on instanceData / staticData, so you will need to use a custom function to fetch the data.

In this form, we will build the shared data for dynamic fields as:

// staticFetcher.ts
import { CustomStaticFetchHandler} from "@skedulo/mex-service-libs/dist/types/inner-function";
import {CustomFunctionStatus, ObjectDataResult} from "@skedulo/mex-service-libs/dist/types/mex-custom-function";

export const fetchMexStaticData: CustomStaticFetchHandler = async (input) => {
    const baseAPI = input.services.BaseAPIService
    const schemaResponse = await baseAPI.get("/custom/metadata/ShowCaseObj__c")

    return {
        status: CustomFunctionStatus.SUCCESS,
        data: {
            "__dynamic": buildFlexibleData(schemaResponse.data.result.fields) as ObjectDataResult
        }
    }
}

const buildFlexibleData = (fields: any[]): any => {
    let dynamicLocalization: any = {}
    let dynamicItems: any[] = []

    const genRequiredValidator = (pageDataKey: string) => {
        return {
            "type": "expression",
            "expression": pageDataKey,
            "errorMessage": "FieldIsRequired"
        }
    }

    let dynamicUIContent: any = {
        "firstPage": "DynamicShowCase",
        "pages": {
            "DynamicShowCase": {
                "type": "flat",
                "header": {
                    "title": "Title",
                    "description": "Description",
                },
                "pageDataExpression": "formData.ShowCaseObject",
                "defaultPageData": {
                    "data": {},
                    "objectName": "ShowCaseObject"
                },
                "upsert": {
                    "defaultDataForNewObject": {
                    },
                    "validator": []
                }
            }
        }
    }

    for(let i = 0; i < fields.length; i++) {
        const field = fields[i]

        let titleKey = `${field.name}_Title`
        let placeholderKey = `${field.name}_Title`
        let pageDataKey = `pageData.${field.name}`

        dynamicLocalization[titleKey] = field.label
        dynamicLocalization[placeholderKey] = field.label

        const isNil = field.nillable;

        if (field.type === 'textarea' || field.type === 'string') {
            dynamicItems.push(
                {
                    "readonly": field.readOnly,
                    "type": "textEditor",
                    "valueExpression": pageDataKey,
                    "title": titleKey,
                    "placeholder": titleKey,
                    "multiline": field.type === 'textarea',
                    "mandatory": !isNil,
                    "validator": isNil ? undefined : genRequiredValidator(pageDataKey)
                },
            )
        }
        else if (field.type == 'double') {
            dynamicItems.push(
                {
                    "type": "textEditor",
                    "valueExpression": pageDataKey,
                    "title": titleKey,
                    "placeholder": titleKey,
                    "keyboardType": "decimal-pad",
                    "dataType": field.scale > 0 ? "decimal" : "integer",
                    "mandatory": !isNil,
                    "validators": isNil ? undefined : genRequiredValidator(pageDataKey)
                },
            )
        }
    }

    dynamicUIContent.pages["DynamicShowCase"].items = dynamicItems

    return {
        dynamicUIContent,
        dynamicLocalization: {
            "en": dynamicLocalization
        }
    }
}
  1. Fetch dynamic data.

The above code is used to generate dynamic component based on dynamic schema (everyone will be the same). However, because we want the user to be able to view and edit old records, so we need to fetch dynamicData as well.

// fetcher.ts
import {
    CustomFetchHandler,
} from '@skedulo/mex-service-libs/dist/types/inner-function'
import {CustomFunctionStatus} from "@skedulo/mex-service-libs/dist/types/mex-custom-function";

const baseShowCaseObjectQuery= "query fetchShowCaseObject($filter: EQLQueryFilterShowCaseObject!) { showCaseObject(filter: $filter) { edges { node { UID $__FIELDS__$ } } } }"

export const fetchMexData: CustomFetchHandler = async (input) => {

    const baseAPI = input.services.BaseAPIService
    const schemaResponse = await baseAPI.get("/custom/metadata/ShowCaseObj__c")

    const supportedTypes = ["textarea", "string", "double"]

    const fieldsToFetch = schemaResponse.data.result.fields
        .filter((field: any) => {
            return supportedTypes.filter(st => field.type == st).length > 0
        })
        .map((field:any) => field.name)
        .join(" ")

    const query = baseShowCaseObjectQuery
        .replace("$__FIELDS__$", fieldsToFetch)

    const data = await input.services.GraphQLService.executeQuery<any, any>(query, { "filter": `JobId == \"${input.contextObjectId}\"` })

    const objects = data.showCaseObject.edges.map((edge:any) => edge.node)

    let singleObject: any|undefined =  { "__typename": "ShowCaseObject" }

    if (objects.length == 0) {
        singleObject = null
    }

    else if (objects.length > 0) {
        singleObject = { ...singleObject, ...objects[0]}
    }


    return {
        status: CustomFunctionStatus.SUCCESS,
        data: {
            "ShowCaseObject": singleObject
        }
    }
}
  1. Define the UI in the UI definition file.
// UI def
{
  "content": {
    "type": "dynamicContent",
    "contentDataExpression": "sharedData.__dynamic.dynamicUIContent"
  },
  "extras": {
    "localesDataExpression": "sharedData.__dynamic.dynamicLocalization"
  }
}

Result

mobile form with dynamic content