Set focus on first invalid input in AngularJs form
Asked Answered
D

13

62

I've read several articles and StackOverflow questions relating to the setting of focus in AngularJs.

Unfortunately all the examples that I have read assume that there is some attribute that I can add to the element to gain focus, e.g. a focusMe directive.

However what if I don't know in advance which input to set focus to? In particular how do I set focus to the first input element in a form that has $invalid set - i.e. an element that fails validation. There could be several inputs that fail validation, so I cannot use a directive that just tries to call .focus() based on this. (I am doing this for Accessibility/WCAG reasons, its good practice to do so on submit being clicked to minimize keypresses to find the first field that has failed validation).

The $error object will give all controls that fail validation, but they are grouped by the type of failure not in any order of appearance on the form.

I'm sure I can come up with some kludged way of doing this. A directive on the form, which receives some broadcast when focus needs to be set - that directive can then search for the first $invalid element. However this seems very complex and I'd like to know whether these is a better more 'angular' way of doing this.

Duester answered 4/12, 2013 at 1:28 Comment(0)
C
17

You can also use angular.element

angular.element('input.ng-invalid').first().focus();

View

<form name="myForm" novalidate="novalidate" data-ng-submit="myAction(myForm.$valid)" autocomplete="off"></form>

Controller

$scope.myAction= function(isValid) {
    if (isValid) {
        //You can place your ajax call/http request here
    } else {
        angular.element('input.ng-invalid').first().focus();
    }
};

used ngMessages for validation

The no jquery way

angular.element($document[0].querySelector('input.ng-invalid')).focus();

When using this method, need to pass $document as parameter in your angular controller

angular.module('myModule')
.controller('myController', ['$document', '$scope', function($document, $scope){
    // Code Here
}]);
Colleague answered 16/3, 2016 at 15:20 Comment(4)
@Duester I have added non jquery method also.. hope this helpsColleague
angular.element($document[0].querySelector('input.ng-invalid'))[0].focus(); worked for me.Creighton
I think my solution is just fine - but I think these ideas are worthy of the 'accept' checkmark.Duester
what will be syntax for angular 12+?Unbonnet
D
87

Ok, so the answer was simpler than I thought.

All I needed was a directive to put on the form itself, with an event handler looking for the submit event. This can then traverse the DOM looking for the first element that has the .ng-invalid class on it.

Example using jQLite:

myApp.directive('accessibleForm', function () {
    return {
        restrict: 'A',
        link: function (scope, elem) {

            // set up event handler on the form element
            elem.on('submit', function () {

                // find the first invalid element
                var firstInvalid = elem[0].querySelector('.ng-invalid');

                // if we find one, set focus
                if (firstInvalid) {
                    firstInvalid.focus();
                }
            });
        }
    };
});

The example here uses an Attribute directive, you could expand the example to have this be an element directive (restrict: 'E') and include a template that converts this to a . This is however a personal preference.

Duester answered 5/12, 2013 at 1:6 Comment(8)
elem is already a angular.element(), so if you're using jQuery at least you can just do elem.find('.ng-invalid:first').focus(). Not sure if jqLite supports the :first selector as well.Wellgrounded
This could be made better by focusing only on visible elements like so: var firstInvalid = element.find('.ng-invalid:visible').first(); There are a few edge cases when this could make sense.Endplay
@Endplay - I also see you made an edit which was rejected suggesting a change to an 'A' attribute directive. Of course an A directive or even a C directive would also work - but why didn't the E directive work for you? It seems to work just fine for me.Duester
@Duester with an attribute directive we can do something like this: <form accessible-form></form> With an element directive where would you put the element? Inside the form? Something like <form><accessible-form></accessible-form></form>? Or would your <accessible-form> be a replacement for the form element?Endplay
@Endplay Yes, the <accessible-form> I've created is a replacement for <form> and does convert to a <form> when run .... but you are correct, the example here doesn't show all that. So I should change it like you suggest - its a better example - thanks.Duester
This is actually not so bad performance as "traversing the dom" might sound. The querySelector is native method in all modern browsers, and have good optimizing and are very fast. Much faster than traversing some javascript object collection of some sort.Excursionist
elem[0] returns you a document element, and then querySelector returns already just the first match. what is the reason to wrap it into angular.element and then unwrapping it again with [0]?Legnica
created an angular module out of this and hosted on github - github.com/NithinBiliya/focus-invalid-fieldHandiwork
C
17

You can also use angular.element

angular.element('input.ng-invalid').first().focus();

View

<form name="myForm" novalidate="novalidate" data-ng-submit="myAction(myForm.$valid)" autocomplete="off"></form>

Controller

$scope.myAction= function(isValid) {
    if (isValid) {
        //You can place your ajax call/http request here
    } else {
        angular.element('input.ng-invalid').first().focus();
    }
};

used ngMessages for validation

The no jquery way

angular.element($document[0].querySelector('input.ng-invalid')).focus();

When using this method, need to pass $document as parameter in your angular controller

angular.module('myModule')
.controller('myController', ['$document', '$scope', function($document, $scope){
    // Code Here
}]);
Colleague answered 16/3, 2016 at 15:20 Comment(4)
@Duester I have added non jquery method also.. hope this helpsColleague
angular.element($document[0].querySelector('input.ng-invalid'))[0].focus(); worked for me.Creighton
I think my solution is just fine - but I think these ideas are worthy of the 'accept' checkmark.Duester
what will be syntax for angular 12+?Unbonnet
F
15

You can create directive as some other answers or alternatively you can hook it with ng-submit and implement logic in the controller.

View:

<form name='yourForm' novalidate ng-submit="save(yourForm)">
</form>

Controller:

$scope.save = function(yourForm) {
  if (!yourForm.$valid) {
    angular.element("[name='" + yourForm.$name + "']").find('.ng-invalid:visible:first').focus();
    return false;
  }
};
Fiance answered 30/10, 2015 at 2:5 Comment(1)
angular.element($document[0].querySelector('input.ng-invalid'))[0].focus(); worked for meCreighton
N
7

You can use pure jQuery to select the first invalid input:

$('input.ng-invalid').first().focus();

Nada answered 2/4, 2015 at 5:28 Comment(2)
And where would this code go? And what if we didn't want jQuery to be part of the solution? The point of the question is not, how do I focus an element (which could be done with a small amount of JavaScript even without jQuery) - the point is, how do I do this so that it is focused when submit is clicked on an invalid form.Duester
how to add animation to this on scroll?Phantasy
F
4

    .directive('accessibleForm', function () {
        return {
            restrict: 'A',
            link: function (scope, elem) {
                // set up event handler on the form element
                elem.on('submit', function () {
                    // find the first invalid element
                    var firstInvalid = elem[0].querySelector('.ng-invalid');
                    if (firstInvalid && firstInvalid.tagName.toLowerCase() === 'ng-form') {
                        firstInvalid = firstInvalid.querySelector('.ng-invalid');
                    }
                    // if we find one, set focus
                    if (firstInvalid) {
                        firstInvalid.focus();
                    }
                });
            }
        };
    })
Fibrilla answered 23/10, 2015 at 10:43 Comment(1)
Hi. It might be helpful if you explained why this is a better answer. I'd happy make it the accepted answer if its got some advantage.Duester
H
2

I have been playing with this idea for a while and I came up with my own solution, it may help people who are adverse to crawling the DOM, like me.

As far as I can tell form elements register themselves in a consistent order (i.e. top to bottom) and their names and validation states are available on the scope through what ever the form name is (e.g. $scope.myForm).

This lead me to think that there was a way to find the first invalid form input without crawling the DOM and instead crawling the internal structures of angular js. Below is my solution but it assumes that you have some other way of focusing form elements, I am broadcasting to a custom directive, if the broadcast matches the name of the element it will focus itself (which is useful in itself as you you get to control which element takes focus on the first load).

The function to find the first invalid (ideally shared to the controllers through a service)

function findFirstInvalid(form){
    for(var key in form){
        if(key.indexOf("$") !== 0){
            if(form[key].$invalid){
                return key;
            }
        }
    }
}

And the custom focus directive

directives.directive('focus', function($timeout){
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, elem, attrs, ctrl){
            scope.$on('inputFocus', function(e, name){
                if(attrs.name === name){
                    elem.focus();
                }
            });
        }
    }
});
Hamsun answered 20/2, 2014 at 4:52 Comment(1)
Hey, I will definitely have a detailed look at this and try it out. DOM traversal seems so inefficient. If this works for me I will switch the accept to this!Duester
P
1

I did some small modifications to the great solution written by iandotkelly. This solution adds an animation that is triggered on scroll, and does a focus to the selected element after that.

myApp.directive('accessibleForm', function () {
    return {
        restrict: 'A',
        link: function (scope, elem) {

            // set up event handler on the form element
            elem.on('submit', function () {

                // find the first invalid element
                var firstInvalid = elem[0].querySelector('.ng-invalid');

                // if we find one, we scroll with animation and then we set focus
                if (firstInvalid) {
                     angular.element('html:not(:animated),body:not(:animated)')
                    .animate({ scrollTop: angular.element(firstInvalid).parent().offset().top },
                        350,
                        'easeOutCubic',
                        function () {
                            firstInvalid.focus();
                        });
                }
            });
        }
    };
});
Polycotyledon answered 26/10, 2016 at 8:45 Comment(1)
It would be really helpful if you attach plnkr or jsfiddleScapula
I
1

just one line:

if($scope.formName.$valid){
    //submit
}
else{
    $scope.formName.$error.required[0].$$element.focus();
}
Icon answered 27/10, 2017 at 7:50 Comment(0)
U
0

You can add an attribute in each form element which is a function (ideally a directive) that receives a field id. This field id would have to correlate somehow to your $error object. The function can check if the id is in your $error object, and if so return the attribute setting for an error.

<input id="name" class="{{errorCheck('name')}}">

If you had an error, it would generate this.

<input id="name" class="error">

You can use this to set your style and you now know which fields have errors. Unfortunately you don't know which is the first field.

One solution would be to use jQuery and the .first filter. If you go this route, check out http://docs.angularjs.org/api/angular.element

Another solution would be to add into your form fields a field order parameter for the function: {{errorCheck('name', 1)}}. You could push the error field names to an array, then sort them by the field order parameter. This could give you more flexibility.

Hope this helps.

Up answered 4/12, 2013 at 2:13 Comment(1)
Hey thanks. I do have a way of adding an attribute to the form elements that have failed - i'm just looking for the most Angular way and traversing the DOM looking for this attribute seems dumb, but probably the only way.Duester
M
0

I was inspired by chaojidan above to suggest this variation for those who are using nested angular 1.5.9 ng-forms:

class FormFocusOnErr implements ng.IDirective
{
    static directiveId: string = 'formFocusOnErr';

    restrict: string = "A";

    link = (scope: ng.IScope, elem, attrs) =>
    {
        // set up event handler on the form element
        elem.on('submit', function () {

            // find the first invalid element
            var firstInvalid = angular.element(
                elem[0].querySelector('.ng-invalid'))[0];

            // if we find one, set focus
            if (firstInvalid) {
                firstInvalid.focus();
                // ng-invalid appears on ng-forms as well as 
                // the inputs that are responsible for the errors.
                // In such cases, the focus will probably fail 
                // because we usually put the ng-focus attribute on divs 
                // and divs don't support the focus method
                if (firstInvalid.tagName.toLowerCase() === 'ng-form' 
                    || firstInvalid.hasAttribute('ng-form') 
                    || firstInvalid.hasAttribute('data-ng-form')) {
                    // Let's try to put a finer point on it by selecting 
                    // the first visible input, select or textarea 
                    // that has the ng-invalid CSS class
                    var firstVisibleInvalidFormInput = angular.element(firstInvalid.querySelector("input.ng-invalid,select.ng-invalid,textarea.ng-invalid")).filter(":visible")[0];
                    if (firstVisibleInvalidFormInput) {
                        firstVisibleInvalidFormInput.focus();
                    }
                }
            }
        });            
    }
}

// Register in angular app
app.directive(FormFocusOnErr.directiveId, () => new FormFocusOnErr());
Montagnard answered 24/10, 2016 at 22:4 Comment(0)
T
0

That's because focus() is not supported in jqLite and from the Angular docs on element.

Tyndall answered 27/6, 2017 at 18:14 Comment(1)
This is a comment on CarComp's answer - not an answer in its own rightDuester
P
0

A minor tweak with what @Sajan said worked for me,

angular.element("[name='" + this.formName.$name + "']").find('.ng-invalid:visible:first')[0].focus();
Perilune answered 25/6, 2019 at 17:55 Comment(0)
H
-1

A non-directive based method could look like this. It is what i used, since i have a 'next' button at the bottom of each page that is actually in index.html in the footer. I use this code in main.js.

if (!$scope.yourformname.$valid) {
      // find the invalid elements
      var visibleInvalids = angular.element.find('.ng-invalid:visible');


      if (angular.isDefined(visibleInvalids)){
        // if we find one, set focus
        visibleInvalids[0].focus();
      }

      return;
    }
Hoi answered 26/6, 2015 at 15:25 Comment(2)
Straightforward and simple, just the way I like itAutoradiograph
using angular's builtin jqLite this code will not work. First, be sure to call angular.element().find, not angular.element.find to avoid an exception. Second - this will only ever return an empty array due to angular-jqLite's limitations.Orlon

© 2022 - 2024 — McMap. All rights reserved.