From 45efef4380993b6bd8463ed6c5c9cd476812e703 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Tue, 1 Jul 2025 15:25:37 -0700 Subject: [PATCH] refactor complete to use actions --- complete.ts | 188 +++++++++++++++++++++++++++++++------------------ package.json | 1 + pnpm-lock.yaml | 8 +++ 3 files changed, 128 insertions(+), 69 deletions(-) diff --git a/complete.ts b/complete.ts index 393fc2b..b86cc05 100644 --- a/complete.ts +++ b/complete.ts @@ -1,78 +1,128 @@ -import { validateInput } from '@repo/validate'; +import type { Action } from 'svelte/action'; +import { validateInput } from './index'; +/** CompletionPayload carries the current value of an input element when it is validated. */ +export type CompletionPayload = { place: google.maps.places.PlaceResult }; +/** CompleteEvent is a custom event that is dispatched when an input is validated. */ +export class CompleteEvent extends CustomEvent {} + +const googleOptions: google.maps.places.AutocompleteOptions = { + // componentRestrictions: { country: ['ca'] }, + fields: ['address_components'], + strictBounds: false +}; + +/** + * AddressComplete is a utility class for handling address input completion using Google Places API. + * It binds to an HTML input element and listens for address changes, populating related fields + * such as address2, city, province, postalCode, and country. + */ export class AddressComplete { - private primary?: HTMLInputElement; - address2?: HTMLInputElement; - city?: HTMLInputElement; - province?: HTMLInputElement; - postalCode?: HTMLInputElement; - country?: HTMLInputElement; + private _primary?: HTMLInputElement; + private _line2?: HTMLInputElement; + private _city?: HTMLInputElement; + private _province?: HTMLInputElement; + private _postalCode?: HTMLInputElement; + private _country?: HTMLInputElement; - bind(primary: HTMLInputElement) { - this.primary = primary; - addListener(this.primary, this); - } -} - -const addListener = (node: HTMLInputElement, completer: AddressComplete) => { - const googleOptions: google.maps.places.AutocompleteOptions = { - // componentRestrictions: { country: ['ca'] }, - fields: ['address_components'], - strictBounds: false + /** registers line 1 address field and attaches an autocompleter */ + primary: Action void }> = ( + node + ) => { + this._primary = node; + this._configurePrimary(); }; - const autocomplete = new google.maps.places.Autocomplete(node, googleOptions); + /** registers line 2 address field */ + line2: Action = (node) => { + this._line2 = node; + }; + /** registers city field */ + city: Action = (node) => { + this._city = node; + }; + /** registers province field */ + province: Action = (node) => { + this._province = node; + }; + /** registers postal code field */ + postalCode: Action = (node) => { + this._postalCode = node; + }; + /** registers country field */ + country: Action = (node) => { + this._country = node; + }; - autocomplete.addListener('place_changed', () => { - let address1 = ''; - let postalCode = ''; - - const place = autocomplete.getPlace(); - // Sort each component - for (const component of place.address_components as google.maps.GeocoderAddressComponent[]) { - const componentType = component.types[0]; - - switch (componentType) { - case 'street_number': { - address1 = `${component.long_name} ${address1}`; - break; - } - case 'route': { - address1 += component.short_name; - break; - } - case 'postal_code': { - postalCode = `${component.long_name}${postalCode}`; - break; - } - case 'postal_code_suffix': { - postalCode = `${postalCode}-${component.long_name}`; - break; - } - case 'locality': { - if (completer.city) completer.city.value = component.long_name; - break; - } - case 'administrative_area_level_1': { - // completer.province?.setValStr(component.short_name); - if (completer.province) completer.province.value = component.short_name; - break; - } - case 'country': { - // completer.country?.setValStr(component.short_name); - if (completer.country) completer.country.value = component.short_name; - break; - } - } + private _configurePrimary = () => { + if (typeof google === 'undefined' || !google.maps || !google.maps.places) { + throw new Error('Google Maps Places API is not loaded.'); + } + if (!this._primary) { + throw new Error('Primary address input is not registered.'); } - node.value = address1; - if (completer.postalCode) completer.postalCode.value = postalCode; - completer.address2?.focus(); + const autocomplete = new google.maps.places.Autocomplete(this._primary, googleOptions); - // Trigger rechecks on each field - validateInput(node); - if (completer.city !== undefined) validateInput(completer.city); - if (completer.postalCode !== undefined) validateInput(completer.postalCode); - }); -}; + autocomplete.addListener('place_changed', () => { + if (!this._primary) { + throw new Error('Primary address input went missing.'); + } + + let address1 = ''; + let postalCode = ''; + + const place = autocomplete.getPlace(); + // Sort each component + for (const component of place.address_components as google.maps.GeocoderAddressComponent[]) { + const componentType = component.types[0]; + + switch (componentType) { + case 'street_number': { + address1 = `${component.long_name} ${address1}`; + break; + } + case 'route': { + address1 += component.short_name; + break; + } + case 'postal_code': { + postalCode = `${component.long_name}${postalCode}`; + break; + } + case 'postal_code_suffix': { + postalCode = `${postalCode}-${component.long_name}`; + break; + } + case 'locality': { + if (this._city) this._city.value = component.long_name; + break; + } + case 'administrative_area_level_1': { + // this._province?.setValStr(component.short_name); + if (this._province) this._province.value = component.short_name; + break; + } + case 'country': { + // this._country?.setValStr(component.short_name); + if (this._country) this._country.value = component.short_name; + break; + } + } + } + + this._primary.value = address1; + if (this._postalCode) this._postalCode.value = postalCode; + this._line2?.focus(); + + // Trigger rechecks on each field + validateInput(this._primary); + if (this._city !== undefined) validateInput(this._city); + if (this._postalCode !== undefined) validateInput(this._postalCode); + + // Dispatch a custom event with the place details + const event = new CompleteEvent('complete', { detail: { place } }); + this._primary.dispatchEvent(event); + }); + }; +} diff --git a/package.json b/package.json index 027e651..5b94be7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/js": "^9.30.0", + "@types/google.maps": "^3.58.1", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.5", "globals": "^16.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b52a2cc..584d7e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@eslint/js': specifier: ^9.30.0 version: 9.30.0 + '@types/google.maps': + specifier: ^3.58.1 + version: 3.58.1 eslint: specifier: ^9.24.0 version: 9.30.0 @@ -141,6 +144,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/google.maps@3.58.1': + resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -710,6 +716,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/google.maps@3.58.1': {} + '@types/json-schema@7.0.15': {} '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0)(typescript@5.8.3))(eslint@9.30.0)(typescript@5.8.3)':