projects/common/lib/components/address/address.component.ts
Note - This component REQUIRES that HttpClientModule
is registered in your NgModule.
OnInit
OnChanges
ControlValueAccessor
providers |
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AddressComponent) }
|
selector | common-address |
templateUrl | ./address.component.html |
viewProviders |
|
Properties |
Methods |
|
Inputs |
Outputs |
Accessors |
constructor()
|
address | |
addressServiceUrl | |
Type : string
|
|
allowExtralines | |
Type : boolean
|
|
Default value : false
|
|
If true, adds a plus icon next to street and enables users to add a second
address line. This value binds to |
bcOnly | |
Type : boolean
|
|
Default value : false
|
|
countryList | |
Type : CountryList[]
|
|
Default value : COUNTRY_LIST
|
|
defaultCountry | |
Type : string
|
|
Default value : CANADA
|
|
defaultProvince | |
Type : string
|
|
Default value : BRITISH_COLUMBIA
|
|
disabled | |
Type : boolean | ReadOnlyFields
|
|
Default value : false
|
|
disableGeocoder | |
Type : boolean
|
|
Default value : false
|
|
isRequired | |
Type : boolean
|
|
Default value : false
|
|
labels | |
Type : AddrLabelList
|
|
maxlengths | |
Type : Maxlengths
|
|
provinceList | |
Type : ProvinceList[]
|
|
Default value : PROVINCE_LIST
|
|
addressChange | |
Type : EventEmitter<Address>
|
|
addLine | ||||||||
addLine(line: "2" | "3")
|
||||||||
Parameters :
Returns :
void
|
Private findCountryCode | ||||||
findCountryCode(country: string)
|
||||||
Parameters :
Returns :
string
|
Private findProvinceDescription | ||||||
findProvinceDescription(prov: string)
|
||||||
Parameters :
Returns :
string
|
isCanada |
isCanada()
|
Returns :
boolean
|
ngOnChanges | ||||
ngOnChanges(changes)
|
||||
Parameters :
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
Parameters :
Returns :
void
|
registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
Parameters :
Returns :
void
|
removeLine | ||||||
removeLine(line: "2" | "3")
|
||||||
Parameters :
Returns :
void
|
selectSuggestedAddress | ||||||
selectSuggestedAddress(address: Address)
|
||||||
Parameters :
Returns :
void
|
setAddress | ||||||
setAddress(data: GeoAddressResult)
|
||||||
Parameters :
Returns :
void
|
setCity | ||||||
setCity(value: string)
|
||||||
Parameters :
Returns :
void
|
setCountry | ||||||
setCountry(value: string)
|
||||||
Set country province blank
Parameters :
Returns :
void
|
Private setDefaultCountryAsOption |
setDefaultCountryAsOption()
|
Set country to default Search uses country code or country name to find item is list.
Returns :
string
|
Private setDefaultProvinceAsOption | ||||||
setDefaultProvinceAsOption(country: string)
|
||||||
Sets the default province option value
Parameters :
Returns :
string
|
setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
Parameters :
Returns :
void
|
Private setLabels |
setLabels()
|
Returns :
void
|
Private setMaxlengths |
setMaxlengths()
|
Returns :
void
|
setPostalCode | ||||||
setPostalCode(value: string)
|
||||||
Sets string after converted upper case
Parameters :
Returns :
void
|
setProvince | ||||||
setProvince(value: string)
|
||||||
Parameters :
Returns :
void
|
Private setReadOnlyFields |
setReadOnlyFields()
|
Returns :
void
|
setStreetAddress | ||||||
setStreetAddress(value: string)
|
||||||
Parameters :
Returns :
void
|
Private truncateAddressLines | ||||||
truncateAddressLines(address: Address)
|
||||||
Parameters :
Returns :
Address
|
Private updateProvList |
updateProvList()
|
Updates the provList variable. Values must be stored in a variable and not accessed via function invocation for performance.
Returns :
void
|
writeValue | ||||||
writeValue(value: Address)
|
||||||
Parameters :
Returns :
void
|
Private _cityComponent |
Type : CityComponent
|
Decorators :
@ViewChild('city')
|
_onChange |
Default value : () => {...}
|
_onTouched |
Default value : () => {...}
|
addr |
Type : Address
|
addrLabels |
Type : AddrLabelList
|
Default value : {
address1: 'Full street address, rural route, PO box or general delivery',
address2: 'Address Line 2',
address3: 'Address Line 3',
city: 'City',
province: 'Province or state',
country: 'Jurisdiction',
postalCode: 'Postal Code or Zip Code'
}
|
fieldMaxLengths |
Type : Maxlengths
|
Default value : {
address: '25',
city: '25',
province: '25',
country: '250',
postalCode: '25'
}
|
provList |
Type : ProvinceList[]
|
readOnlyFields |
Type : ReadOnlyFields
|
Default value : {
address: false,
city: false,
province: false,
country: false,
postalCode: false,
}
|
showLine2 |
Default value : false
|
showLine3 |
Default value : false
|
Public objectId |
Type : string
|
Default value : UUID.UUID()
|
Inherited from
Base
|
Defined in
Base:11
|
An identifier for parents to keep track of components |
address | ||||
getaddress()
|
||||
setaddress(val)
|
||||
Parameters :
Returns :
void
|
useAddressValidator |
getuseAddressValidator()
|
todo
import {
Component,
Input,
Output,
EventEmitter,
OnChanges,
forwardRef,
OnInit,
ViewChild
} from '@angular/core';
import { ControlContainer, ControlValueAccessor, NgForm, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Base } from '../../models/base';
import { GeoAddressResult } from '../../services/geocoder.service';
import { Address } from '../../models/address.model';
import { CountryList, CANADA, COUNTRY_LIST } from '../country/country.component';
import { ProvinceList, BRITISH_COLUMBIA, PROVINCE_LIST } from '../province/province.component';
import { CityComponent } from '../city/city.component';
export interface AddrLabelList {
address1?: string;
address2?: string;
address3?: string;
city?: string;
province?: string;
country?: string;
postalCode?: string;
}
export interface Maxlengths {
address?: string;
city?: string;
province?: string;
country?: string;
postalCode?: string;
}
export interface ReadOnlyFields {
address?: boolean;
city?: boolean;
province?: boolean;
country?: boolean;
postalCode?: boolean;
}
/**
*
* Note - This component REQUIRES that `HttpClientModule` is registered in your NgModule.
*/
@Component({
selector: 'common-address',
templateUrl: './address.component.html',
/* Re-use the same ngForm that it's parent is using. The component will show
* up in its parents `this.form`, and will auto-update `this.form.valid`
*/
viewProviders: [
{ provide: ControlContainer, useExisting: forwardRef(() => NgForm) }
],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AddressComponent) }
]
})
export class AddressComponent extends Base
implements OnInit, OnChanges, ControlValueAccessor {
/* Disable all fields by sending in boolean,
* disable specific fields by sending in ReadOnlyFields structure
*/
@Input() disabled: boolean | ReadOnlyFields = false;
@Input() isRequired: boolean = false;
@Input() countryList: CountryList[] = COUNTRY_LIST;
@Input() defaultCountry: string = CANADA;
@Input() provinceList: ProvinceList[] = PROVINCE_LIST;
@Input() defaultProvince: string = BRITISH_COLUMBIA;
@Input() disableGeocoder: boolean = false; // This should eventually be refactored to `disableAddressValidator`. Leaving it for compatibility.
@Input() labels: AddrLabelList;
@Input() maxlengths: Maxlengths;
@Input() bcOnly: boolean = false;
@Input() addressServiceUrl: string;
@ViewChild('city') private _cityComponent: CityComponent;
@Input()
set address(val: Address) {
if (val) {
this.addr = val;
}
}
get address(): Address {
return this.addr;
}
@Output() addressChange: EventEmitter<Address> = new EventEmitter<Address>();
/**
* If true, adds a plus icon next to street and enables users to add a second
* address line. This value binds to `address.addressLine2`
*/
@Input() allowExtralines: boolean = false;
addr: Address;
provList: ProvinceList[];
showLine2 = false;
showLine3 = false;
// Labels defaulted to MSP
addrLabels: AddrLabelList = {
address1: 'Full street address, rural route, PO box or general delivery',
address2: 'Address Line 2',
address3: 'Address Line 3',
city: 'City',
province: 'Province or state',
country: 'Jurisdiction',
postalCode: 'Postal Code or Zip Code'
};
// Lengths defaulted to MSP
fieldMaxLengths: Maxlengths = {
address: '25',
city: '25',
province: '25',
country: '250',
postalCode: '25'
};
readOnlyFields: ReadOnlyFields = {
address: false,
city: false,
province: false,
country: false,
postalCode: false,
};
_onChange = (_: any) => { };
_onTouched = (_: any) => { };
constructor() {
super();
}
ngOnInit() {
this.setLabels();
this.setMaxlengths();
this.setReadOnlyFields();
if (this.addr) {
if (!this.addr.country) {
this.addr.country = this.setDefaultCountryAsOption();
} else {
// If string for country submitted, need to find code to display value in select box
this.addr.country = this.findCountryCode( this.addr.country );
}
if (!this.addr.province) {
this.addr.province = this.setDefaultProvinceAsOption(this.addr.country);
}
// Make sure addressLine2 is visible if there is data persisted to display there.
if (this.allowExtralines) {
if (this.addr.addressLine2) {
this.addLine(2);
}
if (this.addr.addressLine3) {
this.addLine(3);
}
}
}
this.updateProvList();
}
/**
* Set country province blank
* @param value
*/
setCountry(value: string) {
this.addr.province = this.setDefaultProvinceAsOption( value );
this.addr.country = value;
this.updateProvList();
if ( this.isCanada() ) {
// If Canada, clear postal code to display mask
this.addr.postal = '';
if (!this.disableGeocoder) {
this.showLine2 = false;
this.showLine3 = false;
this.addr.addressLine2 = '';
this.addr.addressLine3 = '';
}
}
this._onChange(this.addr);
this.addressChange.emit(this.addr);
this._onTouched(this.addr);
}
setProvince(value: string) {
this.addr.province = value;
this._onChange(this.addr);
this.addressChange.emit(this.addr);
this._onTouched(this.addr);
}
setStreetAddress(value: string) {
// When emptying a search result from address validator, clear the rest of the fields as well.
if (value === '' && !this.disableGeocoder) {
this.addr.addressLine1 = '';
this.addr.addressLine2 = '';
this.addr.addressLine3 = '';
this.addr.city = '';
this.addr.postal = '';
if (!this.bcOnly) {
this.addr.province = BRITISH_COLUMBIA;
this.addr.country = CANADA;
}
if (this.allowExtralines) {
this.showLine2 = false;
this.showLine3 = false;
}
}
this.addr.addressLine1 = value;
this._onChange(this.addr);
this.addressChange.emit(this.addr);
this._onTouched(this.addr);
}
setCity(value: string) {
this.addr.city = value;
this._onChange(this.addr);
this.addressChange.emit(this.addr);
this._onTouched(this.addr);
}
/**
* Sets string after converted upper case
* @param text
*/
setPostalCode(value: string) {
this.addr.postal = value;
this._onChange(this.addr);
this.addressChange.emit(this.addr);
this._onTouched(this.addr);
}
isCanada(): boolean {
return this.addr && CANADA === this.addr.country;
}
ngOnChanges(changes) {
if (changes['countryList'] && changes['countryList'].currentValue) {
if (this.addr && !this.addr.country) {
// Set defaults
this.addr.country = this.setDefaultCountryAsOption();
// Set defaults
this.addr.province = this.setDefaultProvinceAsOption(this.addr.country);
}
this.updateProvList();
}
if (changes['provinceList'] && changes['provinceList'].currentValue) {
if (this.addr && !this.addr.province) {
// Set defaults
this.addr.province = this.setDefaultProvinceAsOption(this.addr.country);
}
this.updateProvList();
}
}
addLine(line: 2 | 3 = null) {
// Add lines in order
if (line === null) {
if (!this.showLine2) {
this.showLine2 = true;
} else if (!this.showLine3) {
this.showLine3 = true;
}
} else {
// Add specific line number
const lookup = `showLine${line}`;
this[lookup] = true;
}
}
removeLine(line: 2 | 3) {
// We can remove lines in any order, depending on user input
// Dynamically lookup variable based on line number input.
const lookup = `showLine${line}`;
this[lookup] = false;
// TODO - Need to clear the data in the appropriate field, just null/undefined it out.
const addrLookup = `addressLine${line}`;
this.address[addrLookup] = undefined;
}
/**
* Updates the provList variable. Values must be stored in a variable and not
* accessed via function invocation for performance.
*/
private updateProvList() {
if (!this.provinceList) { return; } // When data is async and hasn't loaded
this.provList = this.provinceList
.map(prov => {
if (prov.country === this.addr.country) {
return prov;
}
})
.filter(x => x);
}
/**
* Sets the default province option value
*/
private setDefaultProvinceAsOption(country: string): string {
const provObj = !this.provinceList ? null : this.provinceList.find(
val => (val.provinceCode === this.defaultProvince ||
val.description === this.defaultProvince) &&
val.country === country
);
return (provObj ? provObj.provinceCode : '');
}
private findProvinceDescription(prov: string): string {
const provObj = !this.provinceList ? null : this.provinceList.find(
val => val.provinceCode === prov ||
val.description === prov
);
return (provObj ? provObj.description : null);
}
/**
* Set country to default
* Search uses country code or country name to find item is list.
*/
private setDefaultCountryAsOption(): string {
return this.findCountryCode( this.defaultCountry );
}
private findCountryCode( country: string ): string {
const countryObj = !this.countryList
? null
: this.countryList.find(
val =>
val.countryCode === country ||
val.description === country
);
return countryObj ? countryObj.countryCode : null;
}
get useAddressValidator(): boolean {
if (this.disableGeocoder) {
return false;
}
return this.isCanada();
}
// Only BC addresses therefore no need to copy province into structure.
setAddress(data: GeoAddressResult) {
this.addr.addressLine1 = data.street;
this.addr.city = data.city;
this.addr.province = data.province;
this.addr.country = data.country;
this.addressChange.emit(this.addr);
}
selectSuggestedAddress(address: Address) {
if (!address.street && !address.city && !address.postal) {
return;
}
// Truncate long addresses and drop them down a line to addressLine2 etc.
address = this.truncateAddressLines(address);
if (this.bcOnly && address.province != BRITISH_COLUMBIA) {
alert('Please select a valid BC address.');
setTimeout(() => {
this.addr.addressLine1 = '';
this.addr.addressLine2 = '';
this.addr.addressLine3 = '';
}, 0);
return;
}
// Placed in timeout to override ngx-bootstrap population.
setTimeout(() => {
this.addr.addressLine1 = address.addressLine1 || '';
}, 0);
if (this.allowExtralines) {
this.addr.addressLine2 = address.addressLine2 || '';
this.addr.addressLine3 = address.addressLine3 || '';
if (address.addressLine2) {
this.showLine2 = true;
}
if (address.addressLine3) {
this.showLine3 = true;
}
}
this.addr.city = address.city;
if (this._cityComponent) {
this._cityComponent.onValueChange(this.addr.city);
}
this.addr.postal = address.postal;
if (!this.bcOnly) {
this.addr.province = address.province;
}
this._onChange(this.addr);
this.addressChange.emit(this.addr);
this._onTouched(this.addr);
}
writeValue( value: Address) {
if ( value ) {
this.addr = value;
}
}
registerOnChange(fn: any): void {
this._onChange = fn;
}
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.setReadOnlyFields();
}
private setLabels() {
if ( this.labels ) {
Object.keys(this.labels).map( x => this.addrLabels[x] = this.labels[x] );
}
}
private setMaxlengths() {
if ( this.maxlengths ) {
Object.keys(this.fieldMaxLengths).map( x => this.maxlengths[x] = this.fieldMaxLengths[x]);
}
}
private setReadOnlyFields() {
if ( typeof this.disabled === 'boolean' ) {
Object.keys(this.readOnlyFields).map( x => this.readOnlyFields[x] = this.disabled );
} else {
Object.keys(this.disabled).map( x => this.readOnlyFields[x] = this.disabled[x] );
}
}
private truncateAddressLines(address: Address): Address {
const maxlength: number = parseInt(this.fieldMaxLengths.address, 10);
const lines: Array<string> = [];
const newLines: Array<string> = [];
let lineIndex: number = 0;
// console.log('Before truncation: ', address);
// Create `lines` array of address lines.
for (let i=1; i<=3; i++) {
if (address['addressLine' + i]) {
lines.push(address['addressLine' + i]);
}
}
for (let i=0; i<lines.length; i++) {
if (!lines[i]) {
break;
}
newLines.push('');
const words: Array<string> = lines[i].split(' ');
if (lines[i].length > maxlength && words.length > 1) {
// Iterate over words.
while (words.length > 0) {
newLines[lineIndex] += words[0] + ' ';
words.splice(0, 1);
if (words.length > 0) {
const tempNextLine = newLines[lineIndex] + words[0];
// Add next line in case words remain.
if (tempNextLine.length > maxlength) {
newLines[lineIndex] = newLines[lineIndex].trim();
newLines.push('');
lineIndex++;
}
}
}
newLines[lineIndex] = newLines[lineIndex].trim();
} else {
newLines[lineIndex] = lines[i];
}
lineIndex++;
};
for (let i=0; i<newLines.length; i++) {
address['addressLine' + (i + 1)] = newLines[i];
}
// console.log('After truncatation: ', address);
return address;
}
}
<div class="form-group">
<div class="row">
<div [ngClass]="{'col-11': allowExtralines, 'col-12': !allowExtralines}">
<common-street *ngIf="!useAddressValidator"
name="street_{{objectId}}"
label="{{addrLabels.address1}}"
[ngModel]="addr?.addressLine1"
(ngModelChange)="setStreetAddress($event)"
(selectEvent)="setAddress($event)"
[required]="isRequired"
[disabled]="readOnlyFields.address"
[maxlength]="fieldMaxLengths.address"
commonValidateStreet></common-street>
<!-- Address Validator -->
<common-address-validator *ngIf="useAddressValidator"
name="street_{{objectId}}"
label="{{addrLabels.address1}}"
[(ngModel)]="addr.addressLine1"
(ngModelChange)="setStreetAddress($event)"
[required]="isRequired"
[disabled]="readOnlyFields.address"
[maxlength]="fieldMaxLengths.address"
[serviceUrl]="addressServiceUrl"
(select)="selectSuggestedAddress($event)"
[populateAddressOnSelect]="true"
commonValidateStreet>
</common-address-validator>
</div>
<div class="col-1"
*ngIf='allowExtralines'>
<div class="h-50"></div>
<button class=" btn btn-transparent"
*ngIf='!(showLine2 && showLine3)'
aria-label="Add Additional Address Line (optional)"
(click)="addLine()">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</div>
<div class="form-group"
*ngIf='showLine2'>
<div class="row">
<div class="col-11">
<common-street label="{{addrLabels.address2}}"
name="street_line_2_{{objectId}}"
[(ngModel)]='addr.addressLine2'
[maxlength]="fieldMaxLengths.address"
[disabled]="readOnlyFields.address"
commonValidateStreet></common-street>
</div>
<div class="col-1">
<div class="h-50"></div>
<button class="btn btn-transparent"
*ngIf="!showLine3"
aria-label='Remove Address Line 2'
(click)="removeLine(2)">
<i class="fa fa-minus"></i>
</button>
</div>
</div>
</div>
<div class="form-group"
*ngIf='showLine3'>
<div class="row">
<div class="col-11">
<common-street label="{{addrLabels.address3}}"
name="street_line_3_{{objectId}}"
[(ngModel)]='addr.addressLine3'
[maxlength]="fieldMaxLengths.address"
[disabled]="readOnlyFields.address"
commonValidateStreet></common-street>
</div>
<div class="col-1">
<div class="h-50"></div>
<button class=" btn btn-transparent"
aria-label='Remove Address Line 3 '
(click)="removeLine(3)">
<i class="fa fa-minus"></i>
</button>
</div>
</div>
</div>
<div class="form-group col-sm-11 p-sm-0">
<common-city #city
name="city_{{objectId}}"
label="{{addrLabels.city}}"
[ngModel]="addr?.city"
(ngModelChange)="setCity($event)"
[required]="isRequired"
[disabled]="readOnlyFields.city"
[maxlength]="fieldMaxLengths.city"
commonValidateCity></common-city>
</div>
<div class="form-group col-sm-11 p-sm-0">
<common-province name="province_{{objectId}}"
label="{{addrLabels.province}}"
[ngModel]="addr?.province"
(ngModelChange)="setProvince($event)"
[provinceList]="provList"
[required]="isRequired"
[disabled]="readOnlyFields.province"
placeholder="{{ isCanada()? 'Select a Province': 'Select a State' }}"
[useDropDownList]="isCanada()"
[maxlength]="fieldMaxLengths.province"
commonValidateRegion></common-province>
</div>
<div class="form-group col-sm-11 p-sm-0">
<common-country name="country_{{objectId}}"
label="{{addrLabels.country}}"
[countryList]="countryList"
[ngModel]="addr?.country"
(ngModelChange)="setCountry($event)"
[required]="isRequired"
[disabled]="readOnlyFields.country"
[maxlen]="fieldMaxLengths.country"></common-country>
</div>
<div class="form-group col-sm-4 p-sm-0">
<common-postal-code name="pc_{{objectId}}"
label="{{addrLabels.postalCode}}"
[displayMask]="isCanada()"
[disabled]="readOnlyFields.postalCode"
[ngModel]="addr?.postal"
(ngModelChange)="setPostalCode($event)"
[required]="isRequired"
[maxlen]="fieldMaxLengths.postalCode"
commonValidatePostalcode
[hasMask]="isCanada()"
[bcOnly]="bcOnly"></common-postal-code>
</div>