Using query string parameter in custom search widget

105
1
2 weeks ago
DeanWilson
Occasional Contributor

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

0 Kudos
1 Reply
DeanWilson
Occasional Contributor

Bumping this!

0 Kudos