Render content dynamically in mobile extensions
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": {....}
}
}
}
Important
If you have some pages that will always be rendered (fixed), then using this mechanism requires that you to put the fixed components in as well.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:
- 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
}
}
}
- 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
}
}
}
- Define the UI in the UI definition file.
// UI def
{
"content": {
"type": "dynamicContent",
"contentDataExpression": "sharedData.__dynamic.dynamicUIContent"
},
"extras": {
"localesDataExpression": "sharedData.__dynamic.dynamicLocalization"
}
}
Result
Note
-
This form does not use custom save; it uses the default save functionality.
-
Though the example shows just one page being dynamic, this capability allows you to make multiple pages that can be chained together.
-
This form is for demonstration purposes and the code should not be used as is for production use cases.
Feedback
Was this page helpful?