'Elsa Workflow - How to implement cascade dropdown
I have two dropdowns in an activity. One of them it's populated dynamically from OptionsProvider attribute.
I would to like to populate a second dropdown from the first one.
How to get that?
Regards, Gustavo
Solution 1:[1]
To achieve this, you need to write a designer plugin that basically does the following:
- When the activity editor appears, get the select value of the first dropdown.
- Based on the value of the first dropdown, get a list of items for the second dropdown. You did not specify from where you want to get these items, so I will assume you are getting these from an API endpoint.
- Handle the "change" event of the first dropdown (and repeat step 2).
- When the activity editor appears, you will want to initialize the second dropdown with the value as last selected. This information is stored in the activity.
As an example, I updated the "vehicle activity" sample found https://github.com/elsa-workflows/elsa-core/blob/master/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs
.
The sample activity demonstrates how one might implement cascading dropdown behavior by letting the user first select the car Brand, then pick a Model from the second dropdown.
Here's what that looks like in action:
The activity's code is straightforward enough, so I will only show its two properties that are of interest:
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
OptionsProvider = typeof(VehicleActivity),
DefaultSyntax = SyntaxNames.Literal,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? Brand { get; set; }
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
DefaultSyntax = SyntaxNames.Literal,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? Model { get; set; }
The most important aspect is the UIHint
that is set to ActivityInputUIHints.Dropdown
- which tells the designer to render the input editor a dropdown.
The next step is to create & install a plugin for the designer. Ideally, you already have a StencilJS project in which you encapsulate the designer, but we can do it with just JavaScript directly in your HTML page too. The following snippet shows a scaffold of a plugin and how to install it into the designer:
// A plugin is just a function that receives access to `elsaStudio` services.
function VehicleActivityPlugin(elsaStudio) { }
// To install the plugin, get a reference to the <elsa-studio-root> element:
const elsaStudioRoot = document.querySelector('elsa-studio-root');
// Then install the plugin during the 'initializing' event:
elsaStudioRoot.addEventListener('initializing', e => {
const elsa = e.detail;
elsa.pluginManager.registerPlugin(VehicleActivityPlugin);
}
Now it's up to your plugin to find the appropriate elements when the activity editor is displayed for the custom activity.
For example:
function VehicleActivityPlugin(elsaStudio) {
// Get access to the eventBus to observe events and httpClientFactory to make API calls.
const {eventBus, httpClientFactory} = elsaStudio;
// When the activity editor is opened, setup an event handler on the Brands dropdown list.
eventBus.on('activity-editor-appearing', async e => {
// We are only interested in our custom activity being editor.
if (e.activityDescriptor.type !== 'VehicleActivity')
return;
// Listen for change events on the Brand dropdown list.
const brandsSelectList = await awaitElement('#Brand'); // awaitElement is a custom function that returns a promise that resolves after the element exists.
const currentBrand = brandsSelectList.value;
// Get the current value of the Model property.
const currentModel = e.activityModel.properties.find(p => p.name === 'Model').expressions['Literal'];
// Setup a change handler for when the user changes the selected brand.
brandsSelectList.addEventListener('change', async e => {
await updateModels(e.currentTarget.value);
});
// Update the second dropdown with available options based on the current brand (if any).
// Also provide the currently selected model, if any.
await updateModels(currentBrand, currentModel);
});
// When the activity editor is closing, dispose event handlers.
eventBus.on('activity-editor-disappearing', e => {
if (e.activityDescriptor.type !== 'VehicleActivity')
return;
document.querySelector('#Brand').removeEventListener('change', updateModels);
});
}
I left out two functions to try and show the structure of the plugin, but here are the missing awaitElement
and updateModels
functions:
// Awaits the existence of an element.
// Taken from this [SO answer][3].
const awaitElement = async selector => {
while ( document.querySelector(selector) === null) {
await new Promise( resolve => requestAnimationFrame(resolve) )
}
return document.querySelector(selector);
};
The updateModels function should live inside the plugin, because it requires access to the httpClientFactory
in this example:
// A function that requests a list of models from the server based on the selected car brand.
const updateModels = async (brand, currentModel) => {
let models = [];
// Only attempt to fetch car models if a brand was given.
if (!!brand) {
const httpClient = await httpClientFactory();
const response = await httpClient.get(`api/samples/brands/${brand}/models`);
models = response.data;
}
// Get a reference to the models select list.
const modelsSelectList = await awaitElement('#Model');
modelsSelectList.innerHTML = "";
// Build up the models dropdown list.
for (const model of models) {
const selected = model === currentModel;
const option = new Option(model, model, selected, selected);
modelsSelectList.options.add(option);
}
}
The complete plugin code should look like this:
// A sample plugin for the VehicleActivity sample activity.
function VehicleActivityPlugin(elsaStudio) {
const {eventBus, httpClientFactory} = elsaStudio;
// A function that requests a list of models from the server based on the selected car brand.
const updateModels = async (brand, currentModel) => {
let models = [];
// Only attempt to fetch car models if a brand was given.
if (!!brand) {
const httpClient = await httpClientFactory();
const response = await httpClient.get(`api/samples/brands/${brand}/models`);
models = response.data;
}
// Get a reference to the models select list.
const modelsSelectList = await awaitElement('#Model');
modelsSelectList.innerHTML = "";
// Build up the models dropdown list.
for (const model of models) {
const selected = model === currentModel;
const option = new Option(model, model, selected, selected);
modelsSelectList.options.add(option);
}
}
// When the activity editor is opened, setup an event handler on the Brands dropdown list.
eventBus.on('activity-editor-appearing', async e => {
// We are only interested in our custom activity being editor.
if (e.activityDescriptor.type !== 'VehicleActivity')
return;
// Listen for change events on the Brand dropdown list.
const brandsSelectList = await awaitElement('#Brand');
const currentBrand = brandsSelectList.value;
const currentModel = e.activityModel.properties.find(p => p.name === 'Model').expressions['Literal'];
brandsSelectList.addEventListener('change', async e => {
await updateModels(e.currentTarget.value);
});
await updateModels(currentBrand, currentModel);
});
// When the activity editor is closing, dispose event handlers.
eventBus.on('activity-editor-disappearing', e => {
if (e.activityDescriptor.type !== 'VehicleActivity')
return;
document.querySelector('#Brand').removeEventListener('change', updateModels);
});
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Sipke Schoorstra |