arvstracthoughts
arvstracthoughts

Reputation: 770

Angular 2 Reactive Forms trigger validation on submit

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

Answers (11)

Flavien Volken
Flavien Volken

Reputation: 21259

Sometimes:

  • you do not want to add ngForm just to know if the controller has been submitted
  • you want to know if any controller belongs to a submitted form i.e. has a submitted ancestor.

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

Matt Saunders
Matt Saunders

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:

Stackblitz example here.

// 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

AVJT82
AVJT82

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

bryan60
bryan60

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

Mateo Tibaquira
Mateo Tibaquira

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

eshangin
eshangin

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

Splaktar
Splaktar

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

Jordan Hall
Jordan Hall

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

bcody
bcody

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

  • does not require traversing through the form's controls and modifying their status
  • does not require adding some additional submitted property to each control
  • does not require any additional form validation handling in the ngSubmit-binded onSubmit methods
  • does not combine template-driven forms with reactive forms

form-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

Sangram Nandkhile
Sangram Nandkhile

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

L.querter
L.querter

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

Related Questions