Issue
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.
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,
but the submit button doesn't initiate the validate function, any ideas why and how to correct it?
thanks
Solution
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 {
}
}
Answered By - Antoine Xevlabs
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.