8. Formularios y validación

AngularJS se basa en formularios HTML e inputs estándar. Esto quiere decir que podemos seguir creando nuestra UI a partir de los mismos elementos que ya conocemos, usando herramientas de desarrollo HTML estándar.

8.1. Comparando formullarios tradicionales con formularios en AngularJS

Vamos a ver cómo funcionan los formularios en AngularJS, y cómo éste modifica y extiende el comportamiento de los inputs de HTML, y cómo gestiona las actualizaciones del modelo. También veremos las directivas de validación incluidas en el core de AngularJS, para finalmente crear nuestras propias directivas de validación.

En un formulario HTML estándar, el valor de un input es el valor que se enviará al servidor al ejecutarse la acción submit del formulario.

forms001

El problema es que a veces, no queremos trabajar con los datos tal y como se muestran en el formulario. Por ejemplo, podríamos querer mostrar una fecha formateada (ej: 14 de julio de 2.012), pero lo más seguro es que queramos trabajar con un objeto JavaScript de tipo Date. Tener que realizar estas transformaciones constantemente es algo muy tedioso, y puede conducir a errores.

Al desacoplar el modelo de la vista en AngularJS, no nos tenemos que preocupar del valor del modelo cuando éste cambia en la vista, ni del tipo de dato cuando trabajamos con él en un controlador.

forms002

Esto se consigue a través de las directivas form e input, así como con las directivas de validación y los controladores. Estas directivas de validación sobreescriben el comportamiento por defecto de los formularios HTML. Sin embargo, mirando su código, vemos que son prácticamente iguales que los formularios HTML estándar.

En primer lugar, la directiva ngModel nos permite definir cómo los input se deben asociar (bind) al modelo.

Hemos visto cómo AngularJS crea un databinding entre los campos del objeto scope y los elementos HTML en la página, usando dobles llaves {{}} y la directiva ngBind, que explicaremos ahora haciendo un inciso.

8.1.1. La directiva ngBind

En algunos navegadores, podemos experimentar cierto "parpadeo" de valores en AngularJS. Esto se debe a que primero se carga el HTML y luego el código AngualrJS. De este modo, es posible que veamos las variables entre llaves antes que sus valores.

A este fenómeno se le conoce como PRF (Pre-Render Flickering). Para evitarlo, se introdujo la directiva ngBind.

Para hacer uso de ella sólo tenemos que añadir el atributo ng-bind a un elemento, y escribir una expresión dentro de éste. Por ejemplo, en lugar de:

1
<h1>{{model.header.title}}</h1>

podemos usar:

1
<h1 ng-bind="model.header.title"></h1>

Si en nuestro html no teníamos ningún elemento para nuestro texto, siempre podemos utilizar un <span> para introducir ahí nuestra expresión. Como véis, es igual de sencillo que usar los corchetes dobles, y ayuda a prevenir el PRF.

8.2. Continuemos

Como decíamos, ya sabemos cómo se realiza el databinding con la doble llave o la directiva ngBind. Estas técnicas sólo permiten el binding en una dirección (one-way binding). Para asociar el valor de una directiva input, y así conseguir un two-way data binding usamos, además, la directiva ngModel. Veamos el siguiente ejemplo [1]:

1 2 3 4 5 6 7 8 9 10 11
<div ng-app="databinding" ng-controller="MainCtrl"> <div> Hola, {{name}}! </div> <div> Hola, <span ng-bind="name"></span>! </div> <div> <label>Nombre: <input type="text" ng-model="name" /></label> </div> </div>
1 2 3 4 5
angular .module('databinding', []) .controller('MainCtrl', function($scope){ $scope.name = 'Alejandro'; })

En los dos primeros div, bindamos el atributo name del scope con dobles llaves, mientras que en el segundo lo hacemos a través de la directiva ng-bind. Este binding se realiza únicamente en una dirección: si cambiamos el valor de scope.name en el controlador, éste cambiará en la vista. Sin embargo, no hay manera de cambiarlo en la vista y que esto afecte al controlador.

Sin embargo, en el último div, AngularJS binda el valor de scope.name al del elemento input, a través de la directiva ngModel. Aquí es donde se realiza un two-way data binding. Se puede observar sencillamente ya que, si modificamos el valor del input, los otros dos elementos modifican el texto.

Además, veremos que AngularJS permite que las directivas transformen y validen los valores de ngModel en el momento que se realiza el paso de valores de la vista al controlador.

8.3. Creando un formulario de registro

Para tratar estos temas, vamos a crear un formulario de registro, que tendrá los siguientes campos y restricciones.

  • Nombre. Requerido. Longitud mínima de 3 caracteres y máxima de 25.

  • Apellidos. Requerido. Mínimo dos palabras

  • Email. Requerido. Email válido

  • Sexo. Requerido. Será un selector de tipo radio.

  • Website. Requerido. Deberá ser una URL válida.

  • Provincia. Requerido. Será un selector de tipo select.

  • Suscripción a newsletter. Opcional. Tipo checkbox.

Nuestra primera aproximación sería la siguiente [2]:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
<div ng-app="formExample" ng-controller="mainCtrl"> <form ng-submit=""> <div> <label> nombre: <input type="text" ng-model="user.name" /> </label> </div> <div> <label> apellidos: <input type="text" ng-model="user.lastName" /> </label> </div> <div> <label> email: <input type="text" ng-model="user.email" /> </label> </div> <div> sexo: <label> hombre <input type="radio" value="h" ng-model="user.sex" /> </label> <label> mujer <input type="radio" value="m" ng-model="user.sex" /> </label> </div> <div> <label>website: <input type="text" ng-model="user.website" /></label> </div> <div> provincia: <select ng-model="user.province"> <option value="12">Castellón</option> <option value="46">Valencia</option> <option value="03">Alicante</option> </select> </div> <div> <label> suscribirse a la newsletter <input type="checkbox" ng-model="user.newsletter" /> </label> </div> </form> <pre ng-bind="user | json"></pre> </div>

8.3.1. Campos requeridos

Usaremos la directiva ngRequired (o simplemente required) para especificar aquellos campos obligatorios. Así, todos aquellos campos cuyo valor sea null, undefined o una cadena vacía serán inválidos. Por ejemplo, el campo nombre es uno de los campos obligatorios:

1
<input type="text" ng-model="user.name" required />

8.3.2. Tamaño mínimo y máximo

En el campo nombre, también habíamos definido un tamaño mínimo y máximo. Esto lo conseguimos gracias a las directivas ngMinlength y ngMaxLength:

1
<input type="text" ng-model="user.name" required ng-minlength="3" ng-maxlength="25" />

8.3.3. Expresiones regulares

Por su parte, el campo apellidos, además de ser obligatorio, tenía la restricción de que debía contar, al menos con dos palabras. Podemos definir esta restricción de manera sencilla con expresiones regulares. La directiva ngPattern se encarga de validar que un elemento cumpla con una expresión regular determinada:

1
<input type="text" ng-model="user.lastName" required ng-pattern="/^\w+(\s\w`)`$/" />

8.3.4. Email

La validación de emails es muy sencilla. Simplemente debemos cambiar el input type="text" por input type="email". AngularJS ya se encargará realizar las validaciones necesarias para este tipo:

1
<input type="email" ng-model="user.email" required />

8.3.5. Radio buttons

Los radiobuttons proporcionan un grupo fijo de opciones para un campo. Son muy sencillos de implemntar. Sólo hay que asociar los radiobutton de un mismo grupo al mismo modelo. Se usará el atributo estándar value para determinar qué valor pasar al modelo. Así, el valor de sexo será:

1 2 3 4 5 6 7 8 9
<div> sexo: <label> hombre <input type="radio" value="h" ng-model="user.sex" required /> </label> <label> mujer <input type="radio" value="m" ng-model="user.sex" required /> </label> </div>

8.3.6. URLs

Al igual que el type="email", también disponemos de un type="url" para que la validación de URLs sea lo más sencilla posible:

1
<input type="url" ng-model="user.website" required /></label>

8.3.7. Selectores

La directiva select nos permite crear una drop-down list desde la que el usuario puede seleccionar uno o varios ítems. AngularJS nos permite especificar estas opciones de manera estática, como en un select HTML estándar, o de manera dinámica a partir de un array.

De hecho, es muy normal el uso de un array de objetos. Aquí para simplificar, hemos utilizado las tres provincias de la Comunidad Valenciana. Sería normal introducir las 50 provincias de España (más las dos ciudades autónomas de Ceuta y Melilla) a partir de los datos proporcionados por un servicio. Para simplificarlo, vamos a suponer que ya las tenemos en nuestro controlador:

1 2 3 4 5
$scope.provinces = [ { name : 'Castellón', code : '12'}, { name : 'Valencia', code : '46'}, { name : 'Alicante', code : '03'} ];

Para bindar el valor de este array a un elemento select tenemos que asociarle el atributo ng-options:

1 2 3
<select ng-model="user.province" required ng-options="province.code as province.name for province in provinces"> <option value="">-- Seleccione una opción --</option> </select>

En el ejemplo, estamos iterarndo el array de provincias. El valor que se asocia al modelo es el código de provincia. Sin embargo, la opción que se muestra es el nombre de dicha provincia. Además, establecemos un valor vacío por defecto, con option value=""

Aunque esta es la forma más habitual de trabajar con un select en AngularJS, éste nos permite hacerlo de muchas más maneras. La ayuda de AngularJS[3], nos explica cómo hacerlo de todas las maneras posibles cuando esta fuente es un array de cadenas, o un array de objetos.

8.3.8. Checkboxes

Un checkbox no ex más que un valor booleano. En nuestro formulario, la directiva input le asociará el valor true o false al modelo en función de si está marcado o no. En caso de estar marcado, suscribiremos a nuestro usuario al boletín de noticias.

1
<input type="checkbox" ng-model="user.newsletter" />

8.3.9. Probando el formulario con las nuevas restricciones

Si probamos ahora el formulario[4], con las restricciones de validación que hemos añadido, veremos en el elemento <pre> que el objeto adquiere valores cuando superamos dichas restricciones.

8.4. Mejorando la experiencia mobile

Hemos visto el uso de ciertas directivas para validación de URLs, fechas o emails que cambiaban el atributo type de nuestros inputs. Esto tiene una ventaja adicional: usar estos tipos mejoran la experiencia de uso cuando empleamos dispositivos móviles, ya que permiten que el usuario se evite presionar varias veces el teclado para pulsar botones que debería tener a mano. Esto se debe a que el layout del teclado de nuestros móviles se adapta al tipo de input que estamos definiendo. Y, si estamos usando AngularJS, tenemos la ventaja que que la validación del tipo de dato está garantizada.

Ya hemos visto algunos, pero hay más que merece la pena conocer. Repasémoslos todos.

8.4.1. text

El tipo estándar que ya conocemos todos.

text

8.4.2. email

Muchas veces habremos visto lo incómodo que es introducir un email en nuestro móvil, porque la @ siempre está oculta. Esto se soluciona con el type="email", ya que la hace visible. En muchos casos, además, hace que el teclado muestre directamente un botón .com, ya que es la extensión más habitual.

email

8.4.3. tel

El type="tel" abre un teclado numérico, permitiendo al usuario introducir un número de teléfono, y los caracteres típicos asociados a los teléfonos.

tel

8.4.4. number

Nos permite introducir números y símbolos.

number

8.4.5. password

Conocido por todos, oculta los caracteres de una contraseña de la vista de curiosos.

password

8.4.6. date

Ya no nos tendremos que preocupar en nuestros móviles de componentes de tipo calendario, ya que el type="date" nos muestra, en el teclado nativo de nuestro dispositivo, un selector de fechas muy cómodo de utilizar.

date

8.4.7. month

El type="month" es similar al date, permitiéndonos seleccionar un mes y un año.

month

8.4.8. datetime

Otro selector de fechas, esta vez más completo ya que el type="datetime" nos permite seleccionar una fecha y una hora.

datetime

El input type="search" reemplaza el botón ok de nuestros teclados por un botón buscar.

search

8.5. El controlador ngModelController

Cada directiva ngModel crea una instancia de ngModelController. Se trata de un controlador que estará disponible en todas las directivas asociadas al elemento input

ngModelCtrl1

El controlador ngModelController es el encargado de gestionar el data binding entre el valor almacenado en la el modelo, y el que se muestra en el elemento input.

Además, el ngModelController se encarga de determinar que el valor de la vista es válido, y si el input lo ha modificado para actualizar el modelo.

Para esta actualización, sigue un pipeline de transformaciones que se producen cada vez que se actualiza el data binding. Este pipeline consiste en dos arrays:

  • $formatters: transforman el dato del modelo a la vista. Tengamos en cuenta que los inputs sólo entienden datos de tipo texto, mientras que los datos en el modelo pueden ser objetos complejos.

  • $parsers: transforman los datos de la vista a objetos del modelo.

Cualquier directiva que creemos, puede añadir sus propios parsers y formatters al pipeline para modificar lo que ocurre en el data binding. En la siguiente imagen podemos ver cómo afecta el uso de las directivas date y required. La directiva date parsea y formatea las fechas, mientras que la directiva required se asegura que no falte el valor.

ngModelCtrl2

8.5.1. Seguimiento de cambios en el modelo

Además de transformar el valor entre el modelo y la vista el ngModelController realiza un seguimiento de cambios.

Cuando se inicializa por primera vez, el ngModelController marca el valor como pristine (limpio, no modificado). Además, añade al input la clase CSS .ng-pristine. Una vez cambia el valor en la vista, se marca como dirty, y la clase .ng-pristine se substituye por .ng-dirty.

Gracias a estos estilos CSS, podemos cambiar la apariencia de nuestros elementos input en función de si el usuario ha introducido datos o no.

Las siguientes reglas de CSS hacen el elemento más grueso cuando introducimos datos en un input:

.ng-pristine { border: 1px solid black ; }

.ng-dirty { border: 3px solid black; }

8.5.2. Seguimiento de la validez del dato

Al igual que podemos realizar un tracking de cambios sobre el modelo, también lo podemos hacer sobre un dato válido o no.

De manera análoga a como hacía para los valores modificados o no, el ngModelController introduce las clases CSS .ng-valid y .ng-invalid cuando la validación de un elemento es correcta o no.

Por ejemplo, para marcar de verde o rojo los elementos modificados en función de su validez, utilizaremos las siguientes reglas CSS:

.ng-valid.ng-dirty {
  border: 3px solid green;
}

.ng-invalid.ng-dirty {
  border: 3px solid red;
}

8.6. El controlador ngFormController

Al igual que cada ng-model genera un ngModelController, todo elemento form genera un controlador ngFormController. Éste hace uso de todos los ngModelController en su interior y determina si el formulario está pristine o dirty, así como valid o invalid.

Esto es posible porque, cuando se crea un ngModelController, éste busca un formulario en el árbol del DOM, y se registra en el primero que encuentra. Así, el ngFormController sabe a qué directivas debe realizar un seguimiento.

8.6.1. Dando nombres a los elementos

Podemos conseguir que el ngFormController aparezca en el scope, simplemente dándole un nombre al formularios. Además, si damos nombre a todos los elementos input que tengan una directiva ngModelController, éstos aparecerán como propiedades del objeto ngModelController.

1
<form ng-submit="submitAction()" name="userForm">
1
<input type="email" ng-model="user.email" required name="userEmail" />

8.6.2. Validación programática

Al tener los objetos ngModelController y ngFormController en el scope, podemos trabajar con el estado del formulario de maner programática, usando los valores $dirty e $invalid para cambiar lo que está habilitado o visible para el usuario.

Por ejemplo, podemos hacer uso de la directiva `ng-class`[5] para mostrar los elementos que no son válidos:

.invalidelement { border: 1px solid #f00; }
.validelement { border: 1px solid #0f0; }

Aunque podemos hacerlo directamente en la vista:

1 2 3
<label> nombre: <input type="text" ng-model="user.name" required ng-minlength="3" ng-maxlength="25" name="userName" ng-class="{ 'invalidelement' : userForm.userName.$invalid, 'validelement' : userForm.userName.$valid }" />

Es más adecuado y duplicamos menos código llevando esta funcionalidad al controlador:

1 2 3 4 5 6
$scope.getCssClasses = function(ngModelCtrl){ return { invalidelement: ngModelCtrl.$invalid && ngModelCtrl.$dirty, validelement: ngModelCtrl.$valid && ngModelCtrl.$dirty }; };
1 2 3 4
<label> nombre: <input type="text" ng-model="user.name" required ng-minlength="3" ng-maxlength="25" name="userName" ng-class="getCssClasses(userForm.userName)" /> </label>

Para mostrar los errores de validación, haremos uso de la directiva ngShow [6]:

1 2 3
$scope.showError = function(ngModelCtrl, error) { return ngModelCtrl.$error[error]; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<div> <label> nombre: <input type="text" ng-model="user.name" required ng-minlength="3" ng-maxlength="25" name="userName" ng-class="getCssClasses(userForm.userName)" /> </label> <span ng-show="showError(userForm.userName, 'required')"> Campo obligatorio </span> <span ng-show="showError(userForm.userName, 'minlength')"> Longitud mínima: 3 </span> <span ng-show="showError(userForm.userName, 'maxlength')"> Longitud máxima: 25 </span> </div>

En http://codepen.io/alexsuch/pen/fJgaK tenemos un ejemplo funcionando con todas las validaciones y comprobaciones del formulario. Además, en él también hemos introducido un botón para enviar el formulario. Sin embargo no nos interesará enviarlo a menos que estén todos los campos correctamente introducidos. Es por ello que haremos uso de la directiva ngDisabled [7] para deshabilitar el botón si el formulario no es válido.

1
<button type="submit" ng-disabled="userForm.$invalid">Registrar</button>

8.7. Ejercicio (0.5 puntos)

Aplica el tag form a la versión que quieres que se corrija.

Crea una nueva ruta en nuestra página del carrito, llamada /edit/:productId, donde mostraremos un formulario donde editar nuestro producto. Tendrá los campos:

  • Marca. Texto obligatorio. Longitud máxima: 55 caracteres.

  • Modelo. Texto obligatorio. Longitud máxima: 255 caracteres.

  • Precio. Número obligatorio. Mínimo: 0. Máximo: 999.

  • Descripción: Texto obligatorio. Debe contener al menos dos palabras y terminar en punto. Utilizaremos expresiones regulares para validarlo.

El formulario tendrá un botón Guardar, que estará deshabilitado mientras algún ítem del formulario sea incorrecto. Cuando se válido, se añadirá el ítem al listado de productos.


1. http://codepen.io/alexsuch/pen/gezjy
2. http://codepen.io/alexsuch/pen/DkBmn
3. https://docs.angularjs.org/api/ng/directive/select
4. http://codepen.io/alexsuch/pen/yKbre
5. https://docs.angularjs.org/api/ng/directive/ngClass
6. https://docs.angularjs.org/api/ng/directive/ngShow
7. https://docs.angularjs.org/api/ng/directive/ngDisabled