PageSourceSearch

https://commerce-assets.prodpi.com/assets/controllers/register/autocomplete_controller-os3ee0X.js

js prodpi.com collected 2026-09-25 19:25:06 UTC 4,218 bytes, 140 lines download raw bytes

1import { Controller } from '@hotwired/stimulus'
2
3const LOOKUP_ENDPOINT = 'https://us-autocomplete-pro.api.smarty.com/lookup'
4const AUTOCOMPLETE_COUNTRY = 'US'
5const SEARCH_DEBOUNCE_MS = 200
6
7/**
8 * Client-side SmartyStreets US Street Autocomplete Pro integration for the
9 * studio street address field.
10 *
11 * Modeled on the deprecated api/client/src/Pages/Address.ts wiring: search
12 * only fires once the input contains a space and the selected country is US,
13 * selecting a suggestion fills city/state/zip, and every lookup is logged
14 * into a hidden `autocompleteLogs` field using the same shape
15 * AddressService::buildAddressAutocompleteLogs() already expects from the
16 * ProDPI form. This calls the SmartyStreets REST endpoint directly with the
17 * embedded/website key rather than vendoring the smartystreets-javascript-sdk
18 * package, which isn't wired into the AssetMapper importmap and whose only
19 * capability used here is this one GET request.
20 */
21/* stimulusFetch: 'lazy' */
22export default class extends Controller {
23  static targets = [
24    'input',
25    'menu',
26    'countrySelect',
27    'city',
28    'state',
29    'zip',
30    'autocompleteLogs',
31  ]
32  static values = { websiteKey: String }
33
34  #debounceTimer = null
35  #logs = []
36
37  search() {
38    window.clearTimeout(this.#debounceTimer)
39    this.#debounceTimer = window.setTimeout(
40      () => this.#performSearch(),
41      SEARCH_DEBOUNCE_MS
42    )
43  }
44
45  closeMenu() {
46    this.#toggleMenu(false)
47  }
48
49  async #performSearch() {
50    if (!this.websiteKeyValue) return
51
52    const search = this.inputTarget.value
53    if (!search.includes(' ')) return
54    if (
55      this.hasCountrySelectTarget &&
56      this.countrySelectTarget.value !== AUTOCOMPLETE_COUNTRY
57    ) {
58      return
59    }
60
61    try {
62      const result = await this.#lookup({ search })
63      this.#renderSuggestions(result.suggestions ?? [])
64    } catch {
65      // Autocomplete is a convenience on top of manual entry; a failed
66      // lookup just leaves the field for the studio to type themselves.
67    }
68  }
69
70  async #lookup(params) {
71    const url = new URL(LOOKUP_ENDPOINT)
72    url.searchParams.set('key', this.websiteKeyValue)
73    for (const [key, value] of Object.entries(params)) {
74      if (value) url.searchParams.set(key, value)
75    }
76
77    const response = await fetch(url)
78    const result = await response.json()
79    this.#logLookup(params, result.suggestions ?? [])
80    return result
81  }
82
83  #logLookup(search, suggestions) {
84    this.#logs.push({ type: 'frontEnd', search, response: suggestions })
85    if (this.hasAutocompleteLogsTarget) {
86      this.autocompleteLogsTarget.value = JSON.stringify(this.#logs)
87    }
88  }
89
90  #renderSuggestions(suggestions) {
91    this.menuTarget.innerHTML = ''
92
93    suggestions.forEach((suggestion) => {
94      const option = document.createElement('li')
95      option.textContent = this.#formatSuggestion(suggestion)
96      option.addEventListener('click', () => this.#selectSuggestion(suggestion))
97      this.menuTarget.appendChild(option)
98    })
99
100    this.#toggleMenu(suggestions.length > 0)
101  }
102
103  #formatSuggestion(suggestion) {
104    let address = suggestion.street_line
105
106    if (suggestion.secondary) {
107      address += ` ${suggestion.secondary}`
108      if (suggestion.entries > 1) address += ` (${suggestion.entries} entries)`
109    }
110
111    return `${address}, ${suggestion.city}, ${suggestion.state} ${suggestion.zipcode}`
112  }
113
114  async #selectSuggestion(suggestion) {
115    if (suggestion.entries > 1) {
116      const expanded = [suggestion.street_line, suggestion.secondary]
117        .filter(Boolean)
118        .join(' ')
119      const result = await this.#lookup({
120        search: this.inputTarget.value,
121        selected: expanded,
122      })
123      this.#renderSuggestions(result.suggestions ?? [])
124      return
125    }
126
127    this.inputTarget.value = [suggestion.street_line, suggestion.secondary]
128      .filter(Boolean)
129      .join(' ')
130    if (this.hasCityTarget) this.cityTarget.value = suggestion.city
131    if (this.hasStateTarget) this.stateTarget.value = suggestion.state
132    if (this.hasZipTarget) this.zipTarget.value = suggestion.zipcode
133
134    this.#toggleMenu(false)
135  }
136
137  #toggleMenu(active) {
138    this.menuTarget.toggleAttribute('hidden', !active)
139  }
140}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.