I am attempting to use the DataSourceComponent to listen for selection changes on a data source. This works but I want to isolate the listener to certain events. For example, I only want this to be triggered when a user searches on my data source from the OOTB Search widget. I have the searchWidgetId and assumed the widgetId prop of the DataSourceComponent would handle this, but it doesn't honor that. Any selection made on this data source from any widget triggers the querySelectableDataSources from the onSelectionChange callback.
Below is the relevant functions and full JSX at this point. Any help is appreciated!
const getFeatureLayerDataSource = (dsId: string) => {
const dataSource = dataSourceManager.getDataSource(dsId);
if (dataSource) {
return dataSource as FeatureLayerDataSource;
}
return null
};
const constructFeature = (ds: FeatureLayerDataSource, record: DataRecord): ISelectedFeature => {
const feature = {
dsId: ds.id,
attributes: record.getData(),
geometry: record.getGeometry(),
oidFieldName: record.dataSource.getIdField(),
popupTemplate: ds.getPopupInfo()
}
return feature
};
const doQuery = () => {
// conditions to exit out of the querySelectableDataSources function
if (mapClickQuery["geometry"]) {
// This can get called when a user clicks on the map, exit out of the function
return false;
}
return true;
};
const querySelectableDataSources = (dsId: string) => {
// This function is called when the widget is loaded
// To return all the fields, ensure that each data source in the useDataSources
// object in the app config has a property of "fields: ['*']"
// This should be set in the settings implementation
// It's also called when the data source selections are changed
// Intended to handle selections from the search widget
// Check doQuery to determine if we should proceed with the query
if (!doQuery()) {
return;
}
const dsLoaded = dataSourceManager.getDataSource(dsId);
if (dsLoaded) {
const featureDataSource = getFeatureLayerDataSource(dsId);
const selectedRecords = dsLoaded.getSelectedRecords();
if (selectedRecords.length > 0) {
const selectedFeature = constructFeature(featureDataSource, selectedRecords[0]);
dispatchSelectionChange([selectedFeature]);
}
}
}
const dispatchSelectionChange = (selectedRecords: ISelectedFeature[]) => {
setIsRecordSelected(selectedRecords.length > 0);
props.dispatch(appActions.widgetStatePropChange(props.widgetId, "selection", selectedRecords));
}
return (
<div css={style}>
<div className="imagery-viewer">
</div>
<JimuMapViewComponent
useMapWidgetId={props.useMapWidgetIds?.[0]}
onActiveViewChange={activeViewChangeHandler}
/>
{tryCreateDataSourcesFinished ? (
<>
{props.useDataSources.map((useDataSource, index) => (
<DataSourceComponent
key={index}
useDataSource={useDataSource}
widgetId={props.config.searchWidgetId}
onSelectionChange={() => querySelectableDataSources(useDataSource.dataSourceId)}
/>
))}
{isRecordSelected ? (
<div className="imagery-viewer-header jimu-widget">
<ImagePopup {...props} jimuMapView={jimuMapView} />
</div>
) : (
<div className="imagery-viewer-header">{errorMessage ? (errorMessage) : (props.config.noSelectionMessage)}</div>
)}
<UserOptions {...props} />
</>
) : (
<Loading />
)}
</div>
);
Conceptually, the way I would handle this build is to turn the Search Widget into a custom widget and add a message that triggers the logic of your new custom widget. If you set up a useEffect() function in your custom widget, you can make it only run after getting a message from the Search Widget.
useEffect(()=> {
doStuff()
}, [messageFromSearch])
Thanks for the recommendation and link describing what you did! I like your additional implementation to hide the search results as well, it's odd they hang around. My search bar is right next to a sidebar element. If I collapse/expand my sidebar widget the results are not anchored to the search box either and cover up parts of my custom widget or slide to the center of the screen.
I may ultimately go this route, but want to exhaust other options. I feel like modifying OOTB widgets leads to more development time each time we decide to upgrade the ExB.
Just for sake of clarification when you say OOTB Search Widget, do you mean the Search widget you can drag from the pallet? Or the one you can toggle on that may exist on top of the map? Because from my experience these two function a little differently.
I recently wanted something similar to happen, for a search to automatically open a custom widget which has DataSourceComponents in it, but also queries several layers based on the point click or geometry.
To solve this I actually had to construct a custom action in the custom widget, use the action to store both the name of the widget (which I had to force define rather than rely on an auto name), and have it store whatever I needed to then open the custom widget, and if the value was set in the appStore then it went ahead and ran the query.
In your code it does appear that you are trying to leverage existing actions, but given my own personal experience, I wonder if the issue is that props.widgetId is not consistent enough for it to actually do something in the other widget.
One other thing, it looks like none of your methods are marked as async (nor are they await or .then), but I wonder if some of the calls to get data actually are async, and you may have a batch of code that completes with a key piece undefined so it does nothing. I know I had to work around asynchronicity a bit in my custom widget to get some things to work right.
Good point. I'm using the widget you can drag in from the widgets list. More functionality and configuration that I can apply to it.
When you mention a custom action, are you expanding upon the Message and action capabilities described here: https://developers.arcgis.com/experience-builder/guide/core-concepts/message-action/ If so, were you just intercepting the messages published from the Search widget? Did you have the modify the Search widget at all?
I have async/await implementations for things like waiting for the view, data sources, queries to resolve. Had some fun troubleshooting those 'undefined' results like you described. I will explore that more too.
Yes, that's one example.
This SDK Repo has an example of how to do it.
Experience Builder SDK Resource - Message Subscriber - Custom Action example
The one thing I'll add that took me a while to figure out, if you know you only ever have 1 of the widget in an experience, then you can put a defined id in one of two places either in the widget.json or as I did for my recent action, I created a shared defaultConfig file that I used in the action and the widget so its ID would always be the same.
I think I actually found a way you might infer at least the 'first' instance of it found in the config dynamically (to open that widget) from a method like this:
openCustomWidget = (): void => {
// Lookup the dynamically assigned widget ID in the Experience Builder configuration
const widgetUri = 'widgets/your-custom-widget/'
const widgets = getAppStore().getState().appConfig.widgets // Get the current app widgets config
// Find the widget with the specified URI
const widgetEntry = Object.entries(widgets).find(
([, widgetConfig]: [string, any]) => widgetConfig.uri === widgetUri
)
if (!widgetEntry) {
console.error(`No widget found with URI: ${widgetUri}`)
return
}
const [dynamicWidgetId] = widgetEntry // Extract the dynamically assigned widget ID (e.g., "widget_34")
console.log(`Found widget ID: ${dynamicWidgetId}`)
// Check if the widget class is already loaded
const isClassLoaded = getAppStore().getState().widgetsRuntimeInfo?.[dynamicWidgetId]?.isClassLoaded
if (!isClassLoaded) {
// Load the widget class dynamically
WidgetManager.getInstance()
.loadWidgetClass(dynamicWidgetId)
.then(() => {
getAppStore().dispatch(appActions.openWidget(dynamicWidgetId))
console.log(`Widget (${dynamicWidgetId}) opened after loading.`)
})
.catch((err) => {
console.error('Error loading widget class:', err)
})
} else {
// If already loaded, just open it
getAppStore().dispatch(appActions.openWidget(dynamicWidgetId))
console.log(`Widget (${dynamicWidgetId}) opened directly.`)
}
}
But again, if you have a widget that can open multiple instances I don't think that code approach works 😕