class AddressInput extends HTMLElement { // make element form-associated static formAssociated = true; constructor() { super(); this.attachShadow({ mode: "open" }); this.internals = this.attachInternals(); this.render(); // Element references this._input = this.shadowRoot.querySelector("input"); this._suggestions = this.shadowRoot.querySelector(".suggestions-container"); // Bind methods this._handleInput = this._handleInput.bind(this); // Debounce timer this._debounceTimer = null; // The suggestion data this._suggestionData = null; } // Lifecycle: when element is added to the DOM connectedCallback() { this._input.addEventListener("input", this._handleInput); } // Lifecycle: when element is removed from the DOM disconnectedCallback() { this._input.removeEventListener("input", this._handleInput); } // Lifecycle: watch these attributes for changes static get observedAttributes() { return ["placeholder", "api-key"]; } // Lifecycle: respond to attribute changes attributeChangedCallback(name, oldValue, newValue) { if (name === "placeholder" && this._input) { this._input.placeholder = newValue; } if (name === "api-key") { this._apiKey = newValue; } } // Properties API get value() { return this._input ? this._input.value : ""; } set value(val) { if (this._input) { this._input.value = val; const entries = new FormData(); entries.append("address", val); this.internals.setFormValue(entries); } } // Private methods _handleInput(event) { const searchText = event.target.value.trim(); // Set the form input value if they submit the form without choosing an option this.value = event.target.value; // Clear previous timer clearTimeout(this._debounceTimer); // Clear suggestions if input is less than 3 characters if (searchText.length < 3) { this._suggestions.innerHTML = ""; return; } // Debounce API calls (wait 300ms after typing stops) this._debounceTimer = setTimeout(() => { this._fetchAddressSuggestions(searchText).then((response) => { this._renderSuggestions(response.features); }); }, 300); } async _handleClick(gid) { try { const url = `https://api.stadiamaps.com/geocoding/v2/place_details?ids=${gid}`; const response = await fetch(url); const data = await response.json(); const suggestion = data.features[0]; this.SetValue(suggestion); // Dispatch custom event for clients of this library this.dispatchEvent( new CustomEvent("address-selected", { bubbles: true, composed: true, // Allows event to cross shadow DOM boundary detail: { location: suggestion, }, }), ); } catch (error) { console.error("Error fetching geocode of suggestion:", error); } } async _fetchAddressSuggestions(text) { try { //const url = `https://api.mapbox.com/search/geocode/v6/forward?q=${encodeURIComponent(text)}&access_token=${this._apiKey}`; const url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?text=${encodeURIComponent(text)}&focus.point.lat=35&focus.point.lon=-115`; const response = await fetch(url); const data = await response.json(); return data; } catch (error) { console.error("Error fetching geocoding suggestions:", error); } } _renderSuggestions(suggestions) { console.log("Rendering suggestions", suggestions); this._suggestions.innerHTML = suggestions .map((item, index) => { return `
${item.properties.name}
${item.properties.coarse_location}
`; }) .join(""); // Add click listeners to suggestions this.shadowRoot.querySelectorAll(".suggestion-item").forEach((el) => { el.addEventListener("click", (e) => { this._handleClick(el.dataset.gid); }); }); } // Initial render of component render() { const placeholder = this.getAttribute("placeholder") || "Enter address"; this.shadowRoot.innerHTML = `
`; } // Public methods clear() { if (this._input) { this._input.value = ""; this._suggestions.innerHTML = ""; } } SetValue(suggestion) { const props = suggestion.properties; if (props.formatted_address_line) { this.value = props.formatted_address_line; } else if (props.address_components) { this.value = `${props.address_components.number ?? ""} ${props.address_components.street ?? ""}, ${props.coarse_location ?? ""}`; } else { this.value = `${props.name ?? ""}, ${props.coarse_location}`; } this._suggestions.innerHTML = ""; } } customElements.define("address-input", AddressInput);