Matthew Patrick Cashatt Matthew Patrick Cashatt - 3 months ago 43
AngularJS Question

Using AngularJS, how do I set all form fields to $dirty at once?

I have created a HTML form using AngularJS and added

required
attributes to some fields.

For these fields, I have an error message that displays if the field is not
$pristine
and also
$invalid
:

<input type="text" ng-model="SomeModel.SomeProperty" name="myField" class="input-block-level" required>
<p ng-show="frmMyForm.myField.$invalid && !frmMyForm.myField.$pristine" class="error-text">This field is required!</p>


This works fine. However, if the user simply skips over a required field (never places the cursor in it), then the field is always pristine and therefore the error message isn't displayed, even after clicking the submit button. So the user is faced with a form they can't submit but no error text to tell them why.

My thought is that setting all form fields to
$dirty
on the submit action would trigger the error messages to show up for any required field that the user simply skipped. Is this possible? If so, how?

Thanks in advance.

Answer

We do something similar to your answer, we have a formSubmitted directive that binds to the submit event, if fired we set $submitted variable on the form controller. That way you can use it in a similar fashion than how you use ShowValidationMessages but it is re-usable. Very simple directive:

app.directive('formSubmitted', [function () {
    return {
        restrict: 'A',
        require: 'form',
        link: function (scope, element, attrs, ctrl) {
            ctrl.$submitted = false;
            element.on('submit', function () {
                scope.$apply(function () {
                    ctrl.$submitted = true;
                });
            });
        }
    };
}]);

You apply this on the form tag itself as an attribute.

We took it a couple of steps further, our requirement was to show validation errors only if the following holds true: the element is invalid AND either the form was submitted OR the input element has blurred. So we ended up with another directive that requires ngModel that sets the blurred status of the element on the ngModel controller.

And finally to get rid of a whole lot of repeated boiler-plate code in html to check all these thing, e.g. your ng-show="frmMyForm.myField.$invalid && (!frmMyForm.myField.$pristine || MyObject.ShowValidationMessages)" we encapsulated that into a directive as well. This template directive wraps our input elements with Bootstrap boiler-plate as well as handle all the validation stuff. So now all my form inputs just follow this pattern:

<div data-bc-form-group data-label="Username:">
    <input type="text" id="username" name="username" ng-model="vm.username" data-bc-focus required />
</div>

and bcFormGroup directive transforms that into the following bootstrap enabled html:

<div class="form-group" ng-class="{'has-error': showFormGroupError()}" data-bc-form-group="" data-label="Username:">
    <label for="username" class="col-md-3 control-label ng-binding">Username:</label>
    <div class="col-md-9">
        <input type="text" id="username" name="username" ng-model="vm.username" data-bc-focus="" required="" class="ng-pristine form-control ng-valid ng-valid-required">
        <span class="help-block ng-hide" ng-show="showRequiredError()">Required</span>
    </div>
</div>

This keep things DRY and offers great flexibility into what type of inputs are supported.

Update:

Here is a basic listing of bcFormGroup directive. The default template uses bootstrap's horizontal form, but can be adapted to your liking.

app.directive('bcFormGroup', ['$compile', '$interpolate', function ($compile, $interpolate) {
  return {
    restrict: 'A',
    template:
        '<div class="form-group" ng-class="{\'has-error\': showFormGroupError()}">' +
            '<label for="{{inputId}}" class="col-md-3 control-label">{{label}}</label>' +
            '<div class="col-md-9">' +
                '<bc-placeholder></bc-placeholder>' +
            '</div>' +
        '</div>',
    replace: true,
    transclude: true,
    require: '^form',
    scope: {
        label: '@',
        inputTag: '@'
    },

    link: function (scope, element, attrs, formController, transcludeFn) {

        transcludeFn(function (clone) {
            var placeholder = element.find('bc-placeholder');
            placeholder.replaceWith(clone);
        });

        var inputTagType = scope.inputTag || 'input';
        var inputElement = element.find(inputTagType);
        var fqFieldName = formController.$name + '.' + inputElement.attr('name');
        var formScope = inputElement.scope();

        if (inputElement.attr('type') !== 'checkbox' && inputElement.attr('type') !== 'file') {
            inputElement.addClass('form-control');
        }

        scope.inputId = $interpolate(inputElement.attr('id'))(formScope);
        scope.hasError = false;
        scope.submitted = false;

        formScope.$watch(fqFieldName + '.$invalid', function (hasError) {
            scope.hasError = hasError;
        });

        formScope.$watch(formController.$name + '.$submitted', function (submitted) {
            scope.submitted = submitted;
        });

        if (inputElement.attr('data-bc-focus') != null || inputElement.attr('bc-focus') != null) {
            scope.hasBlurred = false;
            formScope.$watch(fqFieldName + '.$hasBlurred', function (hasBlurred) {
                scope.hasBlurred = hasBlurred;
            });
        }

        if (inputElement.attr('required')) {
            scope.hasRequiredError = false;
            formScope.$watch(fqFieldName + '.$error.required', function (required) {
                scope.hasRequiredError = required;
            });
            inputElement.after($compile('<span class="help-block" ng-show="showRequiredError()">Required</span>')(scope));
        }

        if (inputElement.attr('type') === 'email') {
            scope.hasEmailError = false;
            formScope.$watch(fqFieldName + '.$error.email', function (emailError) {
                scope.hasEmailError = emailError;
            });
            inputElement.after($compile('<span class="help-block" ng-show="showEmailError()">Invalid email address</span>')(scope));
        }

        scope.showFormGroupError = function () {
            return scope.hasError && (scope.submitted || (scope.hasBlurred === true));
        };

        scope.showRequiredError = function () {
            return scope.hasRequiredError && (scope.submitted || (scope.hasBlurred === true));
        };

        scope.showEmailError = function () {
            return scope.hasEmailError && (scope.submitted || (scope.hasBlurred === true));
        };

    }
  };
}]);

Update:

The following directive sets $focused and $hasBlurred:

app.directive('bcFocus', [function () {
    var focusClass = 'bc-focused';
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function (scope, element, attrs, ctrl) {
            ctrl.$focused = false;
            ctrl.$hasBlurred = false;
            element.on('focus', function () {
                element.addClass(focusClass);
                var phase = scope.$root.$$phase;
                if (phase == '$apply' || phase == '$digest') {
                    ctrl.$focused = true;
                } else {
                    scope.$apply(function () {
                        ctrl.$focused = true;
                    });
                }
            }).on('blur', function () {
                element.removeClass(focusClass);
                var phase = scope.$root.$$phase;
                if (phase == '$apply' || phase == '$digest') {
                    ctrl.$focused = false;
                    ctrl.$hasBlurred = true;
                } else {
                    scope.$apply(function () {
                        ctrl.$focused = false;
                        ctrl.$hasBlurred = true;
                    });
                }
            });
        }
    };
}]);