The pattern: an empty child, filled from the parent's onChange
In a Workday Extend PMD, a list depends on another list through the parent's onChange event. When the user picks a value in the parent, the handler calls a deferred endpoint with the selected ID and loads the result into the child with setValues. The child starts with an empty list, "values": "<% [] %>", because Workday requires list values to be initialised when the page loads.
Here is the documented pattern, with a Supervisory Organization list driving a list of its members:
{
"type": "instanceList",
"id": "orgList",
"label": "Supervisory Organization",
"values": "<% supervisoryOrgs.data %>",
"onChange": "<% loadMembers(self.value) %>"
},
{
"type": "instanceList",
"id": "memberList",
"label": "Worker",
"values": "<% [] %>"
}
"script": "<%
var loadMembers = function(selected) {
if (!(empty selected)) {
var listData = supervisoryOrgMembers.invoke({'id': selected[0]});
memberList.setValues(listData.data.map(x => {{'id': x.id, 'descriptor': x.descriptor}}));
} else {
memberList.setValues([]);
}
};
%>"
"endPoints": [
{
"name": "supervisoryOrgs",
"baseUrlType": "workday-staffing",
"url": "<% '/supervisoryOrganizations' %>",
"authType": "sso"
},
{
"name": "supervisoryOrgMembers",
"baseUrlType": "workday-staffing",
"url": "<% '/supervisoryOrganizations/' + id + '/members' %>",
"authType": "sso",
"deferred": true
}
]
Four details make this work:
deferred: truekeeps the member endpoint from running at page load. The values passed toinvoke({'id': ...})become variables in its URL. Withoutdeferred, the page would call the endpoint before any organization was chosen.self.valueis a list. AninstanceListvalue is always a list of IDs, even with one selection, so passselected[0].setValuestakesidanddescriptor. Map the endpoint's rows to those two fields, or setidKeyanddisplayKeyon the child to use other field names.- Handle the empty case. When the user clears the parent, empty the child with
setValues([]), so it can't keep options from the previous choice.
The same onChange and setValues pair works on dropdown, radioGroup and checkBoxList, which all have a setValues(List dataSource) method. Use instanceList when the values are Workday or app records. Use dropdown when they are a short list you control. See which Extend widget to use for what.
Reset the child's selection when the parent changes
Replacing a child's options with setValues loads new choices, but a user who had already picked a member of the old organization shouldn't keep that pick. Set the child's selection explicitly with setValue: pass an empty list to leave nothing selected, or a list of IDs, which must be among the new values, to set a default:
memberList.setValues(newMembers);
memberList.setValue([]);
Keep the two methods apart. setValues replaces all the possible values. setValue sets which of them are selected. The selectedValues attribute only applies when the page loads, so in an event handler always use setValue. For more on setting selections, see selectedValues on an Extend instanceList.
Three or more levels
Chain the handlers so each level loads the next and clears everything below it. For Company, then Cost Center, then Project:
"onChange": "<%
costCenterList.setValues(empty self.value ? [] : costCentersFor.invoke({'company': self.value[0]}).data);
costCenterList.setValue([]);
projectList.setValues([]);
projectList.setValue([]);
%>"
Give the Cost Center list its own onChange that loads projectList the same way. Clearing every lower level on each change is what stops a page from submitting a Project that belongs to a different Company than the one now shown. In a grid, back this up with a check at submit time, as in grid validation.
One prompt that drills down: multilevel lists
When the user really picks one leaf value, such as a worker inside an organization, a multilevel instanceList gives a folder-style prompt instead of two widgets. Define the root values on the list and each sublevel in monikerLevels:
{
"type": "instanceList",
"id": "orgMemberPicker",
"label": "Worker (by organization)",
"values": "<% supervisoryOrganizations.data %>",
"monikerLevels": [
{
"type": "monikerLevel",
"levelValues": "<% organizationMembers.data %>",
"endPoint": "<% endpoints.organizationMembers %>"
}
]
}
The sublevel's deferred endpoint reads the parent selection from promptSelections:
{
"name": "organizationMembers",
"baseUrlType": "workday-staffing",
"authType": "sso",
"url": "<% '/supervisoryOrganizations/' + orgMemberPicker.promptSelections[0] + '/members' %>",
"deferred": "true"
}
promptSelections[0] is the ID chosen at the root, promptSelections[1] at the second level, and so on. Only leaf values become the widget's selected value. Use monikerLevels when every leaf sits at the same depth, and dynamicMonikerLevels when leaves sit at different depths. monikerLevel works only on edit pages.
Choose the two-widget pattern when both choices must be stored, such as Company and Cost Center as separate fields. Choose the multilevel list when only the leaf is stored and the levels just help the user find it.
Cascading lists inside a grid or panelList
Inside a grid or panelList, each row needs its own child options. Use onSearch on the child for this. Workday documents that onSearch knows which row or panel triggered it, so it can read the parent cell on the same row:
{
"type": "instanceList",
"id": "workerCell",
"values": "<% [] %>",
"onSearch": "<%
const type = workerTypeCell.selectedEntries[0].descriptor;
const q = event.query + ' ' + type;
workersMatching.invoke({'q': q}).data ?? [];
%>"
}
event.query holds what the user typed. The handler must return a list with id and descriptor (or the idKey and displayKey fields). onSearch, searchEndPoint and searchValues are mutually exclusive on one list.
Keep it fast
Every invoke() is an endpoint call inside the user's interaction. Each endpoint has 24 seconds, and the whole request has 60 seconds. Point deferred endpoints at filtered sources, such as the organization's members subresource, rather than loading everything and filtering in script. Workday's best practices recommend filtering with endpoint query parameters over PMD Scripting filters, to save memory and time. For very large child lists, use onSearch so the user's typed text narrows the request.
Test it
On the deployed page, signed in as a user with the role that will use it, pick a parent and confirm the child shows only matching values. Change the parent and confirm the child's selection is cleared. Clear the parent and confirm the child empties. Then submit and read the saved record back to check that the stored child ID belongs to the stored parent. Run the test as the real role, because the deferred endpoints run with that user's security (authType sso). A list that is full for an administrator can be empty for a manager.