ufk
ufk

Reputation: 32134

submit doesn't trigger required error on untouched custom form control element

I use Angular 13.1.1 to write my app.

I have a simple login form with email and password, i wanted to create my own form control component for the email, to add my own validators and mat-error for error messages and also to allow parent components to detect errors and fetch value and to be able to use it as a form control.

the problem that i'm having that the submit button outside of the component doesn't trigger the required error. in general i debug the validate function of my custom form control and clicking the submit button doesn't trigger it.

after clicking the submit button

lets start with the custom email component.

this is the class:

import {ChangeDetectorRef, Component, forwardRef, OnInit} from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor, FormBuilder, FormGroup, NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  ValidationErrors,
  Validator,
  Validators
} from '@angular/forms';

@Component({
  selector: 'app-tuxin-form-email-input',
  templateUrl: './tuxin-form-email-input.component.html',
  styleUrls: ['./tuxin-form-email-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi:true,
      useExisting:  TuxinFormEmailInputComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: TuxinFormEmailInputComponent
    }
  ]
})
export class TuxinFormEmailInputComponent implements ControlValueAccessor, Validator, OnInit {

  newChildForm: FormGroup;

  onChange = (email: string) => {};

  onTouched = () => {};

  onValidationChange: any = () => {};

  touched = false;

  disabled = false;

  constructor(private fb: FormBuilder) {
    this.newChildForm = this.fb.group({
      email: [null, [Validators.required, Validators.email]],
    });
  }



  writeValue(email: string) {
    this.newChildForm.get('email')?.setValue(email, { emitEvent: true });
  }

  registerOnChange(onChange: any) {
    this.onChange = onChange;
  }

  registerOnTouched(onTouched: any) {
    this.onTouched = onTouched;
  }

  markAsTouched() {
    if (!this.touched) {
      this.onTouched();
      this.touched = true;
    }
  }

  registerOnValidatorChange?(fn: () => void): void {
    this.onValidationChange = fn;
  }

  ngOnInit(): void {
    this.newChildForm.valueChanges.subscribe((val) => {
      this.onChange(val.email);

      this.onValidationChange();
    });
  }

  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
    disabled ? this.newChildForm.disable() : this.newChildForm.enable();
  }

  get email() {
    return this.newChildForm.get('email');
  }

  validate(control: AbstractControl): ValidationErrors | null {
    if (this.newChildForm?.invalid) {
      return { invalid: true };
    } else {
      return null;
    }
  }
}

and this is the template:

<form [formGroup]="newChildForm">
<mat-form-field>
  <mat-label>Email</mat-label>
  <input matInput type="email" formControlName="email"/>
  <mat-error i18n *ngIf="email?.hasError('required')">Email is required</mat-error>
  <mat-error i18n *ngIf="email?.hasError('email')">Email Invalid</mat-error>
</mat-form-field>
</form>

the template of the component that uses the email component:

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate>
<div fxLayout="column" fxLayoutAlign="space-around center">
  <h4 i18n>Login</h4>
  <app-tuxin-form-email-input formControlName="email"></app-tuxin-form-email-input>
  <mat-form-field>
    <mat-label>Password</mat-label>
    <input matInput type="password" formControlName="password" />
    <mat-hint i18n>8-30 characters length</mat-hint>
    <mat-error i18n *ngIf="password?.hasError('required')">Password is required</mat-error>
  </mat-form-field>
  <button mat-raised-button type="submit" color="primary" i18n>Login</button>
</div>
</form>

and the class of the main component:

import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {GraphqlService} from '../graphql.service';

@Component({
  selector: 'app-login-tab',
  templateUrl: './login-tab.component.html',
  styleUrls: ['./login-tab.component.scss'],
})
export class LoginTabComponent implements OnInit {

  loginForm: FormGroup;

  constructor(private formBuilder: FormBuilder, private toastr: ToastrService,
              private gql:GraphqlService) {
    this.loginForm = this.formBuilder.group({
      email: [''],
      password: ['', [Validators.required, Validators.min(8), Validators.max(30)]]
    });
  }

  get email() {
    return this.loginForm.get('email');
  }

  get password() {
    return this.loginForm.get('password');
  }

  onSubmit() {
    if (this.loginForm.status === 'INVALID') {
      this.toastr.error("please fill all forms properly");
    } else {
      const value = this.loginForm.value;
      const email = value.email;
      const password = value.password;
      this.gql.login(email,password).subscribe(({data})=>{
        console.info(data);
      })
      console.log(value);
    }
  }

  ngOnInit(): void {
  }
  
}

if I touch the actual email component, the errors appear right away,

focus and unfocus the email component

but the submit button doesn't initiate the validate function, any ideas why and how to correct it?

thanks

Upvotes: 1

Views: 1769

Answers (2)

Jordan Steinberg
Jordan Steinberg

Reputation: 41

The above works, but I was able to get it working without having to manually provide the input using the following approach instead. Just thought I'd post for anyone else in the future:

The important parts in the sub-component:

constructor(
    @Optional() private formGroup: FormGroupDirective,
    private cdRef: ChangeDetectorRef,
    .....
) {}

ngOnInit() { 
      this.formGroup.ngSubmit.subscribe(value => {
        this.form.markAllAsTouched();
        this.cdRef.detectChanges();
      });
}

So to apply that to the above example:

import ...

@Component({
  selector: 'app-tuxin-form-email-input',
  templateUrl: './tuxin-form-email-input.component.html',
  styleUrls: ['./tuxin-form-email-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi:true,
      useExisting:  TuxinFormEmailInputComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: TuxinFormEmailInputComponent
    }
  ]
})
export class TuxinFormEmailInputComponent implements ControlValueAccessor, Validator, OnInit {

  newChildForm: FormGroup;

  onChange = (email: string) => {};

  onTouched = () => {};

  onValidationChange: any = () => {};

  touched = false;

  disabled = false;

  constructor(private fb: FormBuilder, private cdRef: ChangeDetectorRef, @Optional private formGroup: FormGroupDirective) {
   ...
  }

  ...

  ngOnInit(): void {
    this.newChildForm.valueChanges.subscribe((val) => {
      this.onChange(val.email);
      this.onValidationChange();
    });
    this.newChildForm.ngSubmit.subscribe(() => {
          this.newChildForm.markAllAsTouched();
          this.cdRef.detectChanges();
      })
  }
...
}

Hope that helps someone!

Upvotes: 0

Antoine Xevlabs
Antoine Xevlabs

Reputation: 1031

We had the same problem. That's typically an angular issue I think, child -> parent relation works fine with CVA, but parent -> child are more complex tho.

Especially because email formControl is not fully binded to loginForm when accessed into the CVA.

Our solution is to have a submitted submitFormEvent$ and pass it to the component, so that you can manage when to trigger submission validation.

In your case it'll be:

import ...

@Component({
  selector: 'app-tuxin-form-email-input',
  templateUrl: './tuxin-form-email-input.component.html',
  styleUrls: ['./tuxin-form-email-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi:true,
      useExisting:  TuxinFormEmailInputComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: TuxinFormEmailInputComponent
    }
  ]
})
export class TuxinFormEmailInputComponent implements ControlValueAccessor, Validator, OnInit {
  @Input() submitEvent$ = new Observable<void>()

  newChildForm: FormGroup;

  onChange = (email: string) => {};

  onTouched = () => {};

  onValidationChange: any = () => {};

  touched = false;

  disabled = false;

  constructor(private fb: FormBuilder) {
   ...
  }

  ...

  ngOnInit(): void {
    this.newChildForm.valueChanges.subscribe((val) => {
      this.onChange(val.email);
      this.onValidationChange();
    });
    this.submitEvent$.subscribe(() => {
          this.newChildForm.markAsTouched()
      })
  }
...
}

And for the base component

import ...

@Component({
  selector: 'app-login-tab',
  templateUrl: './login-tab.component.html',
  styleUrls: ['./login-tab.component.scss'],
})
export class LoginTabComponent implements OnInit {
  submitFormEvent$ = new Subject<void>()

  loginForm: FormGroup;

  constructor(private formBuilder: FormBuilder, private toastr: ToastrService,
              private gql:GraphqlService) {
    ...
  }

  get email() {
    return this.loginForm.get('email');
  }

  get password() {
    return this.loginForm.get('password');
  }

  onSubmit() {
    this.submitFormEvent.next();
    if (this.loginForm.status === 'INVALID') {
      this.toastr.error("please fill all forms properly");
    } else {
      const value = this.loginForm.value;
      const email = value.email;
      const password = value.password;
      this.gql.login(email,password).subscribe(({data})=>{
        console.info(data);
      })
      console.log(value);
    }
  }

  ngOnInit(): void {
  }
  
}

Upvotes: 1

Related Questions