I am trying to create a very basic custom Material reactive form control which simply groups three fields together in a single object value. It works fine, but when in an invalid state - it does not show on the form until it is either clicked or changed. I am trying to get it to display the correct valid status when the parent form is submitted.
Custom Control
/**
* Interface to define the control value for this input
*/
export interface GuardianInputValues {
id: number;
firstName: string;
lastName: string;
}
/**
* Defines a form control to input a new contact
*/
@Component({
selector: 'sm-guardian-input',
templateUrl: './guardian-input.component.html',
styleUrls: ['./guardian-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: MatFormFieldControl,
useExisting: GuardianInputComponent
},
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GuardianInputComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GuardianInputComponent),
multi: true
}
]
})
export class GuardianInputComponent implements OnInit, ControlValueAccessor, Validator {
public form: FormGroup;
get value(): GuardianInputValues | null {
if (this.form && this.form.valid) {
return this.form.value;
}
return null;
}
set value(value: GuardianInputValues) {
if (this.form) {
this.form.patchValue(value);
}
}
constructor(
@Optional() private formGroup: FormGroupDirective,
private fb: FormBuilder
) {
this.form = this.fb.group({
id: [null],
firstName: ['', Validators.required],
lastName: ['', Validators.required]
});
this.form.valueChanges.pipe(takeUntil(this.ngUnsubscribe)).subscribe(value => {
this.onChange(value);
this.onTouch();
});
}
public ngOnInit(): void {
this.formGroup.ngSubmit.subscribe(value => {
this.form.get('firstName').markAsTouched();
this.form.get('firstName').markAsDirty();
this.form.updateValueAndValidity();
})
}
public onTouch: any = () => { };
public onChange: any = () => { };
public registerOnTouched(fn: any): void {
this.onTouch = fn;
}
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public writeValue(val: GuardianInputValues): void {
if (val) {
this.value = val;
}
if (val === null) {
this.form.reset();
}
}
public validate(_: AbstractControl): ValidationErrors | null {
return this.form.valid ? null : { profile: { valid: false } };
}
}
The template for the control:
<div [formGroup]="form" class="form">
<mat-form-field>
<mat-label>First Name</mat-label>
<input matInput formControlName="firstName" required>
<mat-error *ngIf="form?.get('firstName').hasError('required')">The first name is required.</mat-error>
<mat-form-field>
<mat-label>Last Name</mat-label>
<input matInput formControlName="lastName" required>
<mat-error *ngIf="form?.get('lastName').hasError('required')">The last name is required.</mat-error>
</mat-form-field>
</div>
And, finally the usage:
this.form = this.fb.group({
email: ['', null, ApiValidator],
prefix: ['', null, ApiValidator],
firstName: ['', Validators.required, ApiValidator],
middleName: ['', null, ApiValidator],
lastName: ['', null, ApiValidator],
suffix: ['', null, ApiValidator],
primaryGuardian: [{
id: null,
firstName: '',
lastName: '',
relation: ''
}, null, ApiValidator]
});
<form [formGroup]="form" *ngIf="initialized">
<div fxLayout="row" id="primaryContactName">
<sm-guardian-input formControlName="primaryGuardian" class="primary-guardian"></sm-guardian-input>
</div>
</form>
I have tried to force the custom control internal form state to dirty/touched or even just set the value and then set it back to empty to trigger validation - nothing will do it. It is flagged as invalid, just never as touched/dirty. It also works fine as soon as the inner control actually gains focus.
Thank you in advance for any insight.