Set focus on first invalid input in AngularJs form

Viewed 62007

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.

13 Answers

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.

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();
                        });
                }
            });
        }
    };
});

just one line:

if($scope.formName.$valid){
    //submit
}
else{
    $scope.formName.$error.required[0].$$element.focus();
}

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.

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());

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

angular.element("[name='" + this.formName.$name + "']").find('.ng-invalid:visible:first')[0].focus();
Related