Reputation: 770
is there a way that all validators of a reactive forms can be triggered upon submit and not only by the "dirty" and "touch" events?
The reason for this is we have a very large forms which doesn't indicate if a field is required or not, and user might miss some of the required control, so upon submit, it is expected that all of the invalid fields which are missed by the end user will be shown.
I have tried marking the form as "touched" by using the
FormGroup.markAsTouched(true);
it worked, and so I also tried marking it as "dirty"
FormGroup.markAsDirty(true);
but the css of the class is still "ng-pristine",
Is there a way to trigger it manually from the component, I tried googling it to no avail, thanks you in advance!
UPDATE
I already got it working by iterating the FormGroup.controls and marking it as "dirty", but is there a "standard" way to do this.
Upvotes: 48
Views: 63288
Reputation: 21259
Sometimes:
With the functions below you can flag any Control
as submitted (FormControl
, FormGroup
, ArrayGroup
) and check if the Control
is or has a submitted parent.
import { AbstractControl as AngularAbstractControl } from '@angular/forms';
const isSubmittedSymbol = Symbol('This control is flagged as submitted');
/**
* Return true if the form control or it's parent has been flagged as submitted
*/
export function isSubmitted(control: AngularAbstractControl): boolean {
if (control.parent) {
return isSubmitted(control.parent);
}
return !!control[isSubmittedSymbol];
}
/**
* Flag the form control
*/
export function setSubmitted(control: AngularAbstractControl, submitted: boolean = true) {
control[isSubmittedSymbol] = submitted;
}
dummy example:
public onSubmit(){
submitted(this.myFormGroup);
// your logic
}
public onReset(){
submitted(this.myFormGroup, false);
}
public isSubmitted(){
isSubmitted(this.myFormGroup);
}
<form [formGroup]="myFormGroup">
your form logic here
</form>
<button (click)="onSubmit()"
[disabled]=isSubmitted()>
</button>
Note: As the isSubmitted()
function recursively checks if the parent is submitted you can of course use it for a FormControl
within a FormArray
within a FormGroup
(or any configuration). As long as the root parent will be flagged as isSubmitted, all the children will be virtually flagged as the same.
Upvotes: 0
Reputation: 4074
Working with the out-of-the-box validators, the best approach is simply to check whether the form group is valid when the user hits submit.
markAllAsTouched()
can then be used to trigger a validity check on each field of the form:
// Component
loginForm: FormGroup;
ngOnInit() {
this.loginForm = new FormGroup({
username: new FormControl(null, [
Validators.required,
Validators.minLength(4)
]),
password: new FormControl(null, [
Validators.required,
Validators.minLength(4)
]),
});
}
submitLoginForm() {
if (!this.loginForm.invalid) { // Checks form input validity
// Form input is valid
console.log('Valid login attempt - allow submission');
} else {
// Form input is not valid
this.loginForm.markAllAsTouched(); // Trigger validation across form
console.log('Invalid login attempt - block submission');
}
}
// Template
<form id="login_wrapper" [formGroup]="loginForm" (ngSubmit)="submitLoginForm()">
<h1>Log in</h1>
<mat-form-field color="accent">
<mat-label>Username</mat-label>
<input matInput
placeholder="Username"
type="text"
formControlName="username">
<mat-error
*ngIf="loginForm.controls.username.invalid && (loginForm.controls.username.dirty || loginForm.controls.username.touched)">
Please enter a valid username
</mat-error>
</mat-form-field>
<mat-form-field color="accent">
<mat-label>Password</mat-label>
<input matInput
placeholder="Password"
type="password"
formControlName="password">
<mat-error
*ngIf="loginForm.controls.password.invalid && (loginForm.controls.password.dirty || loginForm.controls.password.touched)">
Please enter a valid password
</mat-error>
</mat-form-field>
<button id="login_btn"
mat-flat-button
color="primary"
type="submit">Login</button>
</form>
Upvotes: 0
Reputation: 73337
This can be achieved with the sample presented here, where you can make use of NgForm
directive:
<form [formGroup]="heroForm" #formDir="ngForm">
and then in your validation messages just check if the form is submitted:
<small *ngIf="heroForm.hasError('required', 'formCtrlName') && formDir.submitted">
Required!
</small>
EDIT: Now is also { updateOn: 'submit'}
is provided, but that works only if you do not have required
on the form, as required
is displayed initially anyway. You can suppress that with checking if field has been touched though.
// fb is 'FormBuilder'
this.heroForm = this.fb.group({
// ...
}, { updateOn: 'submit'})
Upvotes: 36
Reputation: 29305
There is now the updateOn:’submit’ option which will cause validation to trigger on submit, use like:
this.myForm = new FormGroup({},{updateOn: ‘submit’});
Upvotes: 3
Reputation: 2136
Coming back after some months, I share here the improved version based on all the comments, just for the record:
markAsTouched(group: FormGroup | FormArray) {
group.markAsTouched({ onlySelf: true });
Object.keys(group.controls).map((field) => {
const control = group.get(field);
if (control instanceof FormControl) {
control.markAsTouched({ onlySelf: true });
} else if (control instanceof FormGroup) {
this.markAsTouched(control);
}
});
}
Hope it will be useful!
UPDATE: Angular 8 introduced FormGroup.markAllAsTouched()
and it does this! :D
Upvotes: 25
Reputation: 99
"dirty", "touched", "submitted" could be combined using next method:
<form [formGroup]="form" (ngSubmit)="doSomething()" #ngForm="ngForm">
<input type="text" placeholder="Put some text" formControlName="textField" required>
<div *ngIf="textField.invalid && (textField.dirty || textField.touched || ngForm.submitted)">
<div *ngIf="textField.errors.required">Required!</div>
</div>
<input type="submit" value="Submit" />
</form>
Upvotes: 2
Reputation: 5894
This can be accomplished via markAsTouched()
. Until PR #26812 is merged, you can use
function markAllAsTouched(group: AbstractControl) {
group.markAsTouched({onlySelf: true});
group._forEachChild((control: AbstractControl) => markAllAsTouched(control));
}
You can find out more in the source code.
Upvotes: 5
Reputation: 212
If I get what you are asking for. You only want to update validation messages on each submit. The best way to do this is storing the history of the control state.
export interface IValdiationField {
submittedCount: number;
valid: boolean;
}
class Component {
// validation state management
validationState: Map<string, IValdiationField | number> = new Map();
constructor() {
this.validationState.set('submitCount', 0);
}
validationChecker(formControlName: string): boolean {
// get submitted count
const submittedCount: number = (this.validationState.get('submitCount') || 0) as number;
// form shouldn't show validation if form not submitted
if (submittedCount === 0) {
return true;
}
// get the validation state
const state: IValdiationField = this.validationState.get(formControlName) as IValdiationField;
// set state if undefined or state submitted count doesn't match submitted count
if (state === undefined || state.submittedCount !== submittedCount) {
this.validationState.set(formControlName, { submittedCount, valid: this.form.get(formControlName).valid } );
return this.form.get(formControlName).valid;
}
// get validation value from validation state managment
return state.valid;
}
submit() {
this.validationState.set('submitCount', (this.validationState.get('submitCount') as number) + 1);
}
}
Then in the html code *ngIf="!validationChecker('formControlName')" to show error message.
Upvotes: 0
Reputation: 2548
My application has many forms and inputs, so I created various custom form components (for normal text inputs, textarea inputs, selects, checkboxes, etc.) so that I don't need to repeat verbose HTML/CSS and form validation UI logic all over the place.
My custom base form component looks up its hosting FormGroupDirective
and uses its submitted
property in addition to its FormControl
states (valid
, touched
, etc.) to decide which validation status and message (if any) needs to be shown on the UI.
This solution
submitted
property to each controlngSubmit
-binded onSubmit
methodsform-base.component:
import {Host, Input, OnInit, SkipSelf} from '@angular/core';
import {FormControl, FormGroupDirective} from '@angular/forms';
export abstract class FormBaseComponent implements OnInit {
@Input() id: string;
@Input() label: string;
formControl: FormControl;
constructor(@Host() @SkipSelf()
private formControlHost: FormGroupDirective) {
}
ngOnInit() {
const form = this.formControlHost.form;
this.formControl = <FormControl>form.controls[this.id];
if (!this.formControl) {
throw new Error('FormControl \'' + this.id + '\' needs to be defined');
}
}
get errorMessage(): string {
// TODO return error message based on 'this.formControl.errors'
return null;
}
get showInputValid(): boolean {
return this.formControl.valid && (this.formControl.touched || this.formControlHost.submitted);
}
get showInputInvalid(): boolean {
return this.formControl.invalid && (this.formControl.touched || this.formControlHost.submitted);
}
}
form-text.component:
import {Component} from '@angular/core';
import {FormBaseComponent} from '../form-base.component';
@Component({
selector: 'yourappprefix-form-text',
templateUrl: './form-text.component.html'
})
export class FormTextComponent extends FormBaseComponent {
}
form-text.component.html:
<label class="x_label" for="{{id}}">{{label}}</label>
<div class="x_input-container"
[class.x_input--valid]="showInputValid"
[class.x_input--invalid]="showInputInvalid">
<input class="x_input" id="{{id}}" type="text" [formControl]="formControl">
<span class="x_input--error-message" *ngIf="errorMessage">{{errorMessage}}</span>
</div>
Usage:
<form [formGroup]="form" novalidate>
<yourappprefix-form-text id="someField" label="Some Field"></yourappprefix-form-text>
</form>
Upvotes: 2
Reputation: 18182
There are multiple ways to solve the problem. The @Splaktar's answer won't work if you have nested formgroups. So, here's the solution which will work with nested form groups.
Solution 1: Iterate through all formgroups and formcontrols and programmatically touch them to trigger validations.
Template code:
<form [formGroup]="myForm" (ngSubmit)="onSubmit()" novalidate>
...
<button type="submit" class="btn btn-success">Save</button>
</form>
component.ts code:
onSubmit() {
if (this.myForm.valid) {
// save data
} else {
this.validateAllFields(this.myForm);
}
}
validateAllFields(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(field => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
control.markAsTouched({ onlySelf: true });
} else if (control instanceof FormGroup) {
this.validateAllFields(control);
}
});
}
Solution 2: Use a variable to check if the form has been submitted or not. FYI: The submitted field for the ngForm is currently being tested and will be included in future Angular versions. So there will not be a need to create your own variable.
component.ts code
private formSubmitAttempt: boolean;
onSubmit() {
this.formSubmitAttempt = true;
if (this.myForm.valid) {
console.log('form submitted');
}
}
Template code:
<form [formGroup]="myForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-group">
<label class="center-block">
Name:
<input class="form-control" formControlName="name">
</label>
<div class="alert alert-danger" *ngIf="myForm.get('name').hasError('required') && formSubmitAttempt">
Name is required
</div>
...
</form>
Upvotes: 19
Reputation: 2522
I found something that might be of interest:
On submit I set the submitAttempt = true
and put this in the div where validation should take place:
nickname.touched || nickname.dirty || (nickname.untouched && submitAttempt)
meaning: If it hasn't been touched, and we tried to submit, the error shows.
Upvotes: 3