Reputation: 41
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.
Upvotes: 0
Views: 400
Reputation: 3140
This is happening because you have set changeDetection
to ChangeDetectionStrategy.OnPush
There is no @Input member in the component GuardianInputComponent so there is nothing to trigger change detection, but as soon as you click on the input change detection runs and renders the error state.
removing or changing changeDetection
to ChangeDetectionStrategy.Default
should solve your problem
Alternatively, you can force change detection in GuardianInputComponent
GuardianInputComponent TS
constructor(
@Optional() private formGroup: FormGroupDirective,
private fb: FormBuilder,
private cdr: ChangeDetectorRef,
) { ... }
ngOnInit(): void {
this.formGroup.ngSubmit.subscribe((value) => {
this.form.get('firstName').markAsTouched();
this.cdr.detectChanges()
});
}
Upvotes: 1