This commit is contained in:
Chris Chen
2026-05-25 17:32:18 -07:00
parent 9b28fbcfb6
commit d5648315a0
262 changed files with 32074 additions and 0 deletions
@@ -0,0 +1,31 @@
<div class="row">
<div class="col-sm-4 col-6">
<div class="form-group">
<label for="zips-{{uuid}}" class="label">Zip Code:</label>
<rbj-drop-down [name]="name+'Zip'" [ngModel]="zip" editable [source]="this.zipCodeList"
[maskExpression]="'00000-9999'" [id]="id+'Zip'" [inputClass]="'text-left '+inputClass"
(selectedChange)="zipCodeChanged($event);zipcodeToCounty(zip);onBlur()" [readonly]="readonly"
[disabled]="isDisabled" [required]="required" [mustMatch]="false">
</rbj-drop-down>
<!-- <input type="text" nbInput fullWidth name="zips-{{uuid}}" [(ngModel)]="zip" (change)="zipCodeChanged()"> (blur)="zipCodeChanged($event);onBlur()" -->
</div>
</div>
<div class="col-sm-5 col-12">
<div class="form-group">
<label for="citys-{{uuid}}" class="label">City</label>
<input type="text" class="{{inputClass}}" nbInput fullWidth [name]="name+'City'" [(ngModel)]="city"
[id]="id+'City'" autocomplete="off" [readonly]="readonly" (blur)="cityOrStateChanged();onBlur()"
[disabled]="isDisabled" [required]="required" [inputLimitation]="37">
</div>
</div>
<div class="col-sm-3 col-6">
<div class="form-group">
<label for="states-{{uuid}}" class="label">State</label>
<input type="text" class="{{inputClass}}" nbInput fullWidth [name]="name+'State'" [(ngModel)]="state"
[id]="id+'State'" autocomplete="off" [mask]="'UU'" validate [invalidMsg]="'Invalid state code format'" ffMsg
[readonly]="readonly" (blur)="cityOrStateChanged();onBlur()" [disabled]="isDisabled" [required]="required"
disableForceFocus>
</div>
</div>
</div>
@@ -0,0 +1,10 @@
@import "../../@theme/styles/themes";
form.ng-touched input.ng-invalid {
border-color: nb-theme(color-danger-default);
}
input[type="text"][disabled] {
color: black;
background-color: white;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CityStateZipComponent } from './city-state-zip.component';
describe('CityStateZipComponent', () => {
let component: CityStateZipComponent;
let fixture: ComponentFixture<CityStateZipComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CityStateZipComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CityStateZipComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,376 @@
import { Component, forwardRef, ViewChild, ElementRef, Input, Output, EventEmitter, Renderer2 } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlContainer, NgForm, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
import { UuidUtils } from '../../utilities/uuid-utils';
import { DropDownOption } from '../../entity/dropDownOption';
import { ForceFocusMsgDirective } from '../../directives/force-focus-msg/force-focus-msg.directive';
import { AddressInfo } from '../../models/contactInfo.model';
import { MsgBoxService } from '../../services/msg-box.service';
import { first, takeUntil } from 'rxjs/operators';
import { ObjectUtils } from '../../utilities/object-utils';
import { CityStateZipService, CityInfo } from '../../services/city-state-zip.service';
import { Subject } from 'rxjs';
import { StringUtils } from '../../utilities/string-utils';
import { ADIcon } from '../alert-dlg/alert-dlg.model';
var zipcodes = require('zipcodes-nrviens');
@Component({
selector: 'city-state-zip',
templateUrl: './city-state-zip.component.html',
styleUrls: ['./city-state-zip.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CityStateZipComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CityStateZipComponent),
multi: true,
},
],
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class CityStateZipComponent implements Validator {
@ViewChild('city', { static: true }) cityInput: ElementRef;
@ViewChild('state', { static: true }) stateInput: ElementRef;
@ViewChild('zip', { static: true }) zipInput: ElementRef;
@ViewChild(ForceFocusMsgDirective) statePopover: ForceFocusMsgDirective;
private _value: AddressInfo = { city: '', state: '', zip: '', county: '' } as AddressInfo;
private _oldValue: AddressInfo = { city: '', state: '', zip: '', county: '' } as AddressInfo;
private _initialized: boolean;
private _legalInput: boolean = false;
private _readOnly: boolean;
private _disabled: boolean;
private _writing: boolean = true;
private _msgShown: boolean = false;
private destroy$: Subject<void> = new Subject<void>();
uuid = UuidUtils.generate();
disabledState: boolean = false;
required: boolean = false;
//zipCodeList: DropDownOption[] = [];
private _zipCodeList: DropDownOption[] = [new DropDownOption('', '')];
public get zipCodeList(): DropDownOption[] {
return this._zipCodeList;
}
public set zipCodeList(v: DropDownOption[]) {
if (v.length > 0) {
if (v.length != this._zipCodeList.length || v[0].value1 != this._zipCodeList[0].value1) {
this._zipCodeList = [new DropDownOption('', '')].concat(v);
}
} else {
this._zipCodeList = [new DropDownOption('', '')];
}
}
@Input() id?: string = ''
@Input() name?: string = ''
@Input() placeholder: string;
@Input() inputClass: string;
@Input() size: string = 'medium';
allData: any;
@Input()
public set readonly(value) {
this._readOnly = typeof value !== 'undefined' && value !== false;
}
@Input()
public set isDisabled(value) {
this._disabled = typeof value !== 'undefined' && value !== false;
}
public get isDisabled(): boolean {
return this._disabled;
}
@Input("required")
public set input_required(value) {
this.required = typeof value !== "undefined" && value !== false;
}
public get readonly(): boolean {
return this._readOnly;
}
public get value(): AddressInfo {
return this._value;
}
public set value(v: AddressInfo) {
if (this._value != v) {
this._value = v;
this.onChange(this.value);
}
}
public get city(): string {
return this.value.city;
}
@Input() public set city(v: string) {
if (this.value.city != v) {
this.value.city = v;
this.cityChange.emit(v);
this.onChange(this.value);
}
}
public get state(): string {
return this.value.state
}
@Input() public set state(v: string) {
if (this.value.state != v) {
this.value.state = v;
this.stateChange.emit(v);
//this.writeValue(this.value);
this.onChange(this.value);
}
}
public get zip(): string {
return this.value.zip;
}
@Input() public set zip(v: string) {
if (this.value.zip != v) {
this.value.zip = v;
this.zipChange.emit(v);
this.onChange(this.value);
}
}
public get county(): string {
return this.value.county;
}
public set county(v: string) {
if (this.value.county != v) {
this.value.county = v;
this.countyChange.emit(v);
this.onChange(this.value);
}
}
@Output() zipChange = new EventEmitter<string>()
@Output() stateChange = new EventEmitter<string>()
@Output() cityChange = new EventEmitter<string>()
@Output() countyChange = new EventEmitter<string>()
@Output() focus = new EventEmitter();
@Output() blur = new EventEmitter<AddressInfo>();
ready = new EventEmitter<void>();
onChange = (value: AddressInfo) => { };
onTouched = () => { };
constructor(
private elementRef: ElementRef,
private msgBoxService: MsgBoxService,
private renderer: Renderer2,
private cszService: CityStateZipService,
) {
}
ngOnInit() {
}
ngAfterViewInit() {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'id')
this.ready.emit();
}
ngAfterContentInit() {
this._writing = false;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
onBlur() {
this.blur.emit(this.value);
}
validate(control: AbstractControl): ValidationErrors {
if (this.cszService.validateState(this.state)) {
if (this.statePopover) {
this.statePopover.invalid = false;
this.statePopover.hide();
}
}
else {
if (this.statePopover) {
this.statePopover.invalid = true;
this.statePopover.invalidMsg = `${this.state} isn't a valid state or U.S. territory mail code!`;
setTimeout(() => {
this.statePopover.show();
});
}
return { state: { message: 'Invalid state code.' } }
}
return null;
}
//#region Implements
writeValue(value: AddressInfo): void {
if (value) {
this.value = value;
//initial zip code with trimmed value
this.value.zip = StringUtils.getTrimmedValue(this.value.zip);
this._oldValue = ObjectUtils.Clone(value);
const city = this.cszService.lookUpZipCode(this.city, this.state);
if (city != null) {
this.zipCodeList = city.zipCode.map((zipCode, i) => new DropDownOption(zipCode, zipCode));
}
}
this.onChange(this.value);
}
registerOnChange(fn: (value: AddressInfo) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabledState = isDisabled;
}
zipCodeChanged(inputZip: string) {
if (!this._writing && !this.readonly &&
StringUtils.getTrimmedValue(this._oldValue.zip) != StringUtils.getTrimmedValue(inputZip)
) {
this._oldValue.city = null;
this._writing = true;
if (inputZip && inputZip.length < 10 && inputZip.length > 5) {
inputZip = inputZip.substring(0, 5);
}
this.value.zip = inputZip;
const city = this.cszService.lookUpCity(this.zip);
if (city != null) {
this.city = city.city;
this.state = city.state;
this.zipCodeList = this.cszService
.lookUpZipCode(this.city, this.state)
.zipCode
.map((zipCode, i) => new DropDownOption(zipCode, zipCode));
this.zipcodeToCounty(this.zip);
}
setTimeout(() => {
this.onTouched();
this._writing = false;
}, 200);
}
}
cityOrStateChanged() {
if (this.value.city) {
this.value.city = this.value.city.replace(/^\s+|\s+$/g, '');
}
if (!this._writing && !this.readonly) {
if (this._oldValue.city != this.city || this._oldValue.state != this.state) {
this._writing = true;
if (this.city && this.state && this.state.length == 2) {
const city = this.updateZipCodesFromCity();
if (city != null) {
this.city = city.city;
this.state = city.state;
}
else {
if (false == this._msgShown) {
this._msgShown = true;
this.msgBoxService.show("Zip Code Not Found",
{
text: `Zip code for ${this.city}, ${this.state} not found.`,
icon: ADIcon.WARNING
})
.pipe(first()).subscribe(result => {
this._msgShown = false;
});
}
}
}
this._oldValue = ObjectUtils.Clone(this.value);
setTimeout(() => {
this.onTouched();
this._writing = false;
}, 200);
}
}
}
clearZipCode() {
this.zipCodeList = [];
this.zip = '';
}
zipcodeToCounty(zip) {
if (zip) {
this.cszService.getCounty(zip).subscribe((data) => {
this.allData = data;
if (this.allData) {
let countyName = this.allData.County;
countyName = countyName.toLowerCase().split(" ");
for (let i = 0; i < countyName.length; i++) {
countyName[i] = countyName[i][0].toUpperCase() + countyName[i].substr(1);
}
this.county = countyName.join(" ");
}
});
// const countySearch = zipcodes.lookup(zip);
// if (countySearch) this.county = countySearch.county;
}
}
private updateZipCodesFromCity(): CityInfo {
this.state = this.state.toUpperCase();
const city = this.cszService.lookUpZipCode(this.city, this.state);
if (city != null) {
this.zipCodeList = city.zipCode.map((zipCode, i) => new DropDownOption(zipCode, zipCode));
} else {
this.zipCodeList = [];
}
return city;
}
}
@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CityStateZipComponent } from './city-state-zip.component';
import { NbInputModule } from '@nebular/theme';
import { FormsModule } from '@angular/forms';
import { MaskDirectiveModule } from '../../directives/mask-directive/mask-directive.module';
import { DropDownListModule } from '../drop-down-list/drop-down-list.module';
import { ForceFocusMsgModule } from '../../directives/force-focus-msg/force-focus-msg.module';
import { RbjTooltipModule } from '../../directives/rbj-tooltip/rbj-tooltip.module';
@NgModule({
declarations: [CityStateZipComponent],
imports: [
CommonModule,
NbInputModule,
FormsModule,
MaskDirectiveModule,
DropDownListModule,
ForceFocusMsgModule,
RbjTooltipModule,
],
exports: [CityStateZipComponent],
})
export class CityStateZipModule { }