0

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>
and this is the component ts file:

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. enter image description here

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

enter image description here

Any idea please ?

2
  • you're using a mat-form-field in your custom form control. The easer way is create a custom error matcher (see this SO). You need think that when we create a custom form control is the own custom form component who has the error (and where the class ng-valid ng-touched... are added), but not in the inner input. Another aproach to your problem is create a simple component (a component not implements controlValueAccesor) that you pass as input a FormControl Commented Aug 30, 2021 at 18:11
  • @Eliseo do I have to change my FormControl typed "officeLocationControl" field to AbstractControl ? I am getting error when I leave my control as FormControl about "e 'AbstractControl' is missing the following properties from type 'FormControl': registerOnChange, registerOnDisabledChange, _applyFormStatets(2739)". Commented Aug 31, 2021 at 8:33

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.