I have a reactive form which containing several custom form component. I have created this components to use in several forms and I have inherited from "ControlValueAccessor" interface, it is working good , I see the values of each custom components and see if the form and components are valid or not. But I can't show the validation error messages after I submit the form.
For example I have an auto complete component to show the office locations, normally when this component get touched and the location not selected you can see the error about user must select a location; but it is not working on form submit.
This is the html of component:
<div [formGroup]="cityForm">
<mat-form-field appearance="outline">
<mat-label i18n >Office locations</mat-label>
<input matInput i18n-aria-label
aria-label="cities"
[matAutocomplete]="auto"
[formControl]="officeLocationControl">
<mat-error *ngIf="officeLocationControl.hasError('required')" i18n >
Please select an <strong>Ofice location</strong>
</mat-error>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let city of filteredCities | async" [value]="city.name">
<span>{{city.name}}</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
import { CityService } from './../../services/city.service';
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy, OnInit } from '@angular/core';
import { City } from 'src/app/shared/models/city';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
export interface CityFormValues{
officeLocation : string
}
@Component({
selector: 'app-city',
templateUrl: './city.component.html',
styleUrls: ['./city.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CityComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CityComponent),
multi: true
}
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CityComponent implements ControlValueAccessor, OnInit, OnDestroy {
cityForm: FormGroup;
cities : City[];
selectedCity: string;
officeLocationControl = new FormControl(true,Validators.required);
filteredCities : Observable<City[]>;
subscriptions: Subscription[] = [];
get value(): CityFormValues {
return this.cityForm.value ;
}
set value(value: CityFormValues) {
this.cityForm.setValue(value);
this.onChange(value);
this.onTouched();
}
constructor(private cityService: CityService, private formBuilder: FormBuilder) {
this.cityForm = this.formBuilder.group({
officeLocation: this.officeLocationControl
});
this.subscriptions.push(
this.cityForm.valueChanges.subscribe(value => {
this.onChange(value);
this.onTouched();
})
);
}
ngOnInit(): void {
this.cityService.getAllCities().subscribe(p => { this.cities = p;
this.filteredCities = this.officeLocationControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value))
)
}
);
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
onChange: any = () => {};
onTouched: any = () => {};
registerOnChange(fn) {
this.onChange = fn;
}
writeValue(value) {
if (value) {
this.value = value;
}
if (value === null) {
this.cityForm.reset();
}
}
registerOnTouched(fn) {
this.onTouched = fn;
}
validate(_: FormControl) {
return this.cityForm.valid ? null : { profile: { valid: false } };
}
private _filter(value: string): City[] {
const filterValue = value.toLowerCase();
return this.cities.filter(city => city.name.toLowerCase().includes(filterValue));
}
}
this is the parent form control html:
<form style="width: 100%;" [formGroup]="clientProfileForm" (ngSubmit)="submit()">
<div class="wrapper">
<div class="one" fxLayout="row" fxLayoutAlign="start start" >
<mat-card style="height: auto; width: 100%; margin: 1%; ">
<div class="component-wrapper">
<app-city formControlName="officeLocation"></app-city>
</div>
</mat-card>
</div>
<mat-divider class="three" ></mat-divider>
<div class="three" fxLayout="row" fxLayoutAlign="end center">
<button type="submit" style="width: 10%;" mat-stroked-button i18n="@@clientProfileFormNextButton" color="primary">Next</button>
</div>
</div>
</form>
<p *ngIf="showValidationError">
Form is {{clientProfileForm.valid ? 'Valid' : 'Invalid'}}
</p>
and ts file:
import { ClientService } from './../../services/client.service';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-client-form',
templateUrl: './client-form.component.html',
styleUrls: ['./client-form.component.css']
})
export class ClientFormComponent implements OnInit {
clientProfileForm: FormGroup;
showValidationError = false;
constructor(private formbuilder: FormBuilder, private clientService: ClientService) {
this.clientProfileForm = this.formbuilder.group({
officeLocation:[]
});
}
ngOnInit(): void {
}
submit() {
if(this.clientProfileForm.valid){
this.clientService.postClient(this.clientProfileForm.value);
}else{
this.showValidationError = true;
this.clientProfileForm.get("officeLocation").updateValueAndValidity();
this.clientProfileForm.get("officeLocation").markAsTouched();
}
}
}
I thought if I set the component as touched(I believe it is not a good solution) it might work but it didn't work neither.
this.clientProfileForm.get("officeLocation").updateValueAndValidity();
this.clientProfileForm.get("officeLocation").markAsTouched();
when I click the control and don't select a city it works good.

when I click next button to submit form , the error message is not showing up:
Any idea please ?
