Hi All!
I'm new to ExB development and coming from WAB originally. This thing has been wracking my brain for a few days and I'm at my wits end here. Not sure if it's just a don't know what you don't know thing, but it's still frustrating none-the-less.
I'm trying to use a query string parameter that's passed through the url when I first load the app I'm building with ExB developer version 1.17. In my custom widget I have a few different components. The main search component service I have that does the actual searches works great when used within the context of the widget itself. The steps I"m trying to get to are as follows:
Drop URL with query param in the browser > widget pulls query param (it's set to auto open on app start) > sets the search text using useState > called the handleSearch function (which is promise based)
Using a useEffect gives me a bunch of issues since you have to declare all of your dependencies, which includes the setSearchText, handleSearch function, and the like. Here's the current widget.tsx:
/** @jsx jsx */
/** @jsxFrag React.Fragment */
import { React, type AllWidgetProps } from 'jimu-core'
import { jsx, css } from 'jimu-core'
import { type BufferSettings, BufferUnit, type IMConfig } from '../config'
import {
Button, Card, CardBody, CardFooter, Dropdown, DropdownButton, DropdownItem,
DropdownMenu, Tab, Tabs, TextInput
} from 'jimu-ui'
import { SearchOutlined } from 'jimu-icons/outlined/editor/search'
import { MoreVerticalOutlined } from 'jimu-icons/outlined/application/more-vertical'
import ResultItem from './components/resultItem'
import ResultsTabTitle from './components/resultsTabTitle'
import { type JimuMapView, JimuMapViewComponent } from 'jimu-arcgis'
import { useMapService } from './services/map'
import { useSearchService } from './services/search'
import type Graphic from '@arcgis/core/Graphic'
import DrawTools from './components/drawTools'
import BufferControls from './components/bufferControls'
const { useState } = React
enum TabName {
ByValue = 'byValue',
ByShape = 'byShape',
Results = 'results'
}
const Widget = (props: AllWidgetProps<IMConfig>) => {
const { id, config } = props
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<Error | null>(null)
const [searchText, setSearchText] = useState('')
const [activeTab, setActiveTab] = useState<TabName>(TabName.ByValue)
const [jimuMapView, setJimuMapView] = useState<JimuMapView>(null)
const [searchResults, setSearchResults] = useState<Graphic[]>([])
const [searchResultsCount, setSearchResultsCount] = useState(0)
const [bufferSettings, setBufferSettings] = useState<BufferSettings>({
enabled: false,
distance: 5,
unit: BufferUnit.Feet
})
const mapService = useMapService(id)
const searchService = useSearchService(config.selectedLayerId)
const handleActiveViewChange = (jmv: JimuMapView) => {
setJimuMapView(jmv)
if (jmv) {
searchService.initialize(jmv)
mapService.initialize(jmv)
}
}
const handleUpdateBufferSetting = (field: keyof BufferSettings, value: any) => {
setBufferSettings({
...bufferSettings,
[field]: value
})
}
const handleSearch = async () => {
if (!jimuMapView || !config.selectedLayerId || !searchText.trim()) {
return
}
setIsLoading(true)
setError(null)
const { features, error } = await searchService.searchFeatures(
searchText,
config.searchFields
)
if (error) {
setError(error)
setIsLoading(false)
return
}
setSearchResults(features)
if (features.length > 0) {
setSearchResultsCount(features.length)
setActiveTab(TabName.Results)
mapService.updateResultsLayer(features, `${config.title} Results`)
}
setIsLoading(false)
}
const handleSpatialSearch = async (graphic: Graphic) => {
if (!jimuMapView || !config.selectedLayerId || !graphic) {
return
}
setIsLoading(true)
setError(null)
const { features, error } = await searchService.searchFeaturesBySpatial(
graphic.geometry,
bufferSettings
)
if (error) {
setError(error)
setIsLoading(false)
return
}
setSearchResults(features)
if (features.length > 0) {
setSearchResultsCount(features.length)
setActiveTab(TabName.Results)
mapService.updateResultsLayer(features, `${config.title} Results`)
if (bufferSettings.enabled) {
mapService.highlightBufferedSearchFeature(graphic)
}
}
setIsLoading(false)
}
const handleClearSearch = () => {
setSearchResults([])
setSearchResultsCount(0)
setSearchText('')
setError(null)
mapService.clearResults()
setActiveTab(TabName.ByValue)
}
const handlePressEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (searchText.trim()) {
handleSearch()
}
}
const handleResultItemClick = (item: Graphic) => {
if (item) {
mapService.highlightFeature(item)
}
}
const handleSearchOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(event.target.value)
if (error) setError(null)
}
const getSearchableFieldsText = () => {
if (!config.searchFields || config.searchFields.length === 0) {
return 'Search by keyword'
}
return config.searchFields.reduce((text, field, index, array) => {
const fieldAlias = field.alias || field.fieldName
if (index === 0) {
return `Search by ${fieldAlias}`
} else if (index === array.length - 1) {
return `${text}, or ${fieldAlias}`
} else {
return `${text}, ${fieldAlias}`
}
}, '')
}
return (
<div className="widget-wc-search" css={css`
height: 100%;
display: flex;
flex-direction: column;
`}>
<Tabs
className='h-100'
defaultValue='byValue'
value={activeTab}
fill
onChange={(tabId: string) => {
if (Object.values(TabName).includes(tabId as TabName)) {
setActiveTab(tabId as TabName)
}
}}
type='pills'
>
<Tab
id={TabName.ByValue}
title='By Value'
>
<Card>
<CardBody>
{error && (
<div className="error-message" css={css`
color: var(--sys-color-error-main);
margin-bottom: 10px;
padding: 8px;
background-color: var(--sys-color-error-light);
border-radius: 4px;
`}>
<p>Error: {error.message}</p>
</div>
)}
<p>{getSearchableFieldsText()}</p>
<TextInput
placeholder='Enter Account Number, Parcel, Etc.'
onPressEnter={handlePressEnter}
disabled={isLoading}
value={searchText}
onChange={handleSearchOnChange}
prefix={<SearchOutlined size="s" />}
/>
</CardBody>
<CardFooter
css={css`
display: flex;
justify-content: space-between;
`}
>
<Button
onClick={handleClearSearch}
disabled={!searchText.trim()}
size='default'
>
Clear
</Button>
<Button
onClick={handleSearch}
disabled={isLoading || !searchText.trim()}
size='default'
css={css`${(isLoading) ? 'background-color: var(--ref-palette-neutral-300) !important;' : ''}`}
>
{(isLoading) ? 'Searching...' : 'Search'}
</Button>
</CardFooter>
</Card>
</Tab>
<Tab
id={TabName.ByShape}
title='By Shape'
>
<Card>
<CardBody>
{error && (
<div className="error-message" css={css`
color: var(--sys-color-error-main);
margin-bottom: 10px;
padding: 8px;
background-color: var(--sys-color-error-light);
border-radius: 4px;
`}>
<p>Error: {error.message}</p>
</div>
)}
<p>Select features by</p>
<DrawTools
jimuMapView={jimuMapView}
onGraphicCreated={handleSpatialSearch}
/>
<BufferControls
settings={bufferSettings}
onUpdateSetting={handleUpdateBufferSetting}
/>
</CardBody>
</Card>
</Tab>
<Tab
id={TabName.Results}
title={(<ResultsTabTitle recordCount={searchResultsCount} />)}
>
<div>
<div
className='d-flex justify-content-end pt-1 pb-1 sticky-top bg-overlay'
>
<Dropdown
direction='down'
>
<DropdownButton
arrow={false}
icon
size='default'
css={css`
border: none !important;
`}
>
<MoreVerticalOutlined />
</DropdownButton>
<DropdownMenu>
<DropdownItem
onClick={handleClearSearch}
>
Clear Results
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<>
{
searchResults.map((feature, index) => {
return (
<ResultItem
key={feature.attributes.OBJECTID}
feature={feature}
displayFields={config.displayFields}
summaryConfig={{
showSummaryLink: config.showSummaryLink,
summaryLinkLabel: config.summaryLinkLabel,
summaryUrlTemplate: config.summaryUrlTemplate,
summaryLinkFieldName: config.summaryLinkFieldName
}}
onItemClick={() => { handleResultItemClick(feature) }}
/>
)
})
}
</>
</div>
</Tab>
</Tabs>
<JimuMapViewComponent
useMapWidgetId={config.useMapWidgetIds?.[0]}
onActiveViewChange={handleActiveViewChange}
/>
</div>
)
}
export default Widget
I still have some refactoring to do but thats the gist of it. Not sure where to go with this and Claude.ai keeps lying to me like AI usually does. lol
Thanks in advance!
- Dean Wilson