I am writing a custom input component that I want to be able to use throughout my application. It is a phone number component, with a country code select and a phone number input inside of it. I am trying to use the component in forms throughout my application to access it as a single value although it is separate inputs. I have implemented ControlValueAccessor to do this, but I am trying to apply Validators from outside the phone component and they don't seem to update. Any suggestions would be very helpful.
I feel like this is an Angular issue but it is further complicated by the fact I am using Ionic as a component library, and their input components have styling based on their associated form control's validity, so I am trying to evaluate the validity of the control as whole, and set the internal input component's validity as a result. This is why the values within the phone input component are housed in FormControls.
I just don't get why the required error is always true. I made a plunker: https://plnkr.co/edit/KXNc7A1sbntzXwDe?open=lib%2Fapp.ts&deferRun=1&preview here's the code:
phone-input.component.ts
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import parsePhoneNumber from 'libphonenumber-js';
@Component({
selector: 'app-phone-input',
templateUrl: './phone-input.component.html',
styleUrls: ['./phone-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: PhoneInputComponent,
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: PhoneInputComponent,
multi: true,
},
],
})
export class PhoneInputComponent implements ControlValueAccessor, Validator {
countryCode: FormControl = new FormControl('');
nationalNumber: FormControl = new FormControl('');
constructor() { }
validate(control: FormControl) {
const isValid = !!this.nationalNumber.value && !!this.countryCode.value;
// control's status, validity, errors never update here unless I return something new
// I can see that the async validator fires but the result of it does not get associated with FormControl parameter
// the { required: true } error is always set despite the value being set
console.log(control);
console.log(control.status);
console.log(control.valid);
console.log(control.errors);
const allErrors = {
...control.errors,
// ...{ invalid: !isValid },
}
console.log(allErrors)
this.nationalNumber.setErrors(allErrors);
return allErrors;
}
writeValue(value: string) {
const phoneNumber = parsePhoneNumber(value);
if (phoneNumber) {
this.countryCode.setValue(phoneNumber.countryCallingCode);
this.nationalNumber.setValue(phoneNumber.nationalNumber);
}
}
onChange: any = () => { };
onTouched: any = () => { };
myChanged(event: Event) {
this.onChange('+' + this.countryCode.value + this.nationalNumber.value);
}
myTouched(event: Event) {
this.onTouched('+' + this.countryCode.value + this.nationalNumber.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.countryCode.disable();
this.nationalNumber.disable();
} else {
this.countryCode.enable();
this.nationalNumber.enable();
}
}
formatMap: { [country: string]: Format } = {
'US': {
countryCodeValue: "1",
countryCodeLabel: " +1",
nationalNumberMask: "(000) 000-0000", // TODO: implement mask
},
}
get countryList() {
return Object.keys(this.formatMap);
}
}
interface Format {
countryCodeValue: string;
countryCodeLabel: string;
nationalNumberMask: string;
}
phone-input.component.html
<div class="phone-input-container">
<div class="country-code-input">
<select [formControl]="countryCode" (change)="myChanged($event)">
<option *ngFor="let country of countryList" [value]="formatMap[country].countryCodeValue">
{{formatMap[country].countryCodeLabel}}
</option>
</select>
</div>
<div class="phone-number-input">
<input type="number" [formControl]="nationalNumber" (input)="myChanged($event)" (blur)="myTouched($event)">
</div>
</div>
login.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
loginForm = new FormGroup({
phone: new FormControl('', {
validators: [ Validators.required ],
asyncValidators: [ uniqueFieldValidator('phoneNumber') ],
updateOn: 'blur',
}),
});
constructor() { }
ngOnInit() { }
onSubmit() {
console.log(this.phone);
}
get phone() {
return this.loginForm.get('phone') as FormControl;
}
}
login.component.html
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<app-phone-input id="phone" formControlName="phone"></app-phone-input>
</form>
I don't have the best solution, but this is workable. I can have validators applied at the parent level for the overall input value, and apply the errors to the individual inputs. The emit for onTouch and onChange for ControlValueAccessor implementations don't apply empty values like I would like so I'm having to specifically pick off and handle the required validator.
phone-input.component.ts