Open psiegers opened 1 year ago
Please update google places to be compatible with Angular 16!
I have literally written my own directive for this in about 30 mins and it works fine! Couldn't find a suitable solution as don't use Angular Material for anything.
@lana-white can you share your solution of ngx-google-places-autocomplete for angular16.
@mfsi-samu, I've just made my own. I couldn't find anything/any plugin suitable.
I last tried Angular Magic's (ngx-gp-autocomplete), but kept getting errors with autocomplete
being undefined when calling reset()
.
So i based my code off of this, but changed it drastically. This utilises the @types/google.maps library.
/// <reference types="@types/google.maps" />
import { Directive, ElementRef, OnInit, Output, EventEmitter, NgZone, Input, Injectable } from '@angular/core';
@Injectable({providedIn: 'root'})
@Directive({
selector: '[google-places]',
exportAs: 'ngx-google-places'
})
export class NgxGooglePlacesDirective implements OnInit {
public element: HTMLInputElement;
autocomplete?: google.maps.places.Autocomplete;
eventListener?: google.maps.MapsEventListener;
ngZone:NgZone;
place?: google.maps.places.PlaceResult;
@Output() onAddressChange: EventEmitter<any> = new EventEmitter();
@Input() options?: google.maps.places.AutocompleteOptions;
constructor(elRef: ElementRef, ngZone: NgZone) {
//elRef will get a reference to the element where
//the directive is placed
this.element = elRef.nativeElement;
this.ngZone = ngZone;
}
ngOnInit() {
this.autocomplete = new google.maps.places.Autocomplete(this.element);
//Event listener to monitor place changes in the input
if (this.autocomplete.addListener !== null)
this.eventListener = google.maps.event.addListener(this.autocomplete, 'place_changed', () => {
this.handleChangeEvent();
});
}
reset() {
this.autocomplete?.setComponentRestrictions(this.options?.componentRestrictions || null);
this.autocomplete?.setTypes(this.options?.types || []);
}
handleChangeEvent() {
this.ngZone.run(() => {
this.place = this.autocomplete?.getPlace();
if (this.place) {
this.onAddressChange.emit(this.place);
}
});
}
}
import { AfterViewInit, Component, ElementRef, forwardRef, Input, isDevMode, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, ControlContainer, ControlValueAccessor, FormControl, FormControlDirective, FormGroup, FormGroupDirective, NG_VALUE_ACCESSOR } from '@angular/forms';
import { map } from 'rxjs';
import { IAddressFields } from '../../models/data/form/address-fields.model';
import { IFormField } from '../../models/data/form/form-field.model';
import { filterByFieldId, getAddressFieldsByStreetId, getCountryFieldIdByStreet } from '../../utils/form-utils';
import { htmlDecode } from '../../utils/utils';
import { NgxGooglePlacesDirective } from '../../directives/ngx-google-places.directive';
@Component({
selector: 'nova-form-field-address-block',
template: `
<div class="form-floating">
<input
google-places
#placesRef="ngx-google-places"
[options]="options"
(change)="callbackFunction ? callbackFunction!($event) : null"
(onAddressChange)="handleAddressChange($event)"
type="text"
[id]="field.id"
[name]="field.id"
[placeholder]="lookupPlaceholderText"
[formControlName]="controlName"
class="form-control has-subtext"
[attr.aria-describedby]="field.id + 'Feedback'"/>
<label [for]="field.id" [innerHtml]="((field.id == -20 ? 'Billing ' : field.id === -27 ? 'Shipping ' : '') + 'Address' + (field.isRequired ? ' *' : '') | safeHtml)"></label>
<div class="subtext">
<p class='p-small'>Can't find your address?
<a (click)="toggleAddrFields($event)">Enter in manually<i [ngClass]="{'fa-chevron-down': !addressFieldsVisible, 'fa-chevron-up': addressFieldsVisible}" class="fa-solid fw-bold ms-1 text-decoration-none text-link fa-2x "></i>
</a>
</p>
</div>
<nova-form-validation [cName]="controlName" [fGroup]="formGroup" [field]="field"></nova-form-validation>
</div>
`,
viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }]
})
export class FormFieldAddressBlockComponent implements OnInit, AfterViewInit {
@Input() field!: IFormField;
@Input() formGroup!: FormGroup;
@Input() controlName!: string;
@Input() data?: any;
@Input() callbackFunction?: (args: any) => void;
@Input() callbackFunctionFocus?: (args: any) => void;
addressObj: IAddressFields = {} as IAddressFields;
lookupControl!: FormControl;
street1Control!: FormControl;
street2Control?: FormControl;
suburbControl!: FormControl;
stateControl!: FormControl;
postcodeControl!: FormControl;
countryControl!: FormControl;
lookupField!: IFormField;
street1Field!: IFormField;
street2Field?: IFormField;
suburbField!: IFormField;
stateField!: IFormField;
postcodeField!: IFormField;
countryField!: IFormField;
backupPlaceholderText: string = 'Start typing your address...';
lookupPlaceholderText: string = 'Start typing your address...';
addressFieldsVisible:boolean = false;
//Set to false if not wanting to have addr subfields update lookup field on blur
updateLookupField: boolean = true;
@ViewChild("placesRef") placesRef?: NgxGooglePlacesDirective;
options!: google.maps.places.AutocompleteOptions;
constructor() {
}
ngOnInit(): void {
this.options = {
componentRestrictions: {
country: this.formGroup.get(getCountryFieldIdByStreet(this.field.id).toString())!.value,
},
types: [],
//Restrict to basic data fields
fields: ['address_component', 'adr_address', 'business_status', 'formatted_address', 'geometry', 'icon', 'icon_mask_base_uri', 'icon_background_color', 'name', 'photo', 'place_id', 'plus_code', 'type', 'url', 'utc_offset_minutes', 'vicinity', 'wheelchair_accessible_entrance']
};
}
ngAfterViewInit(): void {
// this.ngxGpAutocompleteService.setOptions(this.options);
this.addressObj = getAddressFieldsByStreetId(this.field.id);
if (this.addressObj){
if (this.addressObj.address) {
this.lookupControl = this.formGroup.get(this.addressObj.address.toString()) as FormControl;
this.lookupField = filterByFieldId(this.data.form, this.addressObj.address, false) as IFormField;
}
if (this.addressObj.street1) {
this.street1Control = this.formGroup.get(this.addressObj.street1.toString()) as FormControl;
this.street1Field = filterByFieldId(this.data.form, this.addressObj.street1, false) as IFormField;
}
if (this.addressObj.street2) {
this.street2Control = this.formGroup.get(this.addressObj.street2.toString()) as FormControl;
this.street2Field = filterByFieldId(this.data.form, this.addressObj.street2, false) as IFormField;
}
if (this.addressObj.suburb) {
this.suburbControl = this.formGroup.get(this.addressObj.suburb.toString()) as FormControl;
this.suburbField = filterByFieldId(this.data.form, this.addressObj.suburb, false) as IFormField;
}
if (this.addressObj.state) {
this.stateControl = this.formGroup.get(this.addressObj.state.toString()) as FormControl;
this.stateField = filterByFieldId(this.data.form, this.addressObj.state, false) as IFormField;
}
if (this.addressObj.postcode) {
this.postcodeControl = this.formGroup.get(this.addressObj.postcode.toString()) as FormControl;
this.postcodeField = filterByFieldId(this.data.form, this.addressObj.postcode, false) as IFormField;
}
if (this.addressObj.country) {
this.countryControl = this.formGroup.get(this.addressObj.country.toString()) as FormControl;
this.countryField = filterByFieldId(this.data.form, this.addressObj.country, false) as IFormField;
}
}
if (this.placesRef) {
this.placesRef.options = this.options;
}
this.formGroup.get(getCountryFieldIdByStreet(this.field.id).toString())?.valueChanges
.pipe(
map(c => c)
)
.subscribe({
next: (value) => {
if (this.placesRef && this.placesRef.options) {
if (isDevMode()) {
console.log('new country!', value, 'current country:', this.placesRef?.options.componentRestrictions?.country);
}
if (this.options?.componentRestrictions && value && this.placesRef) {
if (this.placesRef.options.componentRestrictions?.country !== value) {
this.options.componentRestrictions.country = value;
this.placesRef.options.componentRestrictions!.country = value;
this.placesRef.reset();
this.resetAddrFields();
}
}
}
}
})
}
handleAddressChange(address: google.maps.places.PlaceResult | any) {
if(isDevMode()) console.log('handle addr change', address);
let addr = address.adr_address;
if (address.adr_address && addr && address) {
//Get Subpremise, street_number and route
let street1Formatted = address.adr_address?.split('locality')[0];
//Replace span tags from address
street1Formatted = htmlDecode(street1Formatted);
// street1Formatted = street1Formatted.replace(/<[^>]*>?/gm, '');
//Remove last comma in street1 address
street1Formatted = street1Formatted.replace(/(^\s*,)|(,\s*$)/g, '');
//Remove all HTML tags
let result = htmlDecode(addr);
// let result = addr.replace(/<[^>]*>?/gm, '');
this.formGroup.get(this.controlName)?.setValue(result, { emitEvent: false });
this.fillInAddress(address, street1Formatted);
}
}
addrFieldsValid() {
return this.street1Control.valid && this.street2Control?.valid && this.suburbControl.valid && this.stateControl.valid && this.postcodeControl.valid;
}
public toggleAddrFields(e?: any) {
if (this.street1Field.hide) {
if (this.updateLookupField) {
this.detectManualAddressChange();
}
else {
this.lookupControl.setValue('', { emitEvent: false });
this.lookupPlaceholderText = this.backupPlaceholderText;
this.lookupControl.disable({ emitEvent: false });
}
this.addressFieldsVisible = true;
this.street1Field.hide = false;
if (this.street2Field) this.street2Field.hide = false;
this.suburbField.hide = false;
this.postcodeField.hide = false;
this.stateField.hide = false;
this.countryField.hide = false;
}
else {
if (this.updateLookupField) {
}
else {
this.lookupPlaceholderText = '';
this.resetAddrFields();
this.lookupControl.enable({ emitEvent: false });
this.lookupControl.updateValueAndValidity();
}
if ((this.addrFieldsValid() && this.updateLookupField) || !this.updateLookupField) {
this.addressFieldsVisible = false;
this.street1Field.hide = true;
if (this.street2Field) this.street2Field.hide = true;
this.suburbField.hide = true;
this.postcodeField.hide = true;
this.stateField.hide = true;
this.countryField.hide = true;
}
}
}
resetAddrFields() {
Object.entries(this.addressObj).forEach(([key, value]) => {
let fc;
fc = this.formGroup.get(value.toString());
if (value !== this.addressObj.country) {
fc?.setValue('', { emitEvent: false });
fc?.updateValueAndValidity();
}
});
}
fillInAddress(address: google.maps.places.PlaceResult, street1Formatted: any) {
// Get the place details from the autocomplete object.
if (address && address.address_components) {
const place = address;
let street1 = "";
let street2 = "";
let suburb = "";
let state = "";
let postcode = "";
//Not requiring Long/Lat at this stage. add to AddrObj etc if needed
let locationLong = "";
let locationLat = "";
// Get each component of the address from the place details,
// and then fill-in the corresponding field on the form.
// place.address_components are google.maps.GeocoderAddressComponent objects
// which are documented at http://goo.gle/3l5i5Mr
for (const component of place.address_components!) {
const componentType = component.types[0];
switch (componentType) {
//Using formatted subpremise/street1address from result.addr_address for street1 field instead
// case "subpremise":
// {
// street1 = `${component.long_name}/`;
// break;
// }
// case "street_number":
// case "route": {
// console.log('street_number:', component.long_name);
// console.log('-----');
// if (street1.length > 0) { street1 += " "; }
// street1 += `${component.long_name}`;
// break;
// }
case "route": {
street1 += `${(street1 ? ' ' : '')}${component.short_name}`;
break;
}
case "administrative_area_level_1":
state += component.short_name;
break;
case "postal_code": {
postcode = `${component.long_name}${postcode}`;
break;
}
case "postal_code_suffix": {
//Only really applies to US postcodes
postcode = `${postcode}-${component.long_name}`;
break;
}
case "sublocality_level_1":
case "locality":
//Allow for some suburbs not listed as locality (e.g. 876 McDonald Ave, Brooklyn in USA)
suburb = component.long_name;
break;
}
}
this.street1Control.setValue(street1Formatted, { emitEvent: false });
this.suburbControl.setValue(suburb, { emitEvent: false });
this.stateControl.setValue(state, { emitEvent: false });
this.postcodeControl.setValue(postcode, { emitEvent: false });
//If required fields aren't filled in from lookup, then show addr fields and don't alow fields to hide.
if (!this.addrFieldsValid() && this.updateLookupField) {
this.toggleAddrFields();
}
}
}
concatAddr(st1:any, st2:any, suburb:any, state:any, postcode:any) {
var fullAddr = '';
fullAddr = st1 ? st1 + ', ' : '';
fullAddr += st2 ? st2 + ', ' : '';
fullAddr += suburb ? suburb + ', ' : '';
fullAddr += state ? state + ' ' : '';
fullAddr += postcode ? postcode + ', ' : '';
fullAddr += this.countryControl.value;
return fullAddr;
}
detectManualAddressChange() {
let fullAddr = '';
var fieldElems:HTMLInputElement[] = [];
Object.entries(this.addressObj).forEach(([key, value]) => {
if (value !== this.addressObj.country) {
let field: HTMLInputElement = document.getElementById(value.toString()) as HTMLInputElement;
fieldElems.push(field);
}
});
fieldElems.forEach(field => {
field.addEventListener('blur', (e: any) => {
switch (Number(field.id)) {
case this.addressObj.street1: {
fullAddr = this.concatAddr(e?.target?.value, this.street2Control?.value, this.suburbControl.value, this.stateControl.value, this.postcodeControl.value);
break;
}
case this.addressObj.street2: {
fullAddr = this.concatAddr(this.street1Control?.value, e?.target?.value, this.suburbControl.value, this.stateControl.value, this.postcodeControl.value);
break;
}
case this.addressObj.suburb: {
fullAddr = this.concatAddr(this.street1Control?.value, this.street2Control?.value, e?.target?.value, this.stateControl.value, this.postcodeControl.value);
break;
}
case this.addressObj.state: {
fullAddr = this.concatAddr(this.street1Control?.value, this.street2Control?.value, this.suburbControl.value, e?.target?.value, this.postcodeControl.value);
break;
}
case this.addressObj.postcode: {
fullAddr = this.concatAddr(this.street1Control?.value, this.street2Control?.value, this.suburbControl.value, this.stateControl.value, e?.target?.value);
break;
}
}
this.lookupControl.setValue(fullAddr, {emitEvent: false})
});
});
}
}
Because of inactivity of this package, I created my own package which is fully compatible with this one, code base was used from this package and adjusted to new coding standards, is Angular 16 compatible.
You can try: https://www.npmjs.com/package/@angular-magic/ngx-gp-autocomplete
I was on holiday, but anyways, thanks for the posts! I didn't actually have the time (then) to wait for any useful answer so we decided to implement an autosuggest input control based on some Material components, and the use of the Bing Maps API (specifically the Locations API at https://learn.microsoft.com/en-us/bingmaps/rest-services/locations/). With this approach I was able to create a similar UX with a back-end API call that did the same and did not depend on any other package. It only does require you to register to get a key. I'm unable to share the code as I rolled off the project before leaving - sorry for that - but it can be done without too many effort.
Because of inactivity of this package, I created my own package which is fully compatible with this one, code base was used from this package and adjusted to new coding standards, is Angular 16 compatible.
You can try: https://www.npmjs.com/package/@angular-magic/ngx-gp-autocomplete
No directive found with exportAs 'ngx-places'.I got this error please help. One more thing do u need ngx-google-places-autocomplete package too??
In HTML: <input nz-input placeholder="Search Location " type="text" formControlName="line2"
ngx-gp-autocomplete
(onAddressChange)="handleAddressChange($event,i)"
/>
In ts:
@ViewChild('ngxPlaces') placesRef!: NgxGpAutocompleteDirective;
Our web application uses google recaptcha which was developed using Angular 14. When we were close to the go-live our client tested it on several real devices, one of them is an iPhone 11 Pro Max with iOS 16.3.1 installed. He reports having problems filling in some form fields. We could replicate these in a Browserstack Live session on a real device of the same model. Turns out that we needed to update Angular and Material from version 14 to 16 to resolve those issues. So we first decided to upgrade to Angular 15 first including Material. We did this without too many problems and used Browserstack Live again and the problems were still there for iOS versions 16. So then we tried to upgrade to Angular / Material 16 but that didn't work out well, as ng-recaptcha reported an issue, this we could fix to upgrade to version 12, but then there was another issue: ngx-google-places-autocomplete did not support Angular 16. Looks like this package is not updated since a couple of years - is it still active? So, the big Q is here, will there be support for Angular 16? Or should we completely steer away from using Google recaptcha?