projects/common/lib/components/geocoder-input/geocoder-input.component.ts
<common-geocoder-input
label='Physical Address'
[(ngModel)]="myAddress">
</common-geocoder-input>
<common-geocoder-input
label='Physical Address'
formControlName="address"
(select)="getAddressObject($event)">
</common-geocoder-input>
selector | common-geocoder-input |
styleUrls | ./geocoder-input.component.scss |
templateUrl | ./geocoder-input.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
constructor(controlDir: NgControl, geocoderService: GeocoderService, cd: ChangeDetectorRef)
|
||||||||||||
Parameters :
|
address | |
Type : string
|
|
label | |
Type : string
|
|
Default value : 'Address Lookup'
|
|
maxlength | |
Type : string
|
|
Default value : '255'
|
|
addressChange | |
Type : EventEmitter<string>
|
|
select | |
Type : EventEmitter<Address>
|
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
onBlur | ||||
onBlur(event)
|
||||
Parameters :
Returns :
void
|
onError | ||||
onError(err)
|
||||
Parameters :
Returns :
Observable<GeoAddressResult[]>
|
onKeyUp | ||||||
onKeyUp(event: KeyboardEvent)
|
||||||
Parameters :
Returns :
void
|
onLoading | ||||||
onLoading(val: boolean)
|
||||||
Parameters :
Returns :
void
|
onNoResults | ||||||
onNoResults(val: boolean)
|
||||||
Parameters :
Returns :
void
|
onSelect | ||||||
onSelect(event: TypeaheadMatch)
|
||||||
Parameters :
Returns :
void
|
registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
Parameters :
Returns :
void
|
registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
Parameters :
Returns :
void
|
Private stripStringToMaxLength | ||||||
stripStringToMaxLength(str: string)
|
||||||
Parameters :
Returns :
any
|
writeValue | ||||||
writeValue(value: any)
|
||||||
Parameters :
Returns :
void
|
_onChange |
Default value : () => {...}
|
_onTouched |
Default value : () => {...}
|
Public controlDir |
Type : NgControl
|
Decorators :
@Optional()
|
Public hasError |
Type : boolean
|
Default value : false
|
Public hasNoResults |
Type : boolean
|
Default value : false
|
Geocoder API has returned and has no results, an empty array. |
Public isTypeaheadLoading |
Type : boolean
|
Default value : false
|
Is the Geocoder API request still in progress? |
Public search |
Type : string
|
The string in the box the user has typed |
Private searchText$ |
Default value : new Subject<string>()
|
The subject that triggers on user text input and gets typeaheadList$ to update. |
Public selectedAddress |
Type : boolean
|
Default value : false
|
Similar to this.address, but we can null it when user is searching for new addresses |
Public typeaheadList$ |
Type : Observable<GeoAddressResult[]>
|
The list of results, from API, that is passed to the typeahead list |
Public objectId |
Type : string
|
Default value : UUID.UUID()
|
Inherited from
Base
|
Defined in
Base:11
|
An identifier for parents to keep track of components |
import { Component, OnInit, Input, ChangeDetectorRef, Output, EventEmitter, Optional, Self } from '@angular/core';
import { Subject, Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { TypeaheadMatch } from 'ngx-bootstrap/typeahead';
import { Base } from '../../models/base';
import { GeocoderService, GeoAddressResult } from '../../services/geocoder.service';
import { CANADA } from '../country/country.component';
import { BRITISH_COLUMBIA } from '../province/province.component';
import { Address } from '../../models/address.model';
import { NgControl, ControlValueAccessor } from '@angular/forms';
/**
* @deprecated Please use `address-validator` component instead.
*
* For TemplateForms, pass in an Address and recieve an Address
* @example
* <common-geocoder-input
* label='Physical Address'
* [(ngModel)]="myAddress">
* </common-geocoder-input>
*
* @note
* For ReactiveForms, pass in a string and recieve a string. If you need the
* Address object you can use (select) in addition.
*
* @example
* <common-geocoder-input
* label='Physical Address'
* formControlName="address"
* (select)="getAddressObject($event)">
* </common-geocoder-input>
*/
@Component({
selector: 'common-geocoder-input',
templateUrl: './geocoder-input.component.html',
styleUrls: ['./geocoder-input.component.scss']
})
export class GeocoderInputComponent extends Base implements OnInit, ControlValueAccessor {
@Input() label: string = 'Address Lookup';
@Input() address: string;
@Output() addressChange: EventEmitter<string> = new EventEmitter<string>();
@Output() select: EventEmitter<Address> = new EventEmitter<Address>();
@Input() maxlength: string = '255';
/** The string in the box the user has typed */
public search: string;
/** Is the Geocoder API request still in progress? */
public isTypeaheadLoading: boolean = false;
/** Geocoder API has returned and has no results, an empty array. */
public hasNoResults: boolean = false;
public hasError: boolean = false;
/** Similar to this.address, but we can null it when user is searching for new addresses */
public selectedAddress: boolean = false;
/** The list of results, from API, that is passed to the typeahead list */
public typeaheadList$: Observable<GeoAddressResult[]>; // Result from GeoCoderService address lookup
/** The subject that triggers on user text input and gets typeaheadList$ to update. */
private searchText$ = new Subject<string>();
_onChange = (_: any) => {};
_onTouched = (_?: any) => {};
constructor(@Optional() @Self() public controlDir: NgControl, private geocoderService: GeocoderService, private cd: ChangeDetectorRef) {
super();
if ( controlDir ) {
controlDir.valueAccessor = this;
}
}
ngOnInit() {
this.typeaheadList$ = this.searchText$.pipe(
debounceTime(500),
distinctUntilChanged(),
// Trigger the network request, get results
switchMap(searchPhrase => this.geocoderService.lookup(searchPhrase)),
catchError(err => this.onError(err))
);
}
onError(err): Observable<GeoAddressResult[]> {
this.hasError = true;
// Empty array simulates no result response, nothing for typeahead to iterate over
return of([]);
}
onLoading(val: boolean): void {
this.isTypeaheadLoading = val;
}
// Note - this will fire after an onError as well
onNoResults(val: boolean): void {
// If we have results, the error has resolved (e.g. network has re-connected)
if (val === false) {
this.hasError = false;
}
this.hasNoResults = val;
}
onSelect(event: TypeaheadMatch): void {
const data: GeoAddressResult = event.item;
// Output string to FormControl. If street is more than the max length shorten
const stripped = this.stripStringToMaxLength(data.street);
const addr = new Address();
addr.city = data.city;
// GeoCoder is only for BC, Canada, values can be set.
addr.country = CANADA; // Default country is Canda
addr.province = BRITISH_COLUMBIA; // Default province is BC
addr.street = stripped;
// Save and emit Address for (select)
this.selectedAddress = true;
this.select.emit(addr);
this._onChange(stripped);
}
onKeyUp(event: KeyboardEvent): void {
// Filter out 'enter' and other similar keyboard events that can trigger
// when user is selecting a typeahead option instead of entering new text.
// Without this filter, we do another HTTP request + force disiplay the UI
// for now reason
if (event.keyCode === 13 || event.keyCode === 9) { // enter & tab
return;
}
// Clear out selection
this.selectedAddress = false;
this.searchText$.next(this.search);
}
onBlur(event): void {
this._onTouched();
this._onChange(this.search);
}
writeValue( value: any ): void {
if ( value !== undefined ) {
this.search = value;
}
}
// Register change function
registerOnChange( fn: any ): void {
this._onChange = fn;
}
// Register touched function
registerOnTouched( fn: any ): void {
this._onTouched = fn;
}
private stripStringToMaxLength(str: string) {
const maxlength = parseInt(this.maxlength, 10);
return str.slice(0, maxlength);
}
}
<label for="geocoder_{{label}}" class='text-nowrap'>{{label}}
<span class="geocoder-status">
<ng-container *ngIf="isTypeaheadLoading; else statusContainer"> — Loading
<i class="fa fa-spinner fa-pulse fa-fw"></i>
</ng-container>
</span>
<ng-template #statusContainer>
<ng-template *ngIf="selectedAddress; then addressSelected; else error;"></ng-template>
</ng-template>
</label>
<input class="form-control"
type="text"
id="geocoder_{{label}}"
name="geocoder_{{label}}"
[(ngModel)]='search'
(keyup)='onKeyUp($event)'
[typeahead]='typeaheadList$'
[typeaheadIsFirstItemActive]="false"
[typeaheadSelectFirstItem]="false"
(typeaheadLoading)="onLoading($event)"
(typeaheadOnSelect)="onSelect($event)"
(typeaheadNoResults)="onNoResults($event)"
(blur)="onBlur($event)"
[attr.maxlength]='maxlength'
typeaheadOptionField='fullAddress'
typeaheadMinLength='3'
autocomplete="off"
spellcheck="false"
/>
<!-- Intentionally using 'nope' for autocomplete as it is invalid and forces false - https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion -->
<ng-template #addressSelected>
<span> — Selected
<i class="fa fa-check fa-fw text-success"></i>
</span>
</ng-template>
<ng-template #noResults>
<span *ngIf="hasNoResults"> — No Results
<i class="fa fa-times fa-fw text-warning"></i>
</span>
</ng-template>
<ng-template #error>
<span *ngIf='hasError; else noResults'> — Error
<i class="fa fa-exclamation-triangle fa-fw text-danger"></i>
</span>
</ng-template>