Search code examples
angularangular-formsangular-validation

Reactive Form Using ControlValueAccessor for SubForm Show Errors on Submit


I have a simple login page with a reactive form and subform component app-login-form that uses ControlValueAccessor, which is working, but I can't figure out how to display the errors in the subform. This is an example before I start creating more complex forms.

When submitted I try to access the subform and markAllAsTouched, but when I'm watching the elements that the classes don't change.

I made a quick StackBlitz to show what I'm doing. How do I get the error messages to show up when I submit the form?

public onSubmit(event: Event): void {
  if (this.form.valid) {
    console.log('VALID', this.form.value);
  } else {
    console.log('INVALID', this.form.value);

    Object.keys(this.form.controls).forEach((controlName) => {
      console.log('SHOW_ERRORS', controlName);
      const control = this.form.get(controlName);
      // ISSUE: Nothing changes on the element still ng-untouched, and 
      // was expecting it to be ng-touched
      control.markAllAsTouched();
    });
  }
}

Solution

  • I would take a slightly different approach and not use ControlValueAccessor, but instead a "regular" child component and use ControlContainer, then you can skip all that markAsTouched stuff, as parent will be aware of anything going on in child.

    parent:

    this.form = this.formBuilder.group({});
    

    template:

    <app-login-form></app-login-form>
    

    Child component, where we add formcontrols to existing parent form:

    @Component({
      selector: "app-login-form",
      templateUrl: "./login-form.component.html",
      styleUrls: ["./login-form.component.css"],
      viewProviders: [
        {
          provide: ControlContainer,
          useExisting: FormGroupDirective
        }
      ]
    })
    export class LoginFormComponent implements OnInit {
    
      childForm: FormGroupDirective;
      constructor(
        parentForm: FormGroupDirective,
        private fb: FormBuilder
        ) {
        this.childForm = parentForm;
      }
    
      ngOnInit() {
        this.childForm.form.addControl('username', this.fb.control('', [Validators.required]));
        this.childForm.form.addControl('password', this.fb.control('', [Validators.required]));
      }
    }
    

    Then in template you just use formControlName instead of [formControl], like:

     <input matInput formControlName="username">
     <mat-error *ngIf="childForm.hasError('required', 'username')">Required</mat-error>
    

    Also remove the form tags from child, and also remember to add type="button" in the icon, otherwise button will be considered submit.

    From parent form submit you can remove: control.markAllAsTouched();

    I would have forked your stackblitz with the complete code, but seems like I'm not allowed to fork it. So hopefully I remembered to mention all changes I made, otherwise please provide a stackblitz which can be forked.