31 января 2013 в 22:41

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

Валидация — одна из автомагических возможностей 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);
                }
            }
        }
    };
});

Основная идея — каждый раз, когда форма хочет сообщить наверх, что в ней что-то невалидно, вместо этого сообщать, что оно валидно.
Артем Андреев @aav
карма
46,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (23)

  • 0
    Может подбросите материалы, как правильно связывать клиентскую валидацию с серверной, например, капча. Как раз по работе столкнулись.
    • 0
      делаете сервис, который общается с бэкендом, сервис через DI инджектится в контроллеры.
      контроллеры на изменение модели дергают сервис

      вот тут можно массу примеров найти
  • 0
    разве angular не поддерживает nested формы by design?

    + мне кажется, отображение сообщений о невалидности лучше делать через ng-show | ui-if.
    • 0
      Извините, я не очень понял к какой части моего поста относится Ваш комментарий. Про ngForm я в явном виде писал. В демо примерах для отображения сообщений о невалидности и используется ngShow.

      Если Вы про последнюю часть поста, то там основной момент в том, чтобы вложенная форма не влияла на валидность родительской.
  • 0
    К сожалению валидация срабатывает сразу. Зачастую бесит. Только открыл форму, а она уже невалидна (классы навешиваются и состояние полей и самой формы проставляются) Или может я еще не разобрался целиком, как знать(
    • 0
      Для этого в AngularJS есть состояния dirty/pristine. Они выражаются в двух видах:
      • css классы ng-dirty и ng-pristine;
      • свойства для именованных форм и элементов управления, например, myForm.$pristine, myForm.$dirty, myForm.myPassIput.$pristine, myForm.myPassInput.$dirty.
      • 0
        Подскажите а как принудительно отменить валидацию формы? Наверстал проект с собственной валидацией, потом стал прикручивать ангулар и все формы блоканулись, переписывать валидацию на ангулар теперь поздно, хочется просто от нее отказаться.
        • 0
          Что Вы подразумеваете под «отменить валидацию формы»? В принципе в NgModelController есть $setValidity. Если его переопределить в пустую функцию, то информация о валидности по идее никуда не пойдет.
          • 0
            Имею в виду что мне не нужно чтобы ангулар заботился о формах на сайте, потому как валидация для них уже настроена и работает собственными средствами. Спасибо, посмотрю в эту сторону.
      • 0
        А есть стандартный способ сделать, чтобы валидация срабатывала после потери элементом фокуса (когда пользователь перешел к следующему полю)? dirty/pristine тут не помощник. А то собираешься вводить что-то нормальное, а тебя на первой же букве ругают за некорректный email или короткое имя пользователя.
        • 0
          Сейчас в стандартной поставке — нет.
          Но есть такие задачи:
          github.com/angular/angular.js/issues/583 (нет никаких проблем самостоятельно реализовать необходимое состояние по аналогии — в проекте, в котором я сейчас работаю, мы так и сделали)
          github.com/angular/angular.js/pull/2129
          • 0
            Спасибо за помощь!
  • 0
    Не помешал бы пример валидации с использованием сервиса.
    • 0
      Не очень понял Вашу мысль — а какие нюансы Вы тут видите? Вы в сервис хотите вынести код валидации, который потом будет использоваться не только в директиве-валидаторе, но и еще где-то?
      • 0
        Я рассматриваю пример, когда в сервисе используется ngResource с кастомными методами, а в директиве как раз таки происходит обращение к сервису. Самый типичный пример — валидация email при регистрации нового пользователя. Т.е. пользователь вводит свой эл.адрес, директива видит изменения, опрашивает внутренний метод, который в свою очередь делает запрос на сервер:

        angular.module('fs.directives', [])
                .directive('validEmail',['UInput',function(service){
        
                    var isValid = function(s) {
                 
                        service.check({email:s},function(out){
                            return out.request; 
        
        
                        }); //не рабочий вариант, функция isValid возвращает объект ресурса а не булев результат из ответа
        
        
        
                    };
                    return {
                        require:'ngModel',
                        link: function(scope,elm,attrs,ngModelCtrl)
                        {
                            ngModelCtrl.$parsers.unshift(function (viewValue) {
                                ngModelCtrl.$setValidity('validEmail', isValid(viewValue));
                                return viewValue;
                            });
        
                           ngModelCtrl.$formatters.unshift(function (modelValue) {
                                ngModelCtrl.$setValidity('validEmail', isValid(modelValue));
                                return modelValue;
                            });
                        }
        
        
                    }
        
        
                }]);
        

        На самом деле это можно проще все сделать, но я решил начать с сложного :)
        • 0
          Как proof-of-concept: jsfiddle.net/xAxud/
          ngResource по-моему тоже promise-подобный объект возвращает. Так что там по аналогии должно быть. Я как-то чаще с чистым $http работаю.
          • 0
            Спасибо за пример, вот и я склоняюсь к варианту с $http
  • 0
    Можно создавать иерархии форм

    Вложенные формы стандартом HTML не допускаются.
    • 0
      А Вы это предложение до конца дочитали? На ссылку ngForm кликнули?
      • 0
        Ok, посмотрел, спс. Думаю стоит поправить текст и указать в нем про это нововведение, чтобы не сбивать с толку читателя.
  • 0
    Скажите пожалуйста, а почему в первом примере вместо свойства $validators были использованы $parsers и $formatters?
    • 0
      Потому что на момент написания статьи $validators еще не существавало. Этот механизм был добавлени только в 1.3.
      • 0
        благодарю за разъяснение, почему-то думал что опция $validators раньше появилась

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.