¿Cómo agregar validación personalizada a un formulario AngularJS?

278

Tengo un formulario con campos de entrada y configuración de validación agregando los requiredatributos y demás. Pero para algunos campos necesito hacer una validación adicional. ¿Cómo "aprovecharía" la validación que FormControllercontrola?

La validación personalizada podría ser algo así como "si se completan estos 3 campos, entonces este campo es obligatorio y debe formatearse de una manera particular".

Hay un método FormController.$setValiditypero no parece una API pública, así que prefiero no usarlo. Crear una directiva personalizada y usarla NgModelControllerparece otra opción, pero básicamente requeriría que creara una directiva para cada regla de validación personalizada, lo que no quiero.

En realidad, marcar un campo del controlador como no válido (aunque también se mantiene FormControllersincronizado) podría ser lo que necesito en el escenario más simple para hacer el trabajo, pero no sé cómo hacerlo.

botteaap
fuente
44
Hay un buen artículo sobre el monstruo de codificación para manejar validaciones personalizadas en JS angular. Compruebe esto hacia fuera
Anshu
No es exactamente lo que estoy buscando, ya que requiere directivas personalizadas, pero aceptaré su respuesta ya que es un buen artículo de todos modos.
botteaap
Me pregunto lo mismo, me encantaría tener algo de control a nivel de FormController. Por ejemplo, quiero que ciertas directivas personalizadas marquen la instancia de FormController como algo así formName.$warning.
Adam Waselnuk
2
Creo que eso $$precede a las API no públicas, con $ser público. Ver stackoverflow.com/questions/19338493/…
Daniel F

Respuestas:

370

Editar: se agregó información sobre ngMessages (> = 1.3.X) a continuación.

Mensajes de validación de formulario estándar (1.0.X y superior)

Dado que este es uno de los mejores resultados si Google "Validación de forma angular", actualmente, quiero agregar otra respuesta a esto para cualquier persona que venga desde allí.

Hay un método en FormController. $ SetValidity pero que no parece una API pública, así que prefiero no usarlo.

Es "público", no se preocupe. Úsalo. Para eso es. Si no estuviera destinado a ser utilizado, los desarrolladores angulares lo habrían privatizado en un cierre.

Para hacer una validación personalizada, si no desea utilizar Angular-UI como sugiere la otra respuesta, simplemente puede rodar su propia directiva de validación.

app.directive('blacklist', function (){ 
   return {
      require: 'ngModel',
      link: function(scope, elem, attr, ngModel) {
          var blacklist = attr.blacklist.split(',');

          //For DOM -> model validation
          ngModel.$parsers.unshift(function(value) {
             var valid = blacklist.indexOf(value) === -1;
             ngModel.$setValidity('blacklist', valid);
             return valid ? value : undefined;
          });

          //For model -> DOM validation
          ngModel.$formatters.unshift(function(value) {
             ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
             return value;
          });
      }
   };
});

Y aquí hay un ejemplo de uso:

<form name="myForm" ng-submit="doSomething()">
   <input type="text" name="fruitName" ng-model="data.fruitName" blacklist="coconuts,bananas,pears" required/>
   <span ng-show="myForm.fruitName.$error.blacklist">
      The phrase "{{data.fruitName}}" is blacklisted</span>
   <span ng-show="myForm.fruitName.$error.required">required</span>
   <button type="submit" ng-disabled="myForm.$invalid">Submit</button>
</form>

Nota: en la 1.2.X es probablemente preferible sustituir ng-ifpor ng-showencima

Aquí hay un enlace obligatorio de plunker

Además, he escrito algunas entradas de blog sobre este tema que entra en un poco más de detalle:

Validación de forma angular

Directivas de validación personalizada

Editar: usando ngMessages en 1.3.X

Ahora puede usar el módulo ngMessages en lugar de ngShow para mostrar sus mensajes de error. Realmente funcionará con cualquier cosa, no tiene que ser un mensaje de error, pero aquí está lo básico:

  1. Incluir <script src="angular-messages.js"></script>
  2. Referencia ngMessagesen la declaración de su módulo:

    var app = angular.module('myApp', ['ngMessages']);
  3. Agregue el marcado apropiado:

    <form name="personForm">
      <input type="email" name="email" ng-model="person.email" required/>
    
      <div ng-messages="personForm.email.$error">
        <div ng-message="required">required</div>
        <div ng-message="email">invalid email</div>
      </div>
    </form>

En el marcado anterior, ng-message="personForm.email.$error"básicamente especifica un contexto para las ng-messagedirectivas secundarias. Luego ng-message="required"y ng-message="email"especifique las propiedades en ese contexto para ver. Lo más importante, también especifican un orden para registrarlos . El primero que encuentre en la lista que sea "verdadero" gana, y mostrará ese mensaje y ninguno de los otros.

Y un plunker para el ejemplo ngMessages

Ben Lesh
fuente
66
Si devuelve el valor de la función que pasa a $ parsers.unshift, también se guardarán valores erróneos en el modelo; creo que sería mejor devolver un valor indefinido (cuando el valor no es válido).
georgiosd
55
+1 @georgiosd ... 100% correcto. Mirando a través de lo que hace Angular, regresan indefinidos. Probablemente no sea un gran problema devolver el valor, ya que (con suerte) los modelos de formularios no válidos no se envían ... pero supongo que es mejor prevenir que curar.
Ben Lesh
2
¡Buena cosa! Si buscaste en Google aquí buscando un buen artículo sobre validación personalizada en Angular, mira lo que escribió
@blesh
¿Ha verificado la validación avanzada de formularios con AngularJS y filtros ? Resuelve la validación del filtro genéricamente.
Benny Bottema
1
Creo que es posible que haya querido hacer lo return value ? valid : undefinedanterior.
GChorn
92

El proyecto de Angular-UI incluye una directiva ui-validate, que probablemente lo ayudará con esto. Le permite especificar una función para llamar para hacer la validación.

Eche un vistazo a la página de demostración: http://angular-ui.github.com/ , busque el encabezado Validar.

Desde la página de demostración:

<input ng-model="email" ui-validate='{blacklist : notBlackListed}'>
<span ng-show='form.email.$error.blacklist'>This e-mail is black-listed!</span>

luego en su controlador:

function ValidateCtrl($scope) {
  $scope.blackList = ['[email protected]','[email protected]'];
  $scope.notBlackListed = function(value) {
    return $scope.blackList.indexOf(value) === -1;
  };
}
Pete BD
fuente
Qué extraño que esto no funcione para mí usando Angular 1.4
Nick
46

Puede usar ng-required para su escenario de validación ("si se completan estos 3 campos, entonces este campo es obligatorio":

<div ng-app>
    <input type="text" ng-model="field1" placeholder="Field1">
    <input type="text" ng-model="field2" placeholder="Field2">
    <input type="text" ng-model="field3" placeholder="Field3">
    <input type="text" ng-model="dependentField" placeholder="Custom validation"
        ng-required="field1 && field2 && field3">
</div>
Mario G.
fuente
2
Esto funcionó para mí. Para validaciones simples que dependen de otros valores de campo, este es el camino a seguir en lugar de
escribir
28

Puedes usar Angular-Validator .

Ejemplo: uso de una función para validar un campo

<input  type = "text"
    name = "firstName"
    ng-model = "person.firstName"
    validator = "myCustomValidationFunction(form.firstName)">

Entonces en tu controlador tendrías algo como

$scope.myCustomValidationFunction = function(firstName){ 
   if ( firstName === "John") {
       return true;
    }

También puedes hacer algo como esto:

<input  type = "text"
        name = "firstName"
        ng-model = "person.firstName"
        validator = "'!(field1 && field2 && field3)'"
        invalid-message = "'This field is required'">

(donde field1 field2 y field3 son variables de alcance. También es posible que desee comprobar si los campos no son iguales a la cadena vacía)

Si el campo no pasa validator, el campo se marcará como no válido y el usuario no podrá enviar el formulario.

Para obtener más casos de uso y ejemplos, consulte: https://github.com/turinggroup/angular-validator

Descargo de responsabilidad: soy el autor de Angular-Validator

usuario3920706
fuente
13

Recientemente creé una directiva para permitir la invalidación basada en expresiones de entradas de forma angular. Se puede utilizar cualquier expresión angular válida, y admite claves de validación personalizadas mediante notación de objeto. Probado con angular v1.3.8

        .directive('invalidIf', [function () {
        return {
            require: 'ngModel',
            link: function (scope, elm, attrs, ctrl) {

                var argsObject = scope.$eval(attrs.invalidIf);

                if (!angular.isObject(argsObject)) {
                    argsObject = { invalidIf: attrs.invalidIf };
                }

                for (var validationKey in argsObject) {
                    scope.$watch(argsObject[validationKey], function (newVal) {
                        ctrl.$setValidity(validationKey, !newVal);
                    });
                }
            }
        };
    }]);

Puedes usarlo así:

<input ng-model="foo" invalid-if="{fooIsGreaterThanBar: 'foo > bar',
                                   fooEqualsSomeFuncResult: 'foo == someFuncResult()'}/>

O simplemente pasando una expresión (se le dará la clave de validación predeterminada de "invalidIf")

<input ng-model="foo" invalid-if="foo > bar"/>
Alex Schwartz
fuente
13

Aquí hay una forma genial de hacer validaciones de expresiones comodín personalizadas en un formulario (de: Validación avanzada de formularios con AngularJS y filtros ):

<form novalidate="">  
   <input type="text" id="name" name="name" ng-model="newPerson.name"
      ensure-expression="(persons | filter:{name: newPerson.name}:true).length !== 1">
   <!-- or in your case:-->
   <input type="text" id="fruitName" name="fruitName" ng-model="data.fruitName"
      ensure-expression="(blacklist | filter:{fruitName: data.fruitName}:true).length !== 1">
</form>
app.directive('ensureExpression', ['$http', '$parse', function($http, $parse) {
    return {
        require: 'ngModel',
        link: function(scope, ele, attrs, ngModelController) {
            scope.$watch(attrs.ngModel, function(value) {
                var booleanResult = $parse(attrs.ensureExpression)(scope);
                ngModelController.$setValidity('expression', booleanResult);
            });
        }
    };
}]);

Demostración jsFiddle (admite nombres de expresiones y múltiples expresiones)

Es similar a ui-validate, pero no necesita una función de validación específica del alcance (esto funciona genéricamente) y, por supuesto, no necesita ui.utils de esta manera.

Benny Bottema
fuente
Gracias. Muy genial. Es especialmente útil aplicar reglas de validación para formularios dinámicos. Sin embargo, todavía establece el valor del modelo, incluso si no es válido. De todos modos para evitar que establezca el modelValue si no es válido?
YuMei
5

Actualizar:

Versión mejorada y simplificada de la directiva anterior (una en lugar de dos) con la misma funcionalidad:

.directive('myTestExpression', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function (scope, element, attrs, ctrl) {
            var expr = attrs.myTestExpression;
            var watches = attrs.myTestExpressionWatch;

            ctrl.$validators.mytestexpression = function (modelValue, viewValue) {
                return expr == undefined || (angular.isString(expr) && expr.length < 1) || $parse(expr)(scope, { $model: modelValue, $view: viewValue }) === true;
            };

            if (angular.isString(watches)) {
                angular.forEach(watches.split(",").filter(function (n) { return !!n; }), function (n) {
                    scope.$watch(n, function () {
                        ctrl.$validate();
                    });
                });
            }
        }
    };
}])

Ejemplo de uso:

<input ng-model="price1" 
       my-test-expression="$model > 0" 
       my-test-expression-watch="price2,someOtherWatchedPrice" />
<input ng-model="price2" 
       my-test-expression="$model > 10" 
       my-test-expression-watch="price1" 
       required />

Resultado: Expresiones de prueba mutuamente dependientes donde los validadores se ejecutan al cambiar el modelo de directiva de otro y el modelo actual.

La expresión de prueba tiene una $modelvariable local que debe usar para compararla con otras variables.

Previamente:

Intenté mejorar el código de @Plantface agregando directivas adicionales. Esta directiva adicional es muy útil si nuestra expresión necesita ser ejecutada cuando se realizan cambios en más de una variable ngModel.

.directive('ensureExpression', ['$parse', function($parse) {
    return {
        restrict: 'A',
        require: 'ngModel',
        controller: function () { },
        scope: true,
        link: function (scope, element, attrs, ngModelCtrl) {
            scope.validate = function () {
                var booleanResult = $parse(attrs.ensureExpression)(scope);
                ngModelCtrl.$setValidity('expression', booleanResult);
            };

            scope.$watch(attrs.ngModel, function(value) {
                scope.validate();
            });
        }
    };
}])

.directive('ensureWatch', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        require: 'ensureExpression',
        link: function (scope, element, attrs, ctrl) {
            angular.forEach(attrs.ensureWatch.split(",").filter(function (n) { return !!n; }), function (n) {
                scope.$watch(n, function () {
                    scope.validate();
                });
            });
        }
    };
}])

Ejemplo de cómo usarlo para hacer campos validados cruzados:

<input name="price1"
       ng-model="price1" 
       ensure-expression="price1 > price2" 
       ensure-watch="price2" />
<input name="price2" 
       ng-model="price2" 
       ensure-expression="price2 > price3" 
       ensure-watch="price3" />
<input name="price3" 
       ng-model="price3" 
       ensure-expression="price3 > price1 && price3 > price2" 
       ensure-watch="price1,price2" />

ensure-expressionse ejecuta para validar el modelo cuando ng-modelo alguna de las ensure-watchvariables se cambia.

knr
fuente
4

@synergetic Creo que @blesh supone poner la función validar de la siguiente manera

function validate(value) {
    var valid = blacklist.indexOf(value) === -1;
    ngModel.$setValidity('blacklist', valid);
    return valid ? value : undefined;
}

ngModel.$formatters.unshift(validate);
ngModel.$parsers.unshift(validate);
Atul Chaudhary
fuente
4

Validaciones personalizadas que llaman a un servidor

Use la API ngModelController$asyncValidators que maneja la validación asincrónica, como hacer una $httpsolicitud al backend. Las funciones agregadas al objeto deben devolver una promesa que debe resolverse cuando sea válida o rechazada cuando no sea válida. Las validaciones asíncronas en progreso se almacenan por clave ngModelController.$pending. Para obtener más información, consulte la Guía para desarrolladores de AngularJS - Formularios (validación personalizada) .

ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
  var value = modelValue || viewValue;

  // Lookup user by username
  return $http.get('/api/users/' + value).
     then(function resolved() {
       //username exists, this means validation fails
       return $q.reject('exists');
     }, function rejected() {
       //username does not exist, therefore this validation passes
       return true;
     });
};

Para más información, ver


Usando la $validatorsAPI

La respuesta aceptada usa las tuberías $parsersy $formatterspara agregar un validador síncrono personalizado. AngularJS 1.3+ agregó una $validatorsAPI, por lo que no es necesario poner validadores en las tuberías $parsersy $formatters:

app.directive('blacklist', function (){ 
   return {
      require: 'ngModel',
      link: function(scope, elem, attr, ngModel) {           
          ngModel.$validators.blacklist = function(modelValue, viewValue) {
              var blacklist = attr.blacklist.split(',');
              var value = modelValue || viewValue;
              var valid = blacklist.indexOf(value) === -1;
              return valid;
          });    
      }
   };
});

Para obtener más información, consulte AngularJS ngModelController API Reference - $ validators .

georgeawg
fuente
3

En AngularJS, el mejor lugar para definir la Validación personalizada es la directiva Cutsom. AngularJS proporciona un módulo ngMessages.

ngMessages es una directiva diseñada para mostrar y ocultar mensajes en función del estado de un objeto clave / valor en el que escucha. La propia directiva complementa la notificación de mensajes de error con el objeto de error ngModel $ (que almacena un estado clave / valor de errores de validación).

Para la validación de formularios personalizados, uno debe usar ngMessages Modules con una directiva personalizada. Aquí tengo una validación simple que verificará si la longitud del número es menor que 6 mostrar un error en la pantalla

 <form name="myform" novalidate>
                <table>
                    <tr>
                        <td><input name='test' type='text' required  ng-model='test' custom-validation></td>
                        <td ng-messages="myform.test.$error"><span ng-message="invalidshrt">Too Short</span></td>
                    </tr>
                </table>
            </form>

Aquí se explica cómo crear una directiva de validación personalizada

angular.module('myApp',['ngMessages']);
        angular.module('myApp',['ngMessages']).directive('customValidation',function(){
            return{
            restrict:'A',
            require: 'ngModel',
            link:function (scope, element, attr, ctrl) {// 4th argument contain model information 

            function validationError(value) // you can use any function and parameter name 
                {
                 if (value.length > 6) // if model length is greater then 6 it is valide state
                 {
                 ctrl.$setValidity('invalidshrt',true);
                 }
                 else
                 {
                 ctrl.$setValidity('invalidshrt',false) //if less then 6 is invalide
                 }

                 return value; //return to display  error 
                }
                ctrl.$parsers.push(validationError); //parsers change how view values will be saved in the model
            }
            };
        });

$setValidity es una función incorporada para establecer el estado del modelo en válido / inválido

Muhammad Nasir
fuente
1

Extendí la respuesta de @Ben Lesh con la capacidad de especificar si la validación distingue entre mayúsculas y minúsculas o no (predeterminado)

utilizar:

<input type="text" name="fruitName" ng-model="data.fruitName" blacklist="Coconuts,Bananas,Pears" caseSensitive="true" required/>

código:

angular.module('crm.directives', []).
directive('blacklist', [
    function () {
        return {
            restrict: 'A',
            require: 'ngModel',
            scope: {
                'blacklist': '=',
            },
            link: function ($scope, $elem, $attrs, modelCtrl) {

                var check = function (value) {
                    if (!$attrs.casesensitive) {
                        value = (value && value.toUpperCase) ? value.toUpperCase() : value;

                        $scope.blacklist = _.map($scope.blacklist, function (item) {
                            return (item.toUpperCase) ? item.toUpperCase() : item
                        })
                    }

                    return !_.isArray($scope.blacklist) || $scope.blacklist.indexOf(value) === -1;
                }

                //For DOM -> model validation
                modelCtrl.$parsers.unshift(function (value) {
                    var valid = check(value);
                    modelCtrl.$setValidity('blacklist', valid);

                    return value;
                });
                //For model -> DOM validation
                modelCtrl.$formatters.unshift(function (value) {
                    modelCtrl.$setValidity('blacklist', check(value));
                    return value;
                });
            }
        };
    }
]);
Liran Brimer
fuente
0

Algunos ejemplos geniales y libs presentados en este hilo, pero no tenían lo que estaba buscando. Mi enfoque: validez angular : una biblioteca de validación basada en promesas para la validación asincrónica, con un estilo Bootstrap opcional incorporado.

Una solución de validez angular para el caso de uso del OP podría verse así:

<input  type="text" name="field4" ng-model="field4"
        validity="eval"
        validity-eval="!(field1 && field2 && field3 && !field4)"
        validity-message-eval="This field is required">

Aquí hay un violín , si quieres probarlo. La biblioteca está disponible en GitHub , tiene documentación detallada y muchas demostraciones en vivo.

2 sapo
fuente