Reputation: 32134
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
Upvotes: 1
Views: 1769
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
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