Pull to refresh

Валидация форм в AngularJS

Reading time 4 min
Views 92K
Валидация — одна из автомагических возможностей AngularJS. Хотя магического здесь, конечно же, ничего нет. Просто такие стандартные теги html как form, input, select, textarea — это тоже директивы. И когда они объединяются с ngModel, required, ngPattern и т.п., начинает работать валидация.


ngModel


Директива, без которой валидация не заработает. Эта директива ответственна за:
  • two-way binding между моделью и представлением (необходимо для элементов управления/директив input, textarea, select и т.п.);
  • предоставляет интерфейс для валидации: $render(), $setValidity(), $setViewValue(), $parsers, $formatters и т.п. (необходимо для директив/валидаторов required, number, email, url, ngPattern и т.п.);
  • сохранение состояния элемента управления (valid/invalid, dirty/pristine, ошибки валидации);
  • установка соответствующих классов для элементов (ng-valid, ng-invalid, ng-dirty, ng-pristine);
  • регистрация элемента управления для родительской формы.

Практически все эти ответственности реализуются через NgModelController.

Простейший пример валидации email (демо).

    <input type="email" ng-model="myEmail">


Формы


Состояние формы зависит от всех зарегистрированных для нее элементов управления и вложенных форм. Форма будет валидна, если валидны все включенные в нее элементы управления и формы. Можно создавать иерархии форм с помощью директивы ngForm для более удобной обработки состояния отдельных частей одной большой формы. Если для формы задано имя с помощью атрибута name, то контроллер этой формы будет опубликован в соответствующей области видимости с этим именем. Примеры использования свойств формы и ее именованных элементов управления (демо):
  • myForm.myInput.$valid
  • myForm.myInput.$error
  • myForm.$invalid
  • myForm.$error.required

Информация о валидации и ошибках отражается также в классах форм и элементов управления. Например: ng-invalid-required, ng-dirty, ng-valid, ng-valid-email.

Создание директивы-валидатора


При создании директивы-валидатора необходимо реализовать обработчики для двух случаев:
  • изменение модели — конвеерно будут вызваны все функции из массива $formatters;
  • изменение представления — конвеерно будут вызваны все функции из массива $parsers. При реализации собственных директив с поддержкой валидации к вызову этих функций приведет вызов метода $setViewValue.

Пример валидатора, проверяющего параметры пароля — не менее 6 символов, как минимум, 1 цифра и, как минимум, один нецифровой символ (демо):
    mod.directive('strongPassRequired', function () {
        var isValid = function(s) {
            return s && s.length > 5 && /\D/.test(s) && /\d/.test(s);
        };

        return {
            require:'ngModel',
            link:function (scope, elm, attrs, ngModelCtrl) {

                ngModelCtrl.$parsers.unshift(function (viewValue) {
                    ngModelCtrl.$setValidity('strongPass', isValid(viewValue));
                    return viewValue;
                });

                ngModelCtrl.$formatters.unshift(function (modelValue) {
                    ngModelCtrl.$setValidity('strongPass', isValid(modelValue));
                    return modelValue;
                });
            }
        };
    });

В методе $setValidity первым параметром идет строка, которая будет использоваться:
  • как свойство объекта $error для элемента управления, к которому будет применен данный валидатор. Например, myPassForm.myPass.$error.strongPass;
  • как css класс (ng-valid-strong-pass или ng-invalid-strong-pass) для элемента управления, к которому будет применен данный валидатор, и всех его родительских форм.


Визуальная директива с поддержкой валидаторов


Для примера реализуем свой checkbox с иконкой из Font Awesome, для которого можно будет использовать, например, валидатор required (демо — для второго checkbox-а как раз применен валидатор required).
Исходный код директивы:
.directive('uiCheckbox', function () {
    return {
        restrict: 'EA',
        replace: true,
        transclude: true,
        template:
            '<div class="checkbox-control" ng-click="toggle()">' +
                '<span ng-class="{\'icon-check-empty\': !value, \'icon-check\': value}"></span>' +
                '<span class="checkbox-label" ng-transclude></span>' +
            '</div>',
        require: 'ngModel',
        scope: true,
        link: function (scope, element, attrs, ngModelCtrl) {
            scope.value = false;

            ngModelCtrl.$render = function () {
                scope.value = ngModelCtrl.$viewValue;
            };

            scope.toggle = function () {
                scope.value = !scope.value;
                ngModelCtrl.$setViewValue(scope.value);
            };
        }
    };
});

Ключевыми моментами здесь являются:
  • требование, чтобы в функцию link был передан ngModelControllerrequire: 'ngModel';
  • реализация метода ngModelCtrl.$render — вызывается, когда происходит обновление модели, указываемой в ngModel;
  • при действиях пользователя, которые должны привести к изменению модели, необходимо вызвать ngModelCtrl.$setViewValue. В этом случае будут вызваны все функции из массива $parsers.


Валидация и директивы с изолированной областью видимости


Допустим у вас есть блок с несколькими элементами интерфейса, сообщений по результатам валидации и т.п. И вам необходимо использовать его на нескольких страницах. Например, блок задания пароля с подтверждением, проверками на качество пароля, идентичность и т.п. и вы хотите использовать этот блок повторно на страницах регистрации пользователя и редактирования профиля. Для подобного случая вполне можно создать директиву с изолированной областью видимости. И результаты валидации будут прекрасно передаваться для родительских форм, т.к. поиск родительских форм происходит на основе иерархии DOM, а не иерархии областей видимости (scope).

Изолированная форма


Допустим есть такая задача: есть форма создания некоторого объекта. Для его создания необходимо заполнить название и необязательно заполнять описание и список сайтов с ссылками. Кнопка «Создать объект» будет неактивна, пока форма создания будет невалидна. При этом для добавление нового сайта в список необходимо обязательно заполнить поля Название и Url. И пока они не будут заполнены кнопка должна быть неактивна. Таким образом, для наполнения списка сайтов будет использоваться своя вложенная форма. Но состояние ее валидности не должно влиять на родительскую форму.

Для подобной задачи у меня есть работоспособное, но не очень красивое решение. Возможно, оно кому-нибудь пригодится. Или кто-то предложит решение лучше. Демо.
Исходный код директивы:
mod.directive('isolatedForm', function(){
    return {
        require: 'form',
        link: function(scope, formElement, attrs, formController) {
            var parentFormCtrl = formElement.parent().controller('form');
            var core$setValidity = formController.$setValidity;
            formController.$setValidity = function(validationToken, isValid, control) {
                core$setValidity(validationToken, isValid, control);
                if (!isValid && parentFormCtrl) {
                    parentFormCtrl.$setValidity(validationToken, true, formController);
                }
            }
        }
    };
});

Основная идея — каждый раз, когда форма хочет сообщить наверх, что в ней что-то невалидно, вместо этого сообщать, что оно валидно.
Tags:
Hubs:
+8
Comments 23
Comments Comments 23

Articles