File

projects/common/lib/components/address-validator/address-validator.component.ts

Extends

AbstractFormControl

Implements

OnInit ControlValueAccessor

Metadata

selector common-address-validator
styleUrls ./address-validator.component.scss
templateUrl ./address-validator.component.html

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(controlDir: NgControl, cd: ChangeDetectorRef, http: HttpClient)
Parameters :
Name Type Optional
controlDir NgControl No
cd ChangeDetectorRef No
http HttpClient No

Inputs

address
Type : string
label
Type : string
Default value : 'Address Lookup'
maxlength
Type : string
Default value : '255'
populateAddressOnSelect
Type : boolean
Default value : false
serviceUrl
Type : string
disabled
Type : boolean
Default value : false
Inherited from AbstractFormControl
errorMessage
Type : ErrorMessage
Inherited from AbstractFormControl
label
Type : string
Inherited from AbstractFormControl

Outputs

addressChange
Type : EventEmitter<string>
select
Type : EventEmitter<Address>

Methods

Protected handleError
handleError(error: HttpErrorResponse)
Parameters :
Name Type Optional
error HttpErrorResponse No
Returns : any
lookup
lookup(address: string)
Parameters :
Name Type Optional
address string No
ngOnInit
ngOnInit()
Returns : void
onBlur
onBlur(event)
Parameters :
Name Optional
event No
Returns : void
onError
onError(err)
Parameters :
Name Optional
err No
onKeyUp
onKeyUp(event: KeyboardEvent)
Parameters :
Name Type Optional
event KeyboardEvent No
Returns : void
onLoading
onLoading(val: boolean)
Parameters :
Name Type Optional
val boolean No
Returns : void
onNoResults
onNoResults(val: boolean)
Parameters :
Name Type Optional
val boolean No
Returns : void
onSelect
onSelect(event: TypeaheadMatch)
Parameters :
Name Type Optional
event TypeaheadMatch No
Returns : void
Protected processResponse
processResponse(obj)

Formats the response from ADDRESS_URL, trimming irrelevant fields.

This works for other requests for the same API too, however it may error out on some items if matchPrecisionNot is not set.

Parameters :
Name Optional Description
obj No

The response from ADDRESS_URL

Returns : AddressResult[]
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
setSearchValue
setSearchValue(value: any)
Parameters :
Name Type Optional
value any No
Returns : void
Private stripStringToMaxLength
stripStringToMaxLength(str: string)
Parameters :
Name Type Optional
str string No
Returns : any
writeValue
writeValue(value: any)
Parameters :
Name Type Optional
value any No
Returns : void
ngOnInit
ngOnInit()
Inherited from AbstractFormControl
Returns : void
registerOnChange
registerOnChange(fn: any)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
fn any No
Returns : void
Protected registerValidation
registerValidation(ngControl: NgControl, fn: ValidationErrors)
Inherited from AbstractFormControl

Register self validating method

Parameters :
Name Type Optional Description
ngControl NgControl No
fn ValidationErrors No

function for validating self

Returns : any
setDisabledState
setDisabledState(isDisabled: boolean)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
Protected setErrorMsg
setErrorMsg()
Inherited from AbstractFormControl
Returns : void
Private validateLabel
validateLabel()
Inherited from AbstractFormControl
Returns : void
Abstract writeValue
writeValue(value: any)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
value any No
Returns : void

Properties

_defaultErrMsg
Type : ErrorMessage
Default value : { required: LabelReplacementTag + ' is required.', invalidChar: LabelReplacementTag + ' must contain letters and numbers, and may include special characters such as a hyphen, period, apostrophe, number sign, ampersand, forward slash, and blank characters.' }
_onChange
Default value : () => {...}
_onTouched
Default value : () => {...}
Public controlDir
Type : NgControl
Decorators :
@Optional()
@Self()
Public hasError
Type : boolean
Default value : false
Public hasNoResults
Type : boolean
Default value : false

has returned and has no results, an empty array.

Public isTypeaheadLoading
Type : boolean
Default value : false

Is the 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<AddressResult[]>
Default value : of([])

The list of results, from API, that is passed to the typeahead list

Abstract _defaultErrMsg
Type : ErrorMessage
Default value : {}
Inherited from AbstractFormControl
_onChange
Default value : () => {...}
Inherited from AbstractFormControl
_onTouched
Default value : () => {...}
Inherited from AbstractFormControl
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, throwError } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, map, catchError } from 'rxjs/operators';
import { TypeaheadMatch } from 'ngx-bootstrap/typeahead';
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { NgControl, ControlValueAccessor } from '@angular/forms';
import { Address } from '../../models/address.model';
import { AbstractFormControl } from '../../models/abstract-form-control';
import { ErrorMessage, LabelReplacementTag } from '../../models/error-message.interface';
import { deburr } from '../../../helpers/deburr';



/**
 * For TemplateForms, pass in an Address and recieve an Address
 * @example
 *           <common-address-validator
 *               label='Physical Address'
 *               [(ngModel)]="myAddress">
 *           </common-address-validator>
 *
 * @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-address-validator
 *              label='Physical Address'
 *              formControlName="address"
 *              (select)="getAddressObject($event)">
 *          </common-address-validator>
 */

export interface AddressResult {
  /** String from the API that includes street, city, province, and country. */
  AddressComplete: string;
  HouseNumber: string;
  SubBuilding: string;
  Street: string;
  Locality: string;
  DeliveryAddressLines: string;
  AddressLines: Array<string>;
  // Set to defaults in response
  Country: string;
  Province: string;
  PostalCode: string;
}

@Component({
  selector: 'common-address-validator',
  templateUrl: './address-validator.component.html',
  styleUrls: ['./address-validator.component.scss']
})
export class AddressValidatorComponent extends AbstractFormControl implements OnInit, ControlValueAccessor {

  @Input() label: string = 'Address Lookup';
  @Input() address: string;
  @Input() serviceUrl: string;
  @Input() populateAddressOnSelect: boolean = false;
  @Output() addressChange: EventEmitter<string> = new EventEmitter<string>();
  @Output() select: EventEmitter<Address> = new EventEmitter<Address>();

  @Input() maxlength: string = '255';

  _defaultErrMsg: ErrorMessage = {
    required:  LabelReplacementTag + ' is required.',
    invalidChar: LabelReplacementTag + ' must contain letters and numbers, and may include special characters such as a hyphen, period, apostrophe, number sign, ampersand, forward slash, and blank characters.'
  };
  /** The string in the box the user has typed */
  public search: string;
  /** Is the request still in progress? */
  public isTypeaheadLoading: boolean = false;
  /** 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<AddressResult[]> = of([]); // Result from 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 cd: ChangeDetectorRef,
              protected http: HttpClient) {
    super();
    if ( controlDir ) {
      controlDir.valueAccessor = this;
    }
  }

  ngOnInit() {
    super.ngOnInit();
    
    this.typeaheadList$ = this.searchText$.pipe(
      debounceTime(500),
      distinctUntilChanged(),
      // Trigger the network request, get results
      switchMap(searchPhrase => this.lookup(searchPhrase)),
      catchError(err => this.onError(err))
    );
  }

  onError(err): Observable<AddressResult[]> {
    this.hasError = true;
    // Empty array simulates no result response, nothing for typeahead to iterate over
    return of([]);
  }

  onLoading(val: boolean): void {
    this.isTypeaheadLoading = val;
    this.hasError = false;
  }

  // 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: AddressResult = event.item;

    // Output string to FormControl. If street is more than the max length shorten
    const stripped = data.AddressLines ? this.stripStringToMaxLength(deburr(data.AddressLines[0])) : null;

    const addr = new Address();
    addr.unitNumber = deburr(data.SubBuilding);
    addr.streetNumber = deburr(data.HouseNumber);
    addr.streetName = deburr(data.Street);
    addr.city = deburr(data.Locality);
    addr.country = data.Country;
    addr.province = data.Province;
    addr.street = stripped;
    addr.postal = deburr(data.PostalCode);
    addr.addressLine1 = data.AddressLines && data.AddressLines[0] ? deburr(data.AddressLines[0]) : null;
    addr.addressLine2 = data.AddressLines && data.AddressLines[1] ? deburr(data.AddressLines[1]) : null;
    addr.addressLine3 = data.AddressLines && data.AddressLines[2] ? deburr(data.AddressLines[2]) : null;
    // Save and emit Address for (select)
    this.selectedAddress = true;
    this.select.emit(addr);

    // For template forms, must explicitly set `search` value upon selecting an item.
    if (this.populateAddressOnSelect) {
      this.search = stripped;
    }
    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();
    if (this.search) {
      this._onChange(this.search);
    }
  }

  writeValue( value: any ): void {
    if ( value  !== undefined ) {
      this.search = value;
    }
  }

  setSearchValue(value: any) {
    this._onChange(value);
    this._onTouched(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);
  }

  lookup(address: string): Observable<AddressResult[]> {
    const params = new HttpParams()
                    .set('address', address);

    return this.http.get(this.serviceUrl, {
      params: params
    }).pipe(map(this.processResponse));
  }

  /**
   * Formats the response from ADDRESS_URL, trimming irrelevant fields.
   *
   * This works for other requests for the same API too, however it may error
   * out on some items if matchPrecisionNot is not set.
   *
   * @param obj The response from ADDRESS_URL
   */
  protected processResponse(obj): AddressResult[] {
    return obj.Address.map(feature => {
      const props = feature;
      const Locality = props.Locality;
      const AddressComplete = props.AddressComplete;
      const AddressLines = props.AddressLines;
      const DeliveryAddressLines = props.DeliveryAddressLines;
      const Province = props.Province;
      const Country = props.Country;
      const PostalCode = props.PostalCode;
      const SubBuilding = props.SubBuilding;
      const Street = props.Street;
      const HouseNumber = props.HouseNumber;

      return {
        AddressComplete,
        AddressLines,
        SubBuilding,
        Street,
        HouseNumber,
        Locality,
        DeliveryAddressLines,
        Province,
        Country,
        PostalCode
      };
    });
  }

  protected handleError(error: HttpErrorResponse) {
    console.error('AddressValidator network error', { error });
    return throwError('AddressValidator error');
  }
}

<label for="address-validator_{{label}}" class='text-nowrap'>{{label}}
  <span class="address-validator-status">
    <ng-container *ngIf="isTypeaheadLoading; else statusContainer"> &mdash; 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="address-validator_{{label}}"
       name="address-validator_{{label}}"
       [(ngModel)]='search'
       (ngModelChange)='setSearchValue($event)'
       (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='AddressComplete'
       [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 -->
<common-error-container
    [displayError]="controlDir && (controlDir?.touched || controlDir?.dirty) && controlDir?.errors">
    <div *ngIf="controlDir?.errors?.required">
        {{_defaultErrMsg.required}}
    </div>
    <div *ngIf="controlDir?.errors?.invalidChar">
        {{_defaultErrMsg.invalidChar}}
    </div>
</common-error-container>

<ng-template #addressSelected>
    <span> &mdash; Selected
        <i class="fa fa-check fa-fw text-success"></i>
    </span>
</ng-template>

<ng-template #noResults>
    <span *ngIf="search !== '' && hasNoResults"> &mdash; No Results</span>
</ng-template>


<ng-template #error>
    <span *ngIf='hasError; else noResults'> &mdash; Error
        <i class="fa fa-exclamation-triangle fa-fw text-danger"></i>
    </span>
</ng-template>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""