Angular 2 Focus on first invalid input after Click/Event
Asked Answered
G

9

19

I have an odd requirement and was hoping for some help.

I need to focus on the first found invalid input of a form after clicking a button (not submit). The form is rather large, and so the screen needs to scroll to the first invalid input.

This AngularJS answer would be what I would need, but didn't know if a directive like this would be the way to go in Angular 2:

Set focus on first invalid input in AngularJs form

What would be the Angular 2 way to do this? Thanks for all the help!

Gabon answered 15/12, 2016 at 20:43 Comment(1)
This is not an odd requirement. In forms with many fields it is quite userfriendly to scroll the first invalid field into view on submit. If none of the invalid fields are visible when user press submit button, they would have no idea why nothing happens.Valerle
T
11

This works for me. Not the most elegant solution, but given the constraints in Angular we are all experiencing for this particular task, it does the job.

scrollTo(el: Element): void {
   if(el) { 
    el.scrollIntoView({ behavior: 'smooth' });
   }
}

scrollToError(): void {
   const firstElementWithError = document.querySelector('.ng-invalid');
   this.scrollTo(firstElementWithError);
}

async scrollIfFormHasErrors(form: FormGroup): Promise <any> {
  await form.invalid;
  this.scrollToError();
}

This works, allowing you to evade manipulating the DOM. It simply goes to the first element with .ng-invalid on the page through the document.querySelector() which returns the first element in the returned list.

To use it:

this.scrollIfFormHasErrors(this.form).then(() => {
  // Run any additional functionality if you need to. 
});

I also posted this on Angular's Github page: https://github.com/angular/angular/issues/13158#issuecomment-432275834

Tortuosity answered 24/10, 2018 at 14:29 Comment(2)
this works nicely, except for the slightly niche scenario where there is a form in a modal, with other controls on the main page underneath. Usually modals are appended to the DOM so any invalid controls on the main page will be higher up. Adding ng-touched to the selector too and ensuring markAllAsTouched() is called helps, but a user could still touch a control on the main page, then open the modal.Sharynshashlik
Thanks for your feedback @AdamMarshall. Agreed, a scenario with modals could require some additional logic.Tortuosity
H
5

Unfortunately I can't test this at the moment, so might be a few bugs, but should be mostly there. Just add it to your form.

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

@Directive({ selector: '[scrollToFirstInvalid]' })
export class ScrollToFirstInvalidDirective {
  @Input('scrollToFirstInvalid') form: NgForm;
  constructor() {
  }
  @HostListener('submit', ['$event'])
  onSubmit(event) {
    if(!this.form.valid) {
      let target;
      for (var i in this.form.controls) {
        if(!this.form.controls[i].valid) {
          target = this.form.controls[i];
          break;
        }
      }
      if(target) {
        $('html,body').animate({scrollTop: $(target.nativeElement).offset().top}, 'slow');
      }
    }
  }
}
Hereabout answered 16/12, 2016 at 2:27 Comment(5)
Sorry for the late response, I will get a test going of this! Thank you for the help!Gabon
How can we do this without jquery?Gabon
Well you could move to the element without animating, or you could write your own custom animation function (or use someone elses). Something like this (#8918421)Hereabout
This does not work, target.nativeElement is undefined. Which version of Angular are you using @Baconbeastnz?Hanzelin
@Hanzelin check out this link : #39643047Derogative
S
3

If you are using AngularMaterial, the MdInputDirective has a focus() method which allow you to directly focus on the input field.

In your component, just get a reference to all the inputs with the @ViewChildren annotation, like this:

@ViewChildren(MdInputDirective) inputs: QueryList<MdInputDirective>;

Then, setting focus on the first invalid input is as simple as this:

this.inputs.find(input => !input._ngControl.valid).focus()

Stucker answered 16/6, 2017 at 9:20 Comment(0)
D
3

I don't know if this is valid approach or not but this is working great for me.

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

@Directive({ selector: '[accessible-form]' })
export class AccessibleForm {

    @Input('form') form: NgForm;

    constructor(private el: ElementRef) {

    }

    @HostListener('submit', ['$event'])
    onSubmit(event) {
        event.preventDefault();

        if (!this.form.valid) {
            let target;

            target = this.el.nativeElement.querySelector('.ng-invalid')

            if (target) {
                $('html,body').animate({ scrollTop: $(target).offset().top }, 'slow');
                target.focus();
            }
        }
    }

}

In HTML

<form [formGroup]="addUserForm" class="form mt-30" (ngSubmit)="updateUser(addUserForm)" accessible-form [form]="addUserForm"></form>

I have mixed the approach of angularjs accessible form directive in this. Improvements are welcomed!!!

Diva answered 5/2, 2018 at 5:52 Comment(0)
A
3

I've created an Angular directive to solve this problem. You can check it here ngx-scroll-to-first-invalid.

Steps:

1.Install the module:

npm i @ismaestro/ngx-scroll-to-first-invalid --save

2.Import the NgxScrollToFirstInvalidModule:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {NgxScrollToFirstInvalidModule} from '@ismaestro/ngx-scroll-to-first-invalid';

@NgModule({
    imports: [
        BrowserModule,
        NgxScrollToFirstInvalidModule
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

3.Use the directive inside a form:

<form [formGroup]="testForm" ngxScrollToFirstInvalid>
  <input id="test-input1" type="text" formControlName="someText1">
  <button (click)="saveForm()"></button>
</form>

Hope it helps! :)

Adagietto answered 31/10, 2018 at 19:0 Comment(0)
T
2

Plain HTML solution. If you don't need to be scrolling , just focus on first valid input, I use :

public submitForm() {
    if(this.form.valid){
        // submit form
    } else {
        let invalidFields = [].slice.call(document.getElementsByClassName('ng-invalid'));
        invalidFields[1].focus();
    }
}

This is for template driven form here. We focus on second element of invalidFields cuz first is the whole form which is invalid too.

Tristis answered 5/12, 2018 at 14:0 Comment(0)
C
2

For Angular Material , The below worked for me

@ViewChildren(MatInput) inputs: QueryList <MatInput>;
this.inputs.find(input => !input.ngControl.valid).focus();
Crist answered 24/9, 2019 at 15:59 Comment(0)
U
1

I recommend putting this in a service, for me it worked like this:

if (this.form.valid) {
  //submit
} else {
  let control;
  Object.keys(this.form.controls).reverse().forEach( (field) => {
    if (this.form.get(field).invalid) {
      control = this.form.get(field);
      control.markAsDirty();
    }
  });

  if(control) {
    let el = $('.ng-invalid:not(form):first');
    $('html,body').animate({scrollTop: (el.offset().top - 20)}, 'slow', () => {
      el.focus();
    });
  }
}
Upwind answered 30/8, 2017 at 17:21 Comment(0)
H
1
  @HostListener('submit', ['$event'])
  onSubmit(event) {
    event.preventDefault();
      if (!this.checkoutForm.valid) {
        let target;
        target = $('input[type=text].ng-invalid').first();
        if (target) {
            $('html,body').animate({ scrollTop: $(target).offset().top }, 'slow', ()=> {
              target.focus();
            });
        }
      }
   }
Headcloth answered 4/4, 2018 at 20:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.