Validación personalizada de formularios en AngularJS

1
22011

En este tutorial vamos a explicar cómo trabajando con formularios en AngularJS podemos añadir una validación de campos personalizada.

0. Índice de contenidos

1. Introducción

Nuestro compañero Jose Manuel nos introdujo a los formularios en AngularJS. Podíamos aprender, además de los componentes para formularios, cómo utilizar las directivas que por defecto proporciona AngularJS para la validación de los campos de un formulario.

En esta ocasión vamos a explicar cómo crear y utilizar directivas personalizadas para tipos de campos no contemplados por defecto en las directivas de validación de AngularJS.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.3 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10.3
  • Entorno de desarrollo:
    • Atom 1.0.4
    • AngularJS 1.4.5
    • Plunker

3. Formulario base

Vamos a crear un formulario en el que se le pida introducir al usuario su NIF.

form.html

<!DOCTYPE HTML>
<html lang="en">


  <head>
    <meta charset="UTF-8">
    <title>Form</title>


    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
    <script src="form-controller.js"></script>


  </head>


  <body ng-app="formAdictos">


    <div ng-controller="TNTUserController">


      <form name="userForm">


        <label for="nif">NIF</label><br />
        <input name="nif" type="text" ng-model="user.nif" />
        
        <br/>


        <input type="button" ng-click="reset(form)" value="Reset" />
        <input type="submit" ng-click="update(user)" value="Save" />


      </form>


    </div>


  </body>


</html>

En la vista creamos la interfaz del formulario. Damos un nombre al formulario con el atributo name para poder referirnos a él en la vista. Añadimos un control input en el que usamos la directiva ng-model de AngularJS para hacer el binding entre la vista y el controlador TNTUserController: así, en el scope compartido entre el controlador y la porción de vista que este controla, se registra una entrada user con el campo nif para albergar el valor del input. Al input le damos un nombre mediante la propiedad name para poder referirnos a él desde la vista.

El controlador del formulario será TNTUserController.

form-controller.js

angular.module('formAdictos', [])
  .controller('TNTUserController', ['$scope', function($scope) {
    $scope.user = {};


    $scope.update = function() {
      console.log(angular.fromJson($scope.user));
    };


    $scope.reset = function(form) {
      $scope.user = {};
      if (form) {
        form.$setPristine();
        form.$setUntouched();
      }
    };


    $scope.reset();
  }]);

En el controlador TNTUserController inicializamos la entrada de user en el objeto $scope y damos de alta dos funciones:

  • la función update, que desde la vista se la llama mediante la directiva ng-click de AngularJS cuando el usuario pincha en el botón de enviar el formulario. Sólo imprime por consola el valor del registro user.
  • la función reset, que desde la vista se la llama mediante la directiva ng-click de AngularJS cuando el usuario pincha en el botón de limpiar el formulario. Se encarga de poner el formulario en el estado inicial.

Hasta aquí un formulario básico sin validaciones.

4. Validación personalizada en el lado del cliente

La validación personalizada de campos en formularios en AngularJS se construye utilizando el mecanismo de creación de directivas. Vamos a añadir una directiva propia llamada tnt-nif-validation:

tnt-nif-validation.js

var app = angular.module('formAdictos');

app.directive('tntNifValidation', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$validators.tntnifvalidation = function(modelValue, viewValue) {


        if (ctrl.$isEmpty(modelValue)) {
          // tratamos los modelos vacíos como correctos
          return true;
        }


        if (viewValue) {


          var letterValue = viewValue.substr(viewValue.length - 1);
          var numberValue = viewValue.substr(viewValue.length - (viewValue.length - 1));
          var controlLetter = "TRWAGMYFPDXBNJZSQVHLCKE".charAt(numberValue % 23);


            if(letterValue === controlLetter ){
              return true;
            } else {
              return false;
            }
        }


      // NIF inválido
      return false;


      };
    }
  };
});

¿A qué tenemos que prestar atención?:

En la propiedad require:

  • Es necesario requerir la directiva ngModel: la instancia del controlador ngModelController para nif será inyectada como el cuarto parámetro de la función que se establezca en la propiedad link.

En la propiedad link:

  • Además de los validadores que ngModelController contiene, se le puede indicar funciones propias de validación añadiéndolas al objeto $validators(tntnifvalidation en este caso). Como hemos dicho, el parámetro ctrl es una instancia de ngModelController, la correspondiente al modelo de nif en este caso.
  • Las funciones que se añadan al objeto $validators reciben los parámetros modelValue y viewValue. Son inyectados y albergan el valor del input en el modelo y en la vista.

Angular llama internamente a la función $setValidity junto con el valor de respuesta de la función de link (true si el valor es válido y false si el valor es inválido). Cada vez que un input se modifica (hay una llamada a $setViewValue) o cuando el valor del modelo cambia, se llama a las funciones de validación registradas. La validación tiene lugar cuando se ejecutan los parsers y los formatters dados de alta en el controlador (objeto $parsers y objeto $formatters). Las validaciones que no se han podido realizar se almacenan por su clave en ngModelController.error.

form.html

<!DOCTYPE HTML>
<html lang="en">


  <head>
    <meta charset="UTF-8">
    <title>Form</title>


    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
    <script src="form-controller.js"></script>
    <script src="tnt-nif-validation.js"></script>


    </script>


    <style type="text/css">


      form.ng-submitted input.ng-invalid{
        border-color: #FA787E;
      }
      form input.ng-invalid.ng-touched {
        border-color: #FA787E;
      }


    </style>


  </head>


  <body ng-app="formAdictos">


    <div ng-controller="TNTUserController">


      <form name="userForm" novalidate>


        <label for="nif">NIF</label><br />
        <input name="nif" type="text" ng-model="user.nif" ng-model-options="{ updateOn: 'blur' }" tnt-nif-validation />
        <span>{{user.nif}}</span>
        <span ng-show="userForm.nif.$invalid">NIF no válido.</span>


        <br/>


        <input type="button" ng-click="reset(form)" value="Limpiar" />
        <input type="submit" ng-click="update(user)" value="Enviar" />


      </form>
    </div>


  </body>


</html>


¿A qué tenemos que prestar atención?:

  • En la etiqueta form se ha añadido el atributo novalidate para que no se realicen comprobaciones nativas de HTML en los controles.
  • En la etiqueta input del nif se añade la directiva tnt-nif-validation. Además, se indica que el valor del registro nif en el scope se actualice cuando el usuario deje de tener el foco en el input (opción blur).
  • Para verificar el funcionamiento de la directiva, hemos indicado que se imprima el valor del nif en el scope ({{user.nif}}), y que nos avise con un mensaje cuando el nif sea inválido (ng-show=»userForm.nif.$invalid»). La instancia de ngModelController del nif nos permite saber el estado de la validación del campo, mediante las propiedades $valid, $invalid y $error. Hay que tener cuidado y distinguir entre $invalid y $error.
  • Se han añadido estilos para resaltar los controles inválidos.

El flujo de ejecución será el siguiente:

  • El usuario introduce un valor para el nif.
  • Cuando cambia el foco a otro elemento del formulario, se intenta actualizar el valor del nif en el scope. Como está añadida la directiva tnt-nif-validation y ha habido un cambio en el valor del input, se llama automáticamente a las funciones registradas en $parsers, $formatters y $validators de la instancia del controlador ngModelController.

    • En el caso de que el valor del campo sea inválido, no se propaga al scope el valor del campo (y en el caso de que ya lo tuviera lo borra) e imprime el mensaje ‘NIF no válido’ ya que encuentra que se encuentra la propiedad $invalid indicando que el estado es inválido. Además se marca en rojo el input.

      En este caso, viewValue y modelValue serán diferentes, desacoplándose la vista y el controlador en el scope. Esto es así para evitar datos inválidos en el modelo.

    • En el caso de que el valor del campo sea válido, el valor se propaga al scope e imprime ese mismo valor.

5. Validación personalizada en el lado del servidor

Es común que haya validaciones que no podamos realizarlas en lado del cliente y tengamos que recurrir a información almacenada en el servidor. Por ejemplo, para comprobar si un usuario está registrado en un sitio web, no se dispone en el cliente del listado de usuarios registrados. Creamos la directiva tnt-user-signedup que dará como válidos los valores de alias de usuarios registrados.

tnt-user-signed-up.js

app.directive('tntUserSignedup', function($q, $timeout) {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
    var usernames = ['adrian', 'josemanuel', 'jorge'];


      ctrl.$asyncValidators.tntusersignedup = function(modelValue, viewValue) {


        if (ctrl.$isEmpty(modelValue)) {
          return $q.when();
        }


        var def = $q.defer();


        $timeout(function() {


          if (usernames.indexOf(modelValue) > -1) {
            // Alias registrado
            def.resolve();
          } else {
            def.reject();
          }


        }, 1000);


        return def.promise;
      };
    }
  };
});

¿A qué tenemos que prestar atención?:

  • A diferencia de en la directiva tnt-nif-validation en la que hemos registrado la función de validación en el objeto $validators del controlador de la directiva ng-model, en este caso hay que registrarla en el el objeto $asyncValidators. Este objeto se encarga de las validaciones asíncronas, como las llamadas http.
  • Se utiliza el mecanismo de promesas: la promesa se resuelve cuando alias está registrado, y se rechaza cuando no lo está (registrando un error en el controlador del modelo).
  • El servidor se mockea con una inyección del servicio $q.

form.html

<!DOCTYPE HTML>
<html lang="en">


  <head>
    <meta charset="UTF-8">
    <title>Form</title>


    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
    <script src="form-controller.js"></script>
    <script src="tnt-nif-validation.js"></script>
    <script src="tnt-user-signedup.js"></script>


    <style type="text/css">


      form.ng-submitted input.ng-invalid{
        border-color: #FA787E;
      }
      form input.ng-invalid.ng-touched {
        border-color: #FA787E;
      }


    </style>


  </head>


  <body ng-app="formAdictos">


    <div ng-controller="TNTUserController">


      <form name="userForm" novalidate>


        <label for="nif">NIF</label><br />
        <input name="nif" type="text" ng-model="user.nif" ng-model-options="{ updateOn: 'blur' }" tnt-nif-validation /> <!-- poner la directiva. ojo con el nombre -->
        <span>{{user.nif}}</span>
        <span ng-show="userForm.nif.$invalid">NIF no válido.</span>


        <br/><br/>
        <label for="alias">Alias</label><br />
        <input name="alias" type="text" ng-model="user.alias" ng-model-options="{ updateOn: 'blur' }" tnt-user-signedup />
        <span ng-show="userForm.alias.$pending">Comprobando alias...</span>
        <span ng-show="userForm.alias.$error.tnt-user-signedup">El usuario no está registrado</span>


        <br/><br/>


        <input type="button" ng-click="reset(form)" value="Limpiar" />
        <input type="submit" ng-click="update(user)" value="Enviar" />


      </form>
    </div>


  </body>


</html>

¿A qué tenemos que prestar atención?:

  • Hemos añadido el control en el formulario para el alias del usuario. Contiene un binding y está indicada la directiva tnt-user-signedup.
  • Hemos añadido dos mensajes que le serán mostrados al usuario:
    • Utilizando la propiedad $pending, mientras se hace la llamada al servidor se le indica al usuario que se está comprobando su alias.
    • Utilizando la propiedad $error, si el alias no está registrado, se le hace saber al usuario (es el error que registra la promesa en la directiva cuando es rechazada).

6. Gestión de los mensajes de validación y reset y envío del formulario

Hemos visto que el input del formulario que sirve para limpiarlo está asociado a la función reset del controlador TNTUserController. Internamente, esta función establece que el usuario no ha interaccionado con ninguno de los controles del formulario ($setPristine()) y que no ha hecho ningún tipo de edición en ellos ($setUntouched()).

Además, en TNTUserController se reinicializa el valor de user en el scope ($scope.user). Cuando los campos de los formularios sean válidos, tanto en la vista como en el modelo se hará efectivo el reinicio de los valores de user.

Hay que tener especial atención cuando un valor sea inválido o erróneo. En primer lugar, las funciones $setPristine() y $setUntouched() sólo actualizan flags internos y no se encargan de resetear el estado de los campos (propiedades $valid, $invalid, $error).

Es común mostrar mensajes para avisar al usuario cuando un campo es inválido o erróneo. Esto se gestiona utilizando la directiva ng-show indicando bajo qué estado o estados de validez del campo queremos que se muestre.

<span ng-show="userForm.nif.$invalid">NIF no válido.</span>

Cuando se limpie el formulario queremos que desaparezcan los mensajes que inicialmente no tienen que aparecer. Como el estado no se reinicia, estos seguirán apareciendo. Por ello, para asegurarnos de que sólo aparecen cuando se ha producido una edición sobre el campo, se comprueba esa condición:

<span class="messagesError" ng-show="userForm.nif.$touched"> 
          <span ng-show="userForm.nif.$invalid">NIF no válido.</span>
 </span>

En segundo lugar cuando el valor de la vista es inválido o erróneo, no se hace efectivo el reinicio del scope en el valor de la vista. Si queremos que siempre se limpie el valor que aparece en el control del formulario, hay que indicar que el tipo del input es reset, para que de manera nativa en HTML se limpie la vista del control.

<input type="reset" ng-click="reset(userForm)" value="Limpiar" />

Con respecto al input de envío del formulario, queremos que esté activo únicamente cuando el formulario sea válido. Automáticamente AngularJS proporciona un controlador FormController para cada etiqueta form. Nos interesa que al igual que en el controlador ngModelController tenemos disponible el estado de un campo, en FormController tenemos disponible el estado del formulario.

Deshabilitamos por tanto el botón de envío cuando el formulario esté en un estado inválido y en este caso, como el formulario hace una transición al el estado $pending cuando valida el alias del usuario asíncronamente, cuando el formulario esté en un estado pendiente.

<input type="submit" ng-click="update(user)" ng-disabled="userForm.$invalid || userForm.$pending" value="Enviar" />

Esta es la implementación completa de la vista:

form.html

<!DOCTYPE HTML>
<html lang="en">


  <head>
    <meta charset="UTF-8">
    <title>Form</title>


    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
    <script src="form-controller.js"></script>
    <script src="tnt-nif-validation.js"></script>
    <script src="tnt-user-signedup.js"></script>


    <style type="text/css">


      .messagesError {
        color: #FA787E;
      }


      .messagesOK{
        color: #21610B;
      }


      form.ng-submitted input.ng-invalid{
        border-color: #FA787E;
      }
      form input.ng-invalid.ng-touched {
        border-color: #FA787E;
      }


    </style>


  </head>


  <body ng-app="formAdictos">


    <div ng-controller="TNTUserController">


      <form name="userForm" novalidate>


        <!-- al hacer el binding referirse a user.nif en el modelo es lo mismo que referirse a userForm.nif en la vista (supongo que el nexo es la directiva) -->
        <label for="nif">NIF</label><br />
        <input name="nif" type="text" ng-model="user.nif" ng-model-options="{ updateOn: 'blur' }" tnt-nif-validation />


        <span class="messagesOK">{{user.nif}}</span>


        <span class="messagesError" ng-show="userForm.nif.$touched">
          <span ng-show="userForm.nif.$invalid">NIF no válido.</span>
        </span>


        <br/><br/>


        <label for="alias">Alias</label><br />
        <input name="alias" type="text" ng-model="user.alias" ng-model-options="{ updateOn: 'blur' }" tnt-user-signedup />


        <span ng-show="userForm.alias.$pending">Comprobando alias...</span>


        <span class="messagesOK" ng-show="userForm.alias.$touched">
          <span ng-show="userForm.alias.$valid && userForm.alias.$viewValue">Usuario registrado</span>
        </span>


        <span class="messagesError" ng-show="userForm.alias.$touched">
          <span ng-show="userForm.alias.$error.tntusersignedup">El usuario no está registrado</span>
        </span>


        <br/><br/>


        <input type="reset" ng-click="reset(userForm)" value="Limpiar" />
        <input type="submit" ng-click="update(user)" ng-disabled="userForm.$invalid || userForm.$pending" value="Enviar" />


      </form>
    </div>


  </body>


</html>

7. Código fuente

El código completo lo puedes encontrar en plunker.

8. Conclusiones

Creando nuestras propias directivas es sencillo realizar la validación de campos de formularios cómodamente de la manera que necesitemos . Hemos visto es posible registrar las funciones de validación en el controlador de la directiva ng-model. Hay dos tipos de validación, la síncrona en la que las funciones se registran en el objeto $validators y la asíncrona, en la que las funciones se registran en el objeto $asyncValidators.

Es conveniente recordar que aunque se realicen validaciones en el front-end, no hay que delegarlas en este, haciendo por tanto siempre una validación del modelo y de la lógica a nivel de back, el cual debe ofrecer unos servicios robustos.

Gracias al mecanismo de binding de AngularJS, se reduce el código javascript que hay que implementar para recuperar los valores introducidos por el usuario en los controles del usuario. Estos valores estarán disponibles en el objeto $scope haciendo uso de la directiva ng-model en cada control.

Las instancias de ngModelController de cada modelo y la de FormController que se crea para el formulario, nos ayudan a gestionar la interacción entre el formulario y el usuario utilizando comprendiendo la transición entre los posibles estados.

9. Referencias

1 COMENTARIO

  1. excelente tutorial, de verdad ayudo mucho ya que apenas voy comenzando en angularJS, pero tengo una duda cuando se tiene que hacer una consulta a una función en específico del lado del servidor?, saludos

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad