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.

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.

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.

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.

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.

8.4.4. number
Nos permite introducir números y símbolos.

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

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.

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

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.

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

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

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.

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.