2
votes

Basically, what I'm trying to accomplish, is to set focus to the first invalid element after a form submit has been attempted. At this point, I have the element being flagged as invalid, and I can get the $name of the element so I know which one it is.

It's "working" but a "$apply already in progress" error is being thrown...
So I must be doing something wrong here :)

Here's my code so far:

$scope.submit = function () {

    if ($scope.formName.$valid) {
        // Good job.
    }
    else 
    {
        var field = null,
            firstError = null;
        for (field in $scope.formName) {
            if (field[0] != '$')
            {
                if (firstError === null && !$scope.formName[field].$valid) {
                    firstError = $scope.formName[field].$name;
                }

                if ($scope.formName[field].$pristine) {
                    $scope.formName[field].$dirty = true;
                }
            }
        }

        formName[firstError].focus();
    }
}

My field looping is based on this solution, and I've read over this question a few times. It seems like the preferred solution is to create a directive, but adding a directive to every single form element just seems like overkill.

Is there a better way to approach this with a directive?

3

3 Answers

2
votes

Directive code:

app.directive('ngFocus', function ($timeout, $log) {
return {
    restrict: 'A',
    link: function (scope, elem, attr) {

        scope.$on('focusOn', function (e, name) {
            // The timeout lets the digest / DOM cycle run before attempting to set focus
            $timeout(function () {
                if (name === attr.ngFocusId) {
                    if (attr.ngFocusMethod === "click")
                        angular.element(elem[0]).click();
                    else
                        angular.element(elem[0]).focus();
                }
            });
        })
    }
}
});

Factory to use in the controller:

app.factory('focus', function ($rootScope, $timeout) {
    return function (name) {
        $timeout(function () {
            $rootScope.$broadcast('focusOn', name);
        }, 0, false);
    };
});

Sample controller:

angular.module('test', []).controller('myCtrl', ['focus', function(focus) {
  focus('myElement');
}
0
votes

Building a directive is definitely the way to go. There is otherwise no clean way to select in element in angularjs. It's just not designed like this. I would recommend you to check out this question on this matter.

You wouldn't have to create a single directive for every form-element. On for each form should suffice. Inside the directive you can use element.find('input');. For the focus itself I suppose that you need to include jQuery and use its focus-function.

You can howerever - and I would not recommend this - use jQuery directly inside your controller. Usually angular form-validation adds classes like ng-invalid-required and the like, which you can use as selector. e.g:

$('input.ng-valid').focus();
0
votes

Based on the feedback from hugo I managed to pull together a directive:

.directive( 'mySubmitDirty', function () {
    return {
        scope: true,
        link: function (scope, element, attrs) {

            var form = scope[attrs.name];

            element.bind('submit', function(event) {
                var field = null;
                for (field in form) {
                    if (form[field].hasOwnProperty('$pristine') && form[field].$pristine) {
                        form[field].$dirty = true;
                    }
                }

                var invalid_elements = element.find('.ng-invalid');
                if (invalid_elements.length > 0)
                {
                   invalid_elements[0].focus();
                }

                event.stopPropagation();
                event.preventDefault();
            });
        }
    };
})

This approach requires jquery as the element.find() uses a class to find the first invalid element in the dom.