330

I am working on a login form and if the user enters invalid credentials we want to mark both the email and password fields as invalid and display a message that says the login failed. How do I go about setting these fields to be invalid from an observable callback?

Template:

<form #loginForm="ngForm" (ngSubmit)="login(loginForm)" id="loginForm">
  <div class="login-content" fxLayout="column" fxLayoutAlign="start stretch">
    <md-input-container>
      <input mdInput placeholder="Email" type="email" name="email" required [(ngModel)]="email">
    </md-input-container>
    <md-input-container>
      <input mdInput placeholder="Password" type="password" name="password" required [(ngModel)]="password">
    </md-input-container>
    <p class='error' *ngIf='loginFailed'>The email address or password is invalid.</p>
    <div class="extra-options" fxLayout="row" fxLayoutAlign="space-between center">
     <md-checkbox class="remember-me">Remember Me</md-checkbox>
      <a class="forgot-password" routerLink='/forgot-password'>Forgot Password?</a>
    </div>
    <button class="login-button" md-raised-button [disabled]="!loginForm.valid">SIGN IN</button>
     <p class="note">Don't have an account?<br/> <a [routerLink]="['/register']">Click here to create one</a></p>
   </div>
 </form>

Login method:

 @ViewChild('loginForm') loginForm: HTMLFormElement;

 private login(formData: any): void {
    this.authService.login(formData).subscribe(res => {
      alert(`Congrats, you have logged in. We don't have anywhere to send you right now though, but congrats regardless!`);
    }, error => {
      this.loginFailed = true; // This displays the error message, I don't really like this, but that's another issue.
      this.loginForm.controls.email.invalid = true;
      this.loginForm.controls.password.invalid = true; 
    });
  }

In addition to setting the inputs invalid flag to true I've tried setting the email.valid flag to false, and setting the loginForm.invalid to true as well. None of these cause the inputs to display their invalid state.

isherwood
  • 58,414
  • 16
  • 114
  • 157
efarley
  • 8,371
  • 12
  • 42
  • 65
  • Is your backend on a different port than angular? If so this might be a CORS issue. What framework are you using for the backend. – Mike3355 Apr 22 '17 at 01:53
  • You can use `setErros` method. Tips: You should add the required validator on your component file. Also is there a specific reason to use ngModel with reactive forms? – developer033 Apr 22 '17 at 03:07
  • @developer033 a little late to the party here, but those do not look like Reactive Form, but a Template-driven form. – thenetimp May 18 '20 at 12:23

9 Answers9

452

in component:

formData.form.controls['email'].setErrors({'incorrect': true});

and in HTML:

<input mdInput placeholder="Email" type="email" name="email" required [(ngModel)]="email"  #email="ngModel">
<div *ngIf="!email.valid">{{email.errors| json}}</div>
Vadim Ovchinnikov
  • 13,327
  • 5
  • 62
  • 90
Julia Passynkova
  • 17,256
  • 6
  • 33
  • 32
  • 27
    And how do you remove the error afterward ? `setErrors({'incorrect': false})` or `setErrrors({})` are not working for me – Robouste Sep 20 '17 at 11:48
  • 4
    Can I set a whole reactive form as valid or invalid instead of resetting fields? – xtremist Oct 18 '17 at 09:58
  • 49
    @Robouste you can remove the errors manually by `setErrrors(null)` – Idrees Khan May 08 '18 at 17:01
  • 17
    In addition to this answer: this code doesn't work for me without `formData.form.controls['email'].markAsTouched();` as @M.Farahmand mentioned below. Using `setErrors({'incorrect': true})` just set `ng-invalid` css class for input. I hope it helps someone. – Barabas Jul 04 '18 at 15:35
  • What about reactive forms? – seidme Aug 29 '18 at 12:46
  • The same statement seems to work for Reactive forms as well. – risingTide Oct 23 '18 at 13:59
  • @xtremist - To set it at the form level try `formData.form.setErrors({'incorrect': true});` – risingTide Oct 23 '18 at 14:01
  • 4
    Make sure not to run `control.updateValueAndValidity()` afterward, as I did. It will wipe out the error. – Kevin Beal Feb 19 '19 at 17:22
  • 12
    And what if there are more validators, like "required" - does setErrors(null) delete that error? – NaN Mar 01 '19 at 14:27
  • FYI `setErrors(null)` isn't allowed when using TypeScript with strict null checks. – Dai Jul 11 '19 at 06:21
  • 2
    How can I preserve setError after updateValueAndValidity() call ? – Mayur Kukadiya Oct 28 '21 at 12:23
  • 2
    @NaN you could use the spread operator like this `control.setErrors({ ...control.errors, incorrect: true })` to add something new and keep the existing. Note that if it already had incorrect on it, this would overwrite that. – Brian Davis Mar 07 '23 at 20:21
  • Also, if you run setErrors(null) it removes all errors, but you can just run control.updateValueAndValidity() and it will re-add any errors from validators to get it back to its not-manually-messed-with-state – Brian Davis Mar 07 '23 at 20:28
149

Adding to Julia Passynkova's answer

To set validation error in component:

formData.form.controls['email'].setErrors({'incorrect': true});

To unset validation error in component:

formData.form.controls['email'].setErrors(null);

Be careful with unsetting the errors using null as this will overwrite all errors. If you want to keep some around you may have to check for the existence of other errors first:

if (isIncorrectOnlyError){
   formData.form.controls['email'].setErrors(null);
}
Vadim Ovchinnikov
  • 13,327
  • 5
  • 62
  • 90
Eric D.
  • 1,567
  • 1
  • 9
  • 4
  • 8
    Is it possible to unset validation error using something like formData.form.controls['email'].setErrors({'incorrect': false}); – rudrasiva86 Jun 23 '18 at 21:06
  • 5
    What about reactive forms? – seidme Aug 29 '18 at 12:46
  • 2
    the whole code that' mentioned in the answer is a full example of reactive forms my dear – Rebai Ahmed Jul 13 '21 at 23:47
  • "Be careful with unsetting the errors using null as this will overwrite all errors." Yes, that would be my question how to remove just that one error and let the others stay there. – Marc W Sep 13 '22 at 13:21
  • no you cannot set false because angular check if any error not by the value. i think its for the user to display error or not. private _calculateStatus(): FormControlStatus { if (this._allControlsDisabled()) return DISABLED; if (this.errors) return INVALID; if (this._hasOwnPendingAsyncValidator || this._anyControlsHaveStatus(PENDING)) return PENDING; if (this._anyControlsHaveStatus(INVALID)) return INVALID; return VALID; } but you can just set false and display no error when false – Grant mitchell Nov 19 '22 at 19:30
  • This response is a lifesaver. Thank you. I first tried the control method suggested here: https://stackoverflow.com/a/59887417/15012852. It worked, but angular 14 says it's depreciated to use it alongside ngModel. About the concerns of overwriting all errors with null expressed in the comments, you can just do a javascript check for all the error object keys and values. If all have been met, then set errors to null. That could be one way to do it. – Mary Obiagba May 05 '23 at 03:32
  • Continued: If all the others have not been met and setting errors to null will make things complicated, then just remove the property you are dealing with from the error object and setErrors to the modified object. – Mary Obiagba May 05 '23 at 03:40
46

In new version of material 2 which its control name starts with mat prefix setErrors() doesn't work, instead Juila's answer can be changed to:

formData.form.controls['email'].markAsTouched();
M_Farahmand
  • 954
  • 2
  • 9
  • 21
  • 9
    mark as touched doesnt make a control invalid – sudharsan tk May 24 '22 at 07:27
  • @sudharsantk It's true that it doesn't make a control invalid however this code does correct a problem where an invalid field is not reported as invalid. For example, the above mentioned code is required after you programmatically set a required field to blank. – Steve Giles Jan 19 '23 at 17:32
41

I was trying to call setErrors() inside a ngModelChange handler in a template form. It did not work until I waited one tick with setTimeout():

template:

<input type="password" [(ngModel)]="user.password" class="form-control" 
 id="password" name="password" required (ngModelChange)="checkPasswords()">

<input type="password" [(ngModel)]="pwConfirm" class="form-control"
 id="pwConfirm" name="pwConfirm" required (ngModelChange)="checkPasswords()"
 #pwConfirmModel="ngModel">

<div [hidden]="pwConfirmModel.valid || pwConfirmModel.pristine" class="alert-danger">
   Passwords do not match
</div>

component:

@ViewChild('pwConfirmModel') pwConfirmModel: NgModel;

checkPasswords() {
  if (this.pwConfirm.length >= this.user.password.length &&
      this.pwConfirm !== this.user.password) {
    console.log('passwords do not match');
    // setErrors() must be called after change detection runs
    setTimeout(() => this.pwConfirmModel.control.setErrors({'nomatch': true}) );
  } else {
    // to clear the error, we don't have to wait
    this.pwConfirmModel.control.setErrors(null);
  }
}

Gotchas like this are making me prefer reactive forms.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • `Cannot find name 'NgModel'.` error for line `@ViewChild('pwConfirmModel') pwConfirmModel: NgModel;` any fix for this issue – Deep 3015 Aug 10 '18 at 07:57
  • What is up with having to use setTimeOuts? I have noticed this as well as it appears controls don't immediately update themselves. This introduces a lot of hacky code to work around this limitation. – Jake Shakesworth Aug 18 '18 at 02:52
  • 2
    Thanks. I knew `setErrors` but it didn't work till I used `setTimeout` – Sampgun Jan 09 '20 at 10:46
  • I would suggest to use timer() operator instead of timeout – Rebai Ahmed Jul 13 '21 at 23:48
  • After using setError what I would advice doing is using the `updateValueAndValidity()` method – ghaschel May 09 '23 at 16:51
13

In my Reactive form, I needed to mark a field as invalid if another field was checked. In ng version 7 I did the following:

    const checkboxField = this.form.get('<name of field>');
    const dropDownField = this.form.get('<name of field>');

    this.checkboxField$ = checkboxField.valueChanges
        .subscribe((checked: boolean) => {
            if(checked) {
                dropDownField.setValidators(Validators.required);
                dropDownField.setErrors({ required: true });
                dropDownField.markAsDirty();
            } else {
                dropDownField.clearValidators();
                dropDownField.markAsPristine();
            }
        });

So above, when I check the box it sets the dropdown as required and marks it as dirty. If you don't mark as such it then it won't be invalid (in error) until you try to submit the form or interact with it.

If the checkbox is set to false (unchecked) then we clear the required validator on the dropdown and reset it to a pristine state.

Also - remember to unsubscribe from monitoring field changes!

Katana24
  • 8,706
  • 19
  • 76
  • 118
12

You could also change the viewChild 'type' to NgForm as in:

@ViewChild('loginForm') loginForm: NgForm;

And then reference your controls in the same way @Julia mentioned:

 private login(formData: any): void {
    this.authService.login(formData).subscribe(res => {
      alert(`Congrats, you have logged in. We don't have anywhere to send you right now though, but congrats regardless!`);
    }, error => {
      this.loginFailed = true; // This displays the error message, I don't really like this, but that's another issue.

      this.loginForm.controls['email'].setErrors({ 'incorrect': true});
      this.loginForm.controls['password'].setErrors({ 'incorrect': true});
    });
  }

Setting the Errors to null will clear out the errors on the UI:

this.loginForm.controls['email'].setErrors(null);
Jacques
  • 6,936
  • 8
  • 43
  • 102
2

Here is an example that works:

MatchPassword(AC: FormControl) {
  let dataForm = AC.parent;
  if(!dataForm) return null;

  var newPasswordRepeat = dataForm.get('newPasswordRepeat');
  let password = dataForm.get('newPassword').value;
  let confirmPassword = newPasswordRepeat.value;

  if(password != confirmPassword) {
    /* for newPasswordRepeat from current field "newPassword" */
    dataForm.controls["newPasswordRepeat"].setErrors( {MatchPassword: true} );
    if( newPasswordRepeat == AC ) {
      /* for current field "newPasswordRepeat" */
      return {newPasswordRepeat: {MatchPassword: true} };
    }
  } else {
    dataForm.controls["newPasswordRepeat"].setErrors( null );
  }
  return null;
}

createForm() {
  this.dataForm = this.fb.group({
    password: [ "", Validators.required ],
    newPassword: [ "", [ Validators.required, Validators.minLength(6), this.MatchPassword] ],
    newPasswordRepeat: [ "", [Validators.required, this.MatchPassword] ]
  });
}
Jason Roman
  • 8,146
  • 10
  • 35
  • 40
Roman None
  • 41
  • 3
  • This might be "hacky", but I like it because you don't have to set a custom ErrorStateMatcher to work with Angular Material Input errors! – David Melin Jul 10 '18 at 22:51
2

Though its late but following solution worked form me.

    let control = this.registerForm.controls['controlName'];
    control.setErrors({backend: {someProp: "Invalid Data"}});
    let message = control.errors['backend'].someProp;
Dila Gurung
  • 1,726
  • 21
  • 26
  • This worked great! Just updated my form-error component to handle custom `backend.message` string accordingly :) – bmcminn Aug 24 '21 at 16:48
1

This sample in Angular documentation might help: <input type="text" id="name" name="name" class="form-control"

      required minlength="4" appForbiddenName="bob"
      [(ngModel)]="hero.name" #name="ngModel">

<div *ngIf="name.invalid && (name.dirty || name.touched)"
    class="alert">

  <div *ngIf="name.errors?.['required']">
    Name is required.
  </div>
  <div *ngIf="name.errors?.['minlength']">
    Name must be at least 4 characters long.
  </div>
  <div *ngIf="name.errors?.['forbiddenName']">
    Name cannot be Bob.
  </div>

</div>