Hi all,
I wanted to share a solution I built for using a JSX component in a Framework markdown file combined with Generators in order to build arbitrary controls that provide reactivity similar to the Inputs
that come with Framework.
I’m not sure if this was the best approach (and would love to hear if anyone has ideas for optimizations) but it wasn’t immediately obvious to me how to combine these at first, so maybe someone else will benefit from this.
Our goal was to have a set of 3 linked select
inputs, each one populated by an array. The array in the 2nd select
would be filtered based on the selected value in the first select
, and the 3rd select
array would be filtered based on the selected value in the second select
.
Changing a select
higher in the chain would also reset the ones below.
Now, the selected values from the JSX need to be available in the global scope so they can be used to filter other components in the markdown file, so we used Generators to make them reactive.
Here’s our starting data (simplified):
const vps = [
{ name: 'VP Name 1', id: 123, mgrId: 0 },
{ name: 'VP Name 2', id: 124, mgrId: 0 }
];
const directors = [
{ name: 'Dir Name 1', id: 125, mgrId: 123 },
{ name: 'Dir Name 2', id: 126, mgrId: 124 }
];
const managers = [
{ name: 'Mgr Name 1', id: 127, mgrId: 125 },
{ name: 'Mgr Name 2', id: 128, mgrId: 126 }
];
Here are the Generators in the Markdown file:
const selectedVp = Generators.observe((change) => {
const vpUp = (evt) => change(evt.detail.value);
document.addEventListener('vpSelectChange', vpUp)
change(0);
return () => removeEventListener('vpSelectChange', vpUp)
});
const selectedDirector = Generators.observe((change) => {
const dirUp = (evt) => change(evt.detail.value);
document.addEventListener('directorSelectChange', dirUp)
change(0);
return () => removeEventListener('directorSelectChange', dirUp)
});
const selectedManager = Generators.observe((change) => {
const mgrUp = (evt) => change(evt.detail.value);
document.addEventListener('managerSelectChange', mgrUp)
change(0);
return () => removeEventListener('managerSelectChange', mgrUp)
});
Here’s the JSX file:
It accepts an array of selects and will create any number of them (in our case it’s 3, but you could pass in 2, or more than 3, etc)…
import React, { useRef, useState } from 'npm:react';
export function MultiSelect( { selects } ) {
const selectState = [];
const selectRefs = [];
selects.forEach((select) => {
selectRefs.push(useRef(null));
selectState.push(useState(select.value));
})
const handleSelectChange = (event, index) => {
selectState[index][1](event.target.value);
document.dispatchEvent(new CustomEvent(selects[index].onChangeEventName, { detail: { value: event.target.value } }));
for (let i = index + 1; i < selects.length; i++) {
selectState[i][1](selects[i].defaultOption.value);
document.dispatchEvent(new CustomEvent(selects[i].onChangeEventName, { detail: { value: selects[i].defaultOption.value } }));
}
}
return (
<div>
{selects.map((select, index) => (
<select ref={selectRefs[index]} id={`select${index}`} value={selectState[index][0]} onChange={(event) => handleSelectChange(event, index)}>
{[select.defaultOption, ...select.options].map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
))}
</div>
);
}
As part of the component’s handleSelectChange()
, we fire a custom event that the generators we declared in the Markdown file are listening for (see below for where the event name is passed into the JSX), which is how we trigger an update to the globally-scoped variables outside of the JSX component.
And finally here’s how the JSX component is called and rendered in the Markdown file:
display(<MultiSelect selects={[
{
defaultOption: { label: 'Select a VP', value: 0 },
options: vps.map((vp) => ({
value: vp.id,
label: vp.name
})),
value: 0,
onChangeEventName: 'vpSelectChange'
},
{
defaultOption: { label: 'Select a director', value: 0 },
options: directors.filter((d) => d.mgrId == selectedVp).map((dir) => ({
value: dir.id,
label: dir.name
})),
value: 0,
onChangeEventName: 'directorSelectChange'
},
{
defaultOption: { label: 'Select a manager', value: 0 },
options: managers.filter((m) => m.mgrId == selectedDirector).map((mgr) => ({
value: mgr.id,
label: mgr.name
})),
value: 0,
onChangeEventName: 'managerSelectChange'
}
]} />)
Because selectedVp
, selectedDirector
and selectedManager
are all driven by generators, whenever they change, it refreshes the options
array which updates the JSX component but also any other component in your Markdown file that relies on them.
In our case, we have a big list of hundreds programs that have a managerId
property and we filter that list based on the selected values in the drop-downs… So as the user drills down by VP, Director and Manager, the chart displaying the programs narrows its scope to less and less programs.
Anyhow, this was a fun exploration in how to combine the reactivity of JSX components with Generators.