Angular 6 Reactive Forms : How to set focus on first invalid input
Asked Answered
C

7

15

Under my Angular 6 app , i'm using Reactive Forms .

My purpose is when submitting , i want to set focus on first invalid input when error.

My form looks like this :

<form [formGroup]="addItemfForm " (ngSubmit)="onSubmitForm()">

  <div class="form-inline form-group">
    <label class="col-md-2 justify-content-start">
        Libellé du pef
        <span class="startRequired mr-1"> *</span>
    </label>
    <input type="text" maxlength="100" formControlName="libellePef" class="col-md-6 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.libellePef.errors }" />
    <div *ngIf="submitted && formFiels.libellePef.errors" class="col invalid-feedback">
      <div class="col text-left" *ngIf="formFiels.libellePef.errors.required">Libellé du pef est requis.</div>
    </div>
  </div>

  <div class="form-inline form-group">
    <label class="col-md-2 justify-content-start">
       Code Basicat
       <span class="startRequired mr-1"> *</span>
    </label>
    <input type="text" maxlength="100" formControlName="codeBasicat" class="col-md-3 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.codeBasicat.errors }" />
    <div *ngIf="submitted && formFiels.codeBasicat.errors" class="col invalid-feedback">
      <div class="text-left" *ngIf="formFiels.codeBasicat.errors.required">Code Basicat est requis.</div>
    </div>
  </div>

  <div class="form-inline form-group">
    <label class="col-md-2 justify-content-start">
        Nom de l'application
        <span class="startRequired mr-1"> *</span>
    </label>
    <input type="text" maxlength="100" formControlName="nomApplication" class="col-md-6 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.nomApplication.errors }" />
    <div *ngIf="submitted && formFiels.nomApplication.errors" class="col invalid-feedback">
      <div class="text-left" *ngIf="formFiels.nomApplication.errors.required">Nom de l'application est requis.
      </div>
    </div>
  </div>
</form>

Under my TS file , my form config looks like this :

this.addItemfForm = this.formBuilder.group({
  libellePef: ['', Validators.required],
  codeBasicat: ['', Validators.required ],
  nomApplication: ['', Validators.required ],
  urlCible: [''],
  modeTransfert: [''],
});

I've tried the autofocus directive but that didn't work

Suggestions?

Cryo answered 25/12, 2018 at 17:40 Comment(0)
I
33

Use below code in your submit.

for (const key of Object.keys(this.addressForm.controls)) {
      if (this.addressForm.controls[key].invalid) {
        const invalidControl = this.el.nativeElement.querySelector('[formcontrolname="' + key + '"]');
        invalidControl.focus();
        break;
     }
}

this.addressForm will be your FormGroup.

We don't even need directive here.

Inflammatory answered 24/12, 2019 at 7:44 Comment(5)
Which is the type for this.el?Ayacucho
Add "private el: ElementRef" to your constructor (ElementRef form '@angular/core')Toll
It worked for me but only for input feild not for dropdown feilds.Goodtempered
This will work for every control and tested in angular version 15 for (const key of Object.keys(this.dynamicFormGroup.controls)) { if (this.dynamicFormGroup.controls[key].invalid) { (<any>this.dynamicFormGroup.get(key)).nativeElement.focus(); break; } }Behemoth
Note that this answer won't work if you're referencing form controls directly in your template - e.g [formControl]=, because the name of the form control is not present in the DOM in that case.Populate
S
7

My Answer is inspired from yurzui's answer here. I'm using the logic from his answer to get the nativeElement of a particular FormControl by using it's FormControl.

This is the logic that does that:

const originFormControlNameNgOnChanges = FormControlName.prototype.ngOnChanges;
FormControlName.prototype.ngOnChanges = function () {
  const result = originFormControlNameNgOnChanges.apply(this, arguments);
  this.control.nativeElement = this.valueAccessor._elementRef.nativeElement;
  return result;
};

Now, the form's errors field would be null even though it's fields are invalid. So to get the exact first field that's invalid, we'll have to loop through all the fields and check for validity for each of them. I can write this logic in the onSubmitForm() method. Something like this:

onSubmitForm() {
  const fieldsToCheck = [
    'codeBasicat',
    'libellePef',
    'nomApplication'
  ];
  for (let i = 0; i < fieldsToCheck.length; i++) {
    const fieldName = fieldsToCheck[i];
    if (this.addItemfForm.get(fieldName).invalid) {
      ( < any > this.addItemfForm.get(fieldName)).nativeElement.focus();
      break;
    }
  }
}

I've deliberately used for instead of Array.forEach as I wanted to break from the loop.

Hopefully this should do the trick for you.

Here's a Working Sample StackBlitz for your ref.

Salliesallow answered 25/12, 2018 at 17:47 Comment(4)
i need to get dynamically the reference on the first erroned input , and set focus on itCryo
@firasKoubaa, I've updated my answer. Please check to see if that works for you. I've also added a Working Sample StackBlitz that you might want to have a look at. Hope that helps.Salliesallow
@Salliesallow you could use Array.some for breaking out :) Nevertheless, I'd like to find the first invalid field in DOM order...Jezreel
Link is not workingMatins
G
5

I did that using directives. So My form would look like this:

<form [formGroup]="userForm" (submit)="saveData()" appFocus >
...
</form>

and the code for the directive itself:

import { Directive, HostListener, Input, ElementRef } from '@angular/core';
import { NgForm } from '@angular/forms';

@Directive({
  selector: '[appFocus]'
})
export class FocusDirective {

  constructor(private el: ElementRef) { }

  @Input() formGroup: NgForm;

  @HostListener('submit', ['$event'])
  public onSubmit(event): void {
    if ('INVALID' === this.formGroup.status) {
      event.preventDefault();

      const formGroupInvalid = this.el.nativeElement.querySelectorAll('.ng-invalid');
      (<HTMLInputElement>formGroupInvalid[0]).focus();
    }
  }
}

However this solution is incomplete as there is a lot of corner cases that have to be considered. For example what if the first element is radio button group. Dispatching focus event will automatically mark the filed. Second not every element to which angular ads ng-invalid will be an input.

Goalkeeper answered 26/12, 2018 at 16:36 Comment(0)
G
3

We can set focus on first invalid input simply by just write this code in the submit() of the form.

   if(this.form.invalid)
    {  
      // Got focus to the error field
    let invalidFields = [].slice.call(document.getElementsByClassName('ng-invalid'));
    invalidFields[1].focus();  

    }
Gettogether answered 5/11, 2019 at 11:44 Comment(0)
B
1

Try this:

import { Directive, HostListener, ElementRef} from '@angular/core';

@Directive({
  selector: '[focusFirstInvalidField]'
})
export class FocusFirstInvalidFieldDirective {

  constructor(private el: ElementRef) { }

  @HostListener('submit')
  onFormSubmit() {
    const invalidElements = this.el.nativeElement.querySelectorAll('.ng-invalid');
    if (invalidElements.length > 0) {
      console.log(invalidElements[0]); 

      invalidElements[0].focus();
    }
  }
}

Remember to debug, see if element 0 is not your own form as it happened to me, so see right what field it is reporting as the first and put the position right.

Babette answered 2/7, 2019 at 19:16 Comment(2)
querySelectorAll() returns a NodeListOf<Element> and the Element type doesn't have a focus() methodSaphead
@Saphead For me it worked, check which version you are usingStephen
S
0

This option does not work for me, but I managed to fix it by changing the code as follows:

@Directive({
  selector: '[appErrorFocusin]'
})
export class ErrorFocusinDirective {

  selectorToFocus : String = 'textArea,mat-select,select,input,button';

  constructor(private el: ElementRef,
    @Inject(DOCUMENT) private document: Document) { }

  @Input() formGroup: NgForm;

  @HostListener('submit', ['$event'])
  public onSubmit(event): void {
    if ('INVALID' === this.formGroup.status) {
      event.preventDefault();

      const formGroupInvalid = this.el.nativeElement.querySelectorAll('.ng-    invalid,:not(.mat-badge-hidden).mat-badge');
      let elementToOffset = this.getElementToOffset(formGroupInvalid[0]);
      this.document.documentElement.scrollTop = elementToOffset.offsetTop;
      this.setFocusOnError(elementToOffset);
    }
  }


  getElementToOffset(element : any){
    let defaultElement = element;
    while (!(element.parentElement instanceof HTMLFormElement)){
      if (element.parentElement){
        element = element.parentElement;
      }else{
        return defaultElement;
      }
    }
    return element;
  }

  setFocusOnError(elementToOffset : any){
    let listaElementos =     elementToOffset.querySelectorAll(this.selectorToFocus);
    if (listaElementos.length>0){
      listaElementos[0].focus();
    }
  }

}
Symptom answered 8/5, 2019 at 5:45 Comment(0)
R
0

A continuation to @Avinash's answer, instead of making it

querySelector('[formcontrolname="' + key + '"]');

We can add id's in the HTML to the input and simply make that as this:

querySelector('#'+ key +'.ng-invalid');
Rollway answered 4/7, 2022 at 11:19 Comment(2)
What difference does it make other than different selector to same element?Dannielledannon
For some reason, if we would not use [formControlName] like this, it would give an error. Instead have double security, and referencing it by #Id made it work.Rollway

© 2022 - 2024 — McMap. All rights reserved.