Manual Básico de AngularJS
Manual Básico de AngularJS
Table of Contents
1. AngularJS .......................................................................................................................... 5
1.1. ¿Qué es AngularJS? .............................................................................................. 5
¿Single-page web applications? ............................................................................ 5
Ejemplos de aplcaciones SPA ....................................................................... 5
1.2. Volviendo a AngularJS… ....................................................................................... 5
1.3. Principales características de AngularJS ............................................................... 6
Two-way data binding ............................................................................................ 6
MVW (Model-View-Whatever) ................................................................................ 7
Plantillas HTML ...................................................................................................... 8
Deep linking ........................................................................................................... 8
Inyección de dependencias ................................................................................... 9
Directivas ............................................................................................................... 9
1.4. Ventajas e inconvenientes de AngularJS ............................................................. 10
Ventajas ............................................................................................................... 10
Inconvenientes ..................................................................................................... 11
1.5. Recursos online .................................................................................................... 11
2. Una tarde con AngularJS ................................................................................................ 12
2.1. Creando la aplicación ........................................................................................... 12
2.2. Directivas, data binding ........................................................................................ 16
2.3. Declarando colecciones e iterando sobre ellas .................................................... 18
2.4. Filtros .................................................................................................................... 19
2.5. Vistas, controladores y scope .............................................................................. 20
2.6. Módulos, Rutas y Factorías ................................................................................. 21
Comunicándonos con el controlador ................................................................... 27
2.7. Usando factorías y servicios ................................................................................ 28
2.8. Ejercicio (1 punto) ................................................................................................ 30
3. Scopes ............................................................................................................................ 31
3.1. Hola mundo (otra vez) ......................................................................................... 31
3.2. El objeto Scope .................................................................................................... 31
Jerarquía y herencia de scopes .......................................................................... 32
Propagación de eventos ...................................................................................... 36
Eventos de AngularJS ................................................................................. 38
El ciclo de vida del scope ................................................................................... 40
Creación ....................................................................................................... 40
Registro de watchers ................................................................................... 40
Mutación del modelo .................................................................................... 40
Observación de la mutación ........................................................................ 40
Destrucción .................................................................................................. 41
3.3. Ejercicios .............................................................................................................. 41
Calculadora (0,66 puntos) ................................................................................... 41
Carrito de la compra (0,67 puntos) ..................................................................... 42
Ping-pong (0,67 puntos) ...................................................................................... 45
4. Módulos y servicios ......................................................................................................... 47
4.1. Hola Mundo (y van tres) ...................................................................................... 47
4.2. Module .................................................................................................................. 47
1
AngularJS
2
AngularJS
3
AngularJS
4
AngularJS
1. AngularJS
1.1. ¿Qué es AngularJS?
AngularJS es un framework de JavaScript de código abierto, mantenido por Google, que ayuda
con la gestión de lo que se conoce como aplicaciones de una sola página (en inglés, single-
page applications). Su objetivo es aumentar las aplicaciones basadas en navegador con (MVC)
Capacidad de Modelo Vista Controlador, en un esfuerzo para hacer que el desarrollo y las
pruebas más fáciles.
AngularJS es un framework JavaScript, mantenido por Google, que se ejecuta en el lado del
cliente, y se utiliza para crear single-page web applications.
En una aplicación SPA, todos los datos necesarios, como el HTML, CSS o JavaScript, se
cargan y añaden en la página cuando es necesario, normalmente respondiendo a acciones
del usuario. En ningún momento del proceso veremos una recarga total de la página. Para
esto, como os imaginaréis a lo largo del proceso de ejecución de una aplicación SPA existe
una comunicación con el servidor en segundo plano.
• GMail
• Google+
5
AngularJS
6
AngularJS
En AngularJS, este mecanismo funciona de la siguiente manera: una plantilla (que es código
HTML) se compila en el navegador. Esta compilación hace que cualquier cambio en el modelo
se refleje inmediatamente en la vista. También hará que todo cambio que realicemos en la
vista se propage al modelo. El modelo es la single-source-of-truth del estado de la aplicación,
lo que simplifica mucho las cosas, al no tener que gestionar más que el modelo, y pensar el
la vista como una proyección de éste.
Dado que la vista es una proyección del modelo, el controlador queda totalmente separado
de la vista y no es consciente de ella. De esta manera, realizar tests sobre un controlador es
mucho más sencillo, ya que no depende de la vista ni de ningún elemento del DOM.
Así, para una pantalla que muestre el nombre de usuario, sería tan sencillo como declarar una
variable con el nombre de usuario en nuestro código JavaScript:
En nuestra vista, utilizaremos la notación {{ }}, que nos pemite ligar expresiones a elementos:
MVW (Model-View-Whatever)
Esto es lo que piensa Igor Minar, lead de AngularJS, cuando se entra en cuestiones sobre
qué patrón sigue el framework.
7
AngularJS
A efectos prácticos, y como se dice en la cita, el patrón Model View ViewModel (MVVM) es
una aproximación bastante cercana para describir de manera general el comportamiento de
AngularJS.
El patrón MVVM funciona muy bien en aplicaciones con interfaces de usuario ricas, ya que
la Vista se vincula al ViewModel y, cuando el estado del ViewModel cambia, la Vista se
actualiza automáticamente gracias, precisamente, al two-way-databinding. En AngularJS, una
Vista es simplemente código HTML compilado con elementos propios del framework. Una vez
finaliza el ciclo de compilación, la Vista se vincula al objeto $scope, que es el ViewModel. Ya
veremos en profundidad el objeto $scope, pero adelantamos que es un objeto JavaScript que
captura ciertos eventos para permitir el data binding. También, podemos exponer funciones
al ViewModel para poder ejecutar funciones.
image::img/ses01/003_mvvm.png
Ya veremos esto más adelante, y veremos lo sencillo que es trabajar con este patrón, que
crea una separación muy clara entre la Vista y la lógica que la conduce. Uno de los "efectos
secundarios" del patrón ViewModel, es que permite que el código sea muy testable.
Plantillas HTML
Otra de las grandes características de AngularJS es el uso de HTML para la creación
de plantillas. Éstas pueden ser útiles cuando queremos predefinir un layout con secciones
dinámicas está conectado con una estructura de datos. Por ejemplo, para repetir un mismo
elemento DOM en una página, como una lista o una tabla. Podríamos definir cómo queremos
que se vea una fila, y después asociarle una estructura de datos, como un array de JavaScript.
Esta plantilla se repetiría tantas veces como ítems encontremos en el array, asociando el ítem
al contenido.
Hay muchas librerías de templating, y muy buenas. Pero para la mayoría de ellas se requiere
aprender una nueva sintaxis. Esta complejidad adicional puede ralentizar al desarrollador.
Además, muchas suelen pasar por un preprocesador.
El navegador evalúa las plantillas de AngularJS como el resto de HTML de la página. Hay
cierta funcionalidad de AngularJS que gestiona cómo se representan los datos, que veremos
cómo funciona en las próximas sesiones.
Deep linking
Como hemos comoentado, AngularJS es un framework para construir aplicaciones SPA. Sin
embargo, es posible que nuestros usuarios no se den cuenta de este detalle. En muchas
aplicaciones SPA modernas, está prohibido usar el back button ya que no se tiene en cuenta
a la hora de programar. Sin embargo, AngularJS hace uso de la API de history de HTML5.
¿Que tu navegador no implementa esa API? Es igual, AngularJS seguirá gestionando bien el
histórico gracias al control de cambios en el hashbang.
Para un usuario, esto significa que se pueden guardar y compartir cualquier estado de la
apliación, cosa muy importante hoy en día debido al social media. También nos permite a los
desarrolladores cambiar el estado de la aplicación de la manera más sencilla posible: mediante
el uso de hipervínculos.
8
AngularJS
Inyección de dependencias
La inyección de dependencias (DI por sus siglas en inglés) describe una técnica que hemos
estado usando toda la vida. Si alguna vez has usado una función que acepta un parámetro, ya
has hecho uso de la inyección de dependencias. Inyectas algo de lo que depende tu función
para realizar su trabajo. Nada más y nada menos.
image::img/ses01/004_di.png
Al modularizar tu código con elementos inyectables, éste es más fácil de testear, ya que
en cualquier momento puedes reemplazar uno de los elementos por otro, siempre y cuando
implemente la misma interfaz.
function a () {
return 5;
}
function b () {
return a() + 1;
}
console.log(b());
service('a', function () {
return 5;
});
service('b', function (a) {
return a() + 5;
});
service('main', function (b) {
console.log(b());
});
Este cambio tiene varias ventajas: por una parte, ya no dependemos del orden, y nuestro
código no tiene que seguir una secuencia. estp implica que, podemos extraer cualquiera de los
bloques a otro fichero, lo que en aplicaciones grandes será más que conveniente. Además, en
cualquier momento podemos sobreescribir cualquiera de las funciones, cosa muy importante
para realizar tests.
Directivas
Las directivas son la parte más interesante de AngularJS, ya que nos permiten extender HTML
para que realice todo lo que nosotros queramos.
9
AngularJS
Podemos crear elementos del DOM personalizados, atributos o clases que incorporan cierta
funcionalidad definida en JavaScript. Aunque HTML es excelente para definir un layout, para
el resto se queda corto. Las directivas nos proporcionan un mecanismo para unir la naturaleza
declarativa de HTML con la naturaleza funcional de JavaScript en un mismo elemento. Así,
cuando aprendamos a utilizar directivas, en lugar de pintar un modal de bootstrap de esta
manera:
Para los curiosos, podéis ir jugando con el código de este modal en http://jsfiddle.net/alexsuch/
RLQhh/.
Ventajas
10
AngularJS
Inconvenientes
• Al igual que hemos dicho que la curva de aprendizaje inicial es muy baja, cuando
queremos hacer algo avanzado en aplicaciones más serias, la cosa puede resultar un poco
difícil debido a la falta de documentación, o documentación errónea en algunos casos.
Afortunadamente, el equipo de AngularJS está trabajando a diario en esto, y entre esto y
la cada vez más creciente comunidad de usuarios, este problema se va minificando a la
carrera.
• El framework de validación de formularios no es del todo perfecto, y de vez en cuando hay
que hacer algunos trucos para que haga lo que nosotros queremos.
• Como pasa con todos los frameworks, ninguno es la panacea y encontraremos escenarios
donde AngularJS no encaje.
2
• Lista de correo en [email protected]
• Comunidad de Google+ https://plus.google.com/u/0/
communities/115368820700870330756
• Canal de IRC #angularjs
• Tag *angularjs* en http://stackoverflow.com
• Twitter @angularjs
2
mailto:[email protected]
11
AngularJS
Además, tenemos diversos sitios donde poder seguir con el aprendizaje de AngularJS, como
pueden ser:
Una vez terminada la sesión, seremos capaces de crear aplicaciones sencillas con AngularJS,
y tendremos conocimiento de unos fundamentos que iremos desarrollando y ampliando en
sesiones posteriores.
Como hay una parte del código de los ejercicios que evolucionará a lo
largo de las sesiones, identificaremos cada entrega mediante el uso de
tags. El nombre de dichos tags se indicará en cada apartado de ejercicios.
12
AngularJS
Crearemos un proyecto vacío, que será la base de todos nuestros módulos y será lo que
subiremos a BitBucket
13
AngularJS
14
AngularJS
15
AngularJS
<!DOCTYPE html>
16
AngularJS
<html ng-app>
<head>
<title>Una tarde con AngularJS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
</head>
<body>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
</body>
</html>
Una vez añadido el script de AngularJS en nuestra página, ya podemos empezar a usarlo. Para
esto, nos vamos a topar con el primero de los elementos propios de AngularJS: las directivas.
Una directiva, como se ha mencionado, nos permite extender HTML, creando componentes,
clases o atributos con una funcionalidad dada.
Por ejemplo, en el código de arriba, ya vemos nuestra primera directiva, y una de las más
importantes: ng-app . Por convenio, todo lo que tenga la forma ng-* va a ser una directiva
built in de AngularJS. Dado que no son exclusivas del core del framework y nosotros podemos
crearnos nuestras propias directivas, por convenio las third parties suelen utilizar un prefijo de
2-3 caracteres para cada directiva y así evitar conflictos de nombres.
También podemos definir un módulo de AngularJS, que será nuestro módulo raíz para la
aplicación. De momento, no le definiremos ningún nombre a la directiva ya que podemos hacer
muchas cosas sin añadir ningún módulo. Por ejemplo, probemos el siguiente bloque de código:
<!DOCTYPE html>
<html ng-app>
<head>
<title>Una tarde con AngularJS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<link rel="stylesheet" href="components/lib/twitter-bootstrap/css/
bootstrap.css" />
</head>
<body>
<div class="container">
<label>Nombre:</label> <input type="text" ng-model="name" /
><br/>
Has escrito {{ name }}.
</div>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
</body>
17
AngularJS
</html>
Como algunos sabréis, la especificación de HTML5 dice que los custom attributes deben
empezar con el prefijo data- . Si en algún proyecto os encontráis que hay que seguir esta
especificación no os preocupéis: el core de AngularJS soporta este prefijo e identifica todas
las directivas que lo lleven.
<!DOCTYPE html>
<html ng-app>
<head>
<title>Una tarde con AngularJS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<link rel="stylesheet" href="components/lib/twitter-bootstrap/css/
bootstrap.css" />
</head>
<body>
<div class="container" ng-init="people = ['Domingo', 'Otto',
'Aitor', 'Eli', 'Fran', 'José Luís', 'Alex']">
<ul>
<li ng-repeat="person in people">{{ person }}</li>
</ul>
</div>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
</body>
</html>
Por su parte, hemos visto que únicamente hemos declarado una plantilla para la colección, y
ha sido la directiva ng-repeat quien se ha encargado de instanciarla para cada elemento
de la colección. El formato de expresión más utilizado para esta directiva es variable
in expression , donde variable es el nombre de una variable definida por el usuario,
18
AngularJS
y expression es el nombre de nuestra colección. Más adelante, veremos que hay más
expresiones que podemos usar con esta directiva.
2.4. Filtros
Otra cosa que podemos usar en AngularJS son filtros. Un filtro se encarga de recibir entrada
determinada, realizar una transformación sobre ella, y devolver el resultado de la misma.
<!DOCTYPE html>
<html ng-app>
<head>
<title>Una tarde con AngularJS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<link rel="stylesheet" href="components/lib/twitter-bootstrap/css/
bootstrap.css" />
</head>
<body>
<div class="container" ng-init="people = [{name:'Domingo',
subject:'JPA'}, {name:'Otto', subject:'Backbone'},
{name:'Aitor',subject:'JavaScript'}, {name:'Miguel Ángel',subject:'JHD'},
{name:'Eli', subject:'REST'}, {name:'Fran', subject:'Grails'},
{name:'José Luís', subject:'PaaS'}, {name:'Alex', subject:'AngularJS'}]">
<label>Filtro:</label> <input type="text" ng-
model="textFilter" /><br/>
<ul>
<li ng-repeat="person in people | filter:textFilter |
orderBy:'name'">{{ person.name }} - {{ person.subject | uppercase }}</li>
</ul>
</div>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
</body>
</html>
AngularJS dispone de una gran cantidad de filtros predefinidos. En el bloque de código anterior,
vemos cómo utilizamos, el filtro uppercase para transformar una cadena a mayúsculas una
cadena, o el filtro orderBy nos permite ordenar una colección de elementos. También vemos
que podemos encadenar tantos filtros como queramos. En la directiva ng-repeat usamos
los filtros filter y orderBy .
Al encadenar filtros, el resultado se irá propagando al siguiente en el orden que los hemos
declarado. En este caso, primero Hemos usado el filtro filter para filtrar elementos
basándonos en el modelo textFilter . Luego, hemos ordenado el conjunto de resultados
por la propiedad name .
Para no repetirnos en el uso de filtros ya tienen cierto coste computacional, podemos declarar
variables en una directiva ng-repeat :
<ul>
<li ng-repeat="person in filteredPeople = (people | filter:textFilter |
orderBy:'name')">{{ person.name }} - {{ person.subject | uppercase }}</
li>
</ul>
19
AngularJS
Nos detendremos en el capítulo dedicado a filtros a explicar cómo funciona cada uno de ellos.
También, veremos cómo construir nuestros propios filtros.
En AngularJS, tenemos una vista, como las que hemos estado viendo en los anteriores
ejemplos con sus filtros, sus directivas y su data binding. Como se ha comentado, no queremos
declarar variables en la vista, porque hace nuestro código menos portable y testable. Para
estos menesteres, disponemos de un objeto JavaScript llamado Controller , que va a
gestionar qué datos se pasan a la vista, si éstos se actualizan y, también, va a comunicarse
con el servidor en caso de que haya que actializar información en su lado.
Otra ventaja, es que podemos tener un único controlador vinculado a diferentes vistas. Por
ejemplo, podemos tener una vista para una versión mobile otra para una versión desktop
asociadas al mismo controlador.
¿Y qué es exactamente un ViewModel? Un ViewModel no es, ni más ni menos, que los datos
que van a ser gestionados por la vista. Y eso es lo que el scope es.
Vamos a transformar nuestro último ejemplo, creando un controlador y pasando los datos a
la vista, en lugar de declararlos en ella.
<!DOCTYPE html>
<html ng-app>
<head>
<title>Una tarde con AngularJS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<link rel="stylesheet" href="components/lib/twitter-bootstrap/css/
bootstrap.css" />
</head>
<body>
<div class="container" ng-controller="TeachersCtrl">
<label>Filtro:</label> <input type="text" ng-
model="textFilter" /><br/>
<ul>
<li ng-repeat="person in filteredPeople = (people
| filter:textFilter | orderBy:'name')">{{ person.name }} -
{{ person.subject | uppercase }}</li>
</ul>
Encontrados {{ filteredPeople.length }} resultados.
</div>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
20
AngularJS
<script>
function TeachersCtrl($scope){
$scope.people = [
{name:'Domingo', subject:'JPA'},
{name:'Otto', subject:'Backbone'},
{name:'Aitor',subject:'JavaScript'},
{name:'Miguel Ángel',subject:'JHD'},
{name:'Eli', subject:'REST'},
{name:'Fran', subject:'Grails'},
{name:'José Luís', subject:'PaaS'},
{name:'Alex', subject:'AngularJS'}
];
}
</script>
</body>
</html>
Vemos que nuestro controlador es, simplemente, una función JavaScript. Una cosa interesante
es que le pasamos como parámetro la variable $scope . Este $scope se pasa por Inyección
de Dependencias, otra de las características de AngularJS de las que hemos hablado. En el
momento en que se usa este controlador, AngularJS le inyectará de manera automática el
objeto $scope . Una vez lo tenga, el controlador le añade una propiedad, llamada people,
que es el array que anteriormente habíamos declarado en nuestra vista.
El controlador actúa como fuente de datos para la vista, pero no debería saber nada
de la vista. Así que por eso inyectamos la variable $scope . Ésta va a permitir que
nuestro controlador se comunique con la vista. El $scope pasará a la vista, una vez ésta
sepa cuál es el controlador que la gestiona. Esto lo conseguimos gracias al atributo ng-
controller="TeachersCtrl" . El scope estará visible para el div que lo ha llamado,
así como para sus hijos. Vemos que el resto de nuestro código no ha variado y podemos
seguir accediendo a la colección people . Solo que ahora estamos accediendo a la propiedad
people del $scope .
Ahora vamos a ver cómo modularizar nuestra aplicación, y conceptos como Rutas y Factorías.
Cuando definimos una ruta en AngularJS, también debemos definir dos elementos:
• Una vista. Por ejemplo, si estamos en la ruta /profile , entonces mostraremos la vista
/partials/profile.html
• Un controlador. En lugar de definir el controlador en la vista con la etiqueta ng-
controller , podemos establecerlo al definir una ruta. Siguiendo con el ejemplo anterior,
asociaríamos el controlador ProfileCtrl
21
AngularJS
En la vista, tenemos el apoyo de las directivas y filtros, algunos de las cuales ya hemos visto.
Para poder definir rutas, tenemos que estructurar nuestra aplicación un poquito mejor de lo
que hemos hecho hasta ahora. Aunque lo que hemos hecho hasta ahora es una aplicación
AngularJS, no es la manera recomendada de implementarla.
En primer lugar, tenemos que definir un módulo en nuestra etiqueta ng-app , cosa que
no habíamos hecho aún. Dentro de nuestro objeto module es donde vamos a poder
configurar nuestras rutas, y también definir filtros, directivas, controladores, factorías, y demás
servicios que serán específicos para nuestra app. Podríamos pensar en un module como un
contenedor de objetos donde podemos tener todas estas cosas.
22
AngularJS
Lo importante es definir un nombre para nuestro módulo. Como estamos haciendo una
pequeña aplicación que muestra un listado de profesores y asignaturas, la llamaremos
teachersApp . Éste es el nombre que irá en nuestra etiqueta [ng-app].
<html ng-app="teachersApp">
Fácil, ¿no?. Con esto, estamos creando un módulo, y diciendo a AngularJS que se va a llamar
teachersApp. Vemos que está seguido por un array vacío. Aquí es donde vemos la potencia
de la inyección de dependencias. Resulta que nuestro módulo puede incluir otros módulos de
los que nuestra aplicación depende en cierto grado. Por ejemplo, en el siguiente fragmento
estamos incluyendo un módulo llamado myCustomModule :
Aquí, estamos diciendo a Angular que busque otro módulo, llamado myCustomModule , y
lo inyecte en el nuestro, de manera que todos sus elementos (directivas, filtros, factorías, …
) estarán disponibles en el momento que los inyectemos.
Haciendo una analogía, el incluir aquí estos elementos sería como hacer un import de un
paquete completo en Java.
Crear un controlador para nuestro módulo no es nada distinto a como lo hemos hecho hasta
ahora. La única diferencia radica en que hay que definirlo dentro de nuestro módulo:
teachersApp.controller('teachersCtrl', function($scope){
$scope.people = [
{name:'Domingo', subject:'JPA'},
{name:'Otto', subject:'Backbone'},
23
AngularJS
{name:'Aitor',subject:'JavaScript'},
{name:'Miguel Ángel',subject:'JHD'},
{name:'Eli', subject:'REST'},
{name:'Fran', subject:'Grails'},
{name:'José Luís', subject:'PaaS'},
{name:'Alex', subject:'AngularJS'}
];
});
La manera de llamar al controlador desde la vista es exactamente la misma que hace un rato.
<!DOCTYPE html>
<html ng-app="teachersApp">
<head>
<title>Una tarde con AngularJS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<link rel="stylesheet" href="components/lib/twitter-bootstrap/css/
bootstrap.css" />
</head>
<body ng-controller="TeachersCtrl">
<div class="container">
<label>Filtro:</label> <input type="text" ng-
model="textFilter" /><br/>
<ul>
<li ng-repeat="person in filteredPeople = (people
| filter:textFilter | orderBy:'name')">{{ person.name }} -
{{ person.subject | uppercase }}</li>
</ul>
Encontrados {{ filteredPeople.length }} resultados.
</div>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
<script>
var teachersApp = angular.module('teachersApp', []);
teachersApp.controller('TeachersCtrl', function($scope){
$scope.people = [
{name:'Domingo', subject:'JPA'},
{name:'Otto', subject:'Backbone'},
{name:'Aitor',subject:'JavaScript'},
{name:'Miguel Ángel',subject:'JHD'},
{name:'Eli', subject:'REST'},
{name:'Fran', subject:'Grails'},
{name:'José Luís', subject:'PaaS'},
{name:'Alex', subject:'AngularJS'}
];
});
</script>
</body>
</html>
En el ejemplo, hemos decidido refactorizar nuestro código y crear una función anónima para el
controlador. Recordemos que esto es JavaScript, donde las funciones se pueden pasar como
argumentos. Otra manera de hacer lo mismo tocando menos código hubiera sido:
24
AngularJS
function TeachersCtrl($scope){
$scope.people = [
{name:'Domingo', subject:'JPA'},
{name:'Otto', subject:'Backbone'},
{name:'Aitor',subject:'JavaScript'},
{name:'Miguel Ángel',subject:'JHD'},
{name:'Eli', subject:'REST'},
{name:'Fran', subject:'Grails'},
{name:'José Luís', subject:'PaaS'},
{name:'Alex', subject:'AngularJS'}
];
}
var teachersApp = angular.module('teachersApp', []);
teachersApp.controller('TeachersCtrl', TeachersCtrl);
Una vez hemos definido un módulo y un controlador, en algún momento vamos a tener que
definir rutas para nuestra aplicación SPA.
Veamos aquí un ejemplo de rutas para una aplicación. En algún momento, debido a un evento,
nuestra aplicación pasará de la vista 1 a la 2, mapeada por la ruta /view2 , de la 2 a la 3…
y así realizando un ciclo.
Lo importante aquí es saber que, con el cambio de ruta, no se recargará la página completa,
sino el bloque que nosotros hayamos indicado.
25
AngularJS
A partir de la versión 1.2, el módulo de rutas se extrajo del core de AngularJS como módulo
independiente, con lo que lo primero que tendremos que hacer es traernos este módulo:
<script src="https://code.angularjs.org/1.2.16/angular-route.min.js"></
script>
Como hemos mencionado anteriormente, para incluir módulos extras en el nuestro, tenemos
que declararlo en la definición:
Una vez hecho esto, ya podemos definir las rutas. Hemos dicho anteriormente que un módulo
de AngularJS tenía una función de configuración, donde definíamos las rutas. Vamos a usar
esta función, a la que tenemos que inyectarle un objeto llamado $routeProvider .
En cualquier otro caso, podemos definir una ruta por defecto a la que seremos redirigidos si
introducimos algo a donde nuestra aplicación no sabe qué hacer con ello. Para este ejemplo,
será la lista de profesores:
teachersApp.config(function($routeProvider){
$routeProvider
.when('/teachers',{
controller : 'teachersCtrl',
templateUrl : 'teachers.html'
})
.when('/subjects',{
templateUrl : 'subjects.html'
})
.otherwise({ redirectTo : '/teachers'});
});
Para cada ruta, podemos definir una plantilla y un controlador. La configuración de rutas no
obliga a introducir un par controlador - plantilla, como vemos en la segunda ruta.
Para adaptar nuestra aplicación a lo que acabamos de definir, tenremos que crearnos una
página HTML, a la que llamaremos teachers.html. En ella estará el listado de profesores.
26
AngularJS
Tendremos que refactorizar nuestro fichero index.html e introducir una nueva directiva,
llamada ng-view .
<!DOCTYPE html>
<html ng-app="teachersApp">
<head>
...
</head>
<body ng-controller="teachersCtrl">
<div class="container">
<ul class="nav nav-pills nav-justified">
<li><a href="#/teachers">Profesores</a></li>
<li><a href="#/subjects">Asignaturas</a></li>
</ul>
<ng-view></ng-view>
</div>
<script src="https://code.angularjs.org/1.2.16/angular.min.js"></
script>
<script src="https://code.angularjs.org/1.2.16/angular-
route.min.js"></script>
<script>
...
</script>
</body>
</html>
Justo antes de la directiva ng-view hemos definido dos links, que nos permitirán navegar
por la aplicación. Al no estar incluídos dentro del bloque ng-view , éstos permanecerán
invariables durante toda la navegación.
<h1>Profesorado</h1>
<label>Filtro:</label> <input type="text" ng-model="textFilter" /><br/>
<ul>
<li ng-repeat="person in filteredPeople = (people | filter:textFilter
| orderBy:'name')">{{ person.name }} - {{ person.subject | uppercase }}</
li>
</ul>
27
AngularJS
Fijémonos en el botón, que incorpora una nueva directiva, llamada ng-click . Ésta responde
al evento onClick , realizando una llamada a la función addTeacher() .
Como hemos dicho antes, la vista no sabe nada del controlador. Pero disponemos del objeto
$scope , que permite exponer elementos del controlador en la vista. Lo hemos hecho con
una colección (el array de profesores), y también podemos exponer una función:
teachersApp.controller('teachersCtrl', function($scope){
$scope.people = [
...
];
$scope.addTeacher = function() {
$scope.people.push({
name : $scope.newTeacher.name,
subject : $scope.newTeacher.subject
});
};
});
Vemos que no se ha pasado ningún dato como parámetro, ya que al pasarlo como ng-model
en nuestros inputs, ya lo podemos obtener a través del $scope .
AngularJS nos permite encapsular los datos de nuestra aplicación en una serie de elementos:
• Factorías
• Servicios
• Providers
• Valores
• Constantes
Los tres primeros (Factorías, Servicios y Providers), además de datos, nos permiten
encapsular funcionalidades dentro de nuestra aplicación. Por ejemplo, si necesito mi lista de
profesores en múltiples controladores, lo correcto sería guardarlos en uno de estos elementos.
Estos tres elementos pueden realizar la misma funcionalidad, y la diferencia entre ellos radica
en cómo se crean. Lo veremos más adelante.
28
AngularJS
Además, estos elementos implementan el patrón singleton, lo que los convierte en los
candidatos perfectos para intercambiar información entre controladores.
Así, ahora vamos a modificar nuestro código para utilizar una factoría, donde podremos
obtener el listado de profesores, así como añadir ítems a la lista. Una factoría en AngularJS
devuelve un objeto javascript, con lo que su forma será la siguiente:
teachersApp.factory('teachersFactory', function(){
var teachers = [
{name:'Domingo', subject:'JPA'},
{name:'Otto', subject:'Backbone'},
{name:'Aitor',subject:'JavaScript'},
{name:'Miguel Ángel',subject:'JHD'},
{name:'Eli', subject:'REST'},
{name:'Fran', subject:'Grails'},
{name:'José Luís', subject:'PaaS'},
{name:'Alex', subject:'AngularJS'}
];
return {
getTeachers : function() {
return teachers;
},
addTeacher : function(newTeacher) {
teachers.push({
name : newTeacher.name,
subject : newTeacher.subject
})
}
}
});
Más adelante en el curso, veremos de qué manera podemos obtener ese listado de profesores
a través de una llamada AJAX o un servicio REST, en lugar de tener ese array harcoded en
nuestra factoría.
$scope.addTeacher = function() {
teachersFactory.addTeacher($scope.newTeacher);
};
});
Como hemos dicho, una factoría implementa el patrón singleton. Esto significa que se instancia
una única vez (la primera vez que es requerida), y está disponible durante toda la vida de la
29
AngularJS
Otra cosa que debemos saber, es que AngularJS permite encadenar operaciones. Esto
significa que podemos refactorizar nuestro código de la siguiente manera:
angular
.module('teachersApp', ['ngRoute'])
.config(function($routeProvider){
...
})
.controller('teachersCtrl', function($scope, teachersFactory){
...
})
.factory('teachersFactory', function(){
...
});
Todo el código de la sesión estará dentro de una carpeta llamada intro también.
Tras haber realizado este breve tutorial, ya tenemos unos fundamentos bastante básicos
del core de AngularJS. Opcionalmente, podemos ampliar nuestra aplicación realizando los
siguientes incrementos:
3
http://www.w3schools.com/jsref/jsref_splice.asp
4
https://docs.angularjs.org/api/ng/directive/ngRepeat
30
AngularJS
3. Scopes
La mayoría de las aplicaciones web están basadas en el patrón MVC (Model-View-Controller).
Sin embargo, MVC no es un patrón muy preciso, sino un patrón arquitectural de alto nivel.
Además, existen muchas variaciones del patrón original, siendo los más conocidos MVP
y MVVM. Para añadir un poco más de confusión, muchos frameworks y desarrolladores
interpretan estos patrones de manera diferente. Esto da como resultado que el nombre MVC
se use para describir diferentes arquitecturas y aproximaciones.
<!DOCTYPE html>
<html>
<head>
<title>Hola mundo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body ng-app="holaMundo">
<div ng-controller="SaludaCtrl">
Saluda a: <input type="text" ng-model="nombre" /><br/><br/>
<h1>¡Hola, {{ nombre }}!</h1>
</div>
<!DOCTYPE html>
<html>
31
AngularJS
<head>
<title>Hola mundo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body ng-app="holaMundo">
<div ng-controller="SaludaCtrl">
Saluda a: <input type="text" ng-model="nombre" /><br/><br/>
<h1>¡Hola, {{ getNombre() }}!</h1>
</div>
$scope.getNombre = function() {
return $scope.nombre.toUpperCase();
};
});
</script>
</body>
</html>
De esta forma, podemos controlar precisamente qué parte del modelo y qué operaciones
queremos exponer a la capa de presentación. Conceptualmente, un $scope tiene un
comportamiento muy similar al de un ViewModel en el patrón MVVM.
Podemos instanciar un nuevo $scope en cualquier momento a través del método $new() .
Cuando lo instanciamos, un $scope hereda las propiedades de su padre, como vemos en
este ejemplo:
padre.saludo = "Hola";
hijo.nombre = "Mundo";
hijo.saludo = "Bienvenido";
Como hemos visto, podemos instanciar un nuevo scope en cualquier momento, lo normal
es que AngularJS lo haga por nosotros cuando lo necesitemos. Por ejemplo, la directiva ng-
32
AngularJS
controller instancia un nuevo scope, que será el que inyecte en el controlador. En este
caso, el scope será hijo del $rootScope .
A las directivas que crean nuevos scopes se las conoce como scope creating directives, y
como hemos dicho será el propio AngularJS el encargado de crear los scopes por nosotros
cuando se encuentre con una de estas directivas en el árbol del DOM.
Los scopes forman una estructura de árbol, cuya raíz siempre será el
$rootScope . Como la creación de scopes, está dirigida por el árbol del
DOM, no debería resultar extraño que el árbol de scopes imite de alguna
manera el árbol del DOM.
Ahora que sabemos que algunas directivas crean nuevos scopes hijos, quizá nos preguntemos
por qué tanta complejidad. Para entenderlo, echemos un ojo a este ejemplo que utiliza la
directiva ng-repeat :
El controlador sería:
<ul ng-controller="peopleCtrl">
<li ng-repeat="person in people">{{person.name}} da la asignatura:
{{person.subject}} ({{ person.hours}} horas)</li>
</ul>
La directiva ng-repeat nos permite iterar sobre una colección de, en este caso, personas.
Mientras itera, irá creando nuevos elementos en el DOM. El problema es que si utilizáramos
el mismo $scope , estaríamos sobreescribiendo el valor de la variable persona . AngularJS
soluciona este problema creando un nuevo $scope para cada elemento de la colección.
Como se ha comentado, los scopes generan una jerarquía en forma de árbol, similar a la de
los elementos del DOM. La extensión de AngularJS para Chrome nos permite verla:
33
AngularJS
Podemos ver en el pantallazo que cada scope (delimitado por un recuadro rojo) tiene su propio
conjunto de valores del modelo. Así, cada item tiene su propio namespace, donde cada <li>
posee un scope propio donde se puede definir la variable person .
Otra característica interesante de los objetos scope es que toda propiedad que definamos
en un scope será visible para sus descendientes. Es muy interesante porque hace que no
sea necesario redefinir elementos a medida que creamos scopes hijos.
Siguiendo con el ejemplo anterior, podemos calcular, en el scope del controlador padre, el
número total de horas en base al conjunto de gente:
34
AngularJS
Como habíamos comentado, este total de horas se propagará a los ámbitos hijos, que podrán
hacer uso de él para, por ejemplo, determinar el porcentaje del total que supondrá cada
asignatura:
<ul ng-controller="PeopleCtrl">
<li ng-repeat="person in people">{{person.name}} da la asignatura:
{{person.subject}} ({{ person.hours}} horas - {{person.hours / hours *
100 | number:2}}%)</li>
</ul>
La herencia resulta muy sencilla de usar cuando estamos leyendo, pero sin embargo cuando
estamos escribiendo puede darnos algún problema. Supongamos el siguiente bloque de
código:
<div ng-app>
<form name="myForm" ng-controller="Ctrl">
Input dentro de un switch:
<span ng-switch="show">
<input ng-switch-when="true" type="text" ng-model="myModel" />
</span>
<br/>
Input fuera del switch:
<input type="text" ng-model="myModel" />
<br/>
Valor:
{{myModel}}
</form>
</div>
function Ctrl($scope) {
$scope.show = true;
$scope.myModel = 'hello';
}
Si manipulamos el segundo input , todos los elementos se modificarán a la vez. Pero, ¿qué
sucede si modificamos el primero, y luego el segundo nuevamente? Parece como que el
primero queda desconectado del resto. De hecho, se crea una nueva variable en el scope
hijo que hace que esto funcione de esta manera. Podéis hacer la prueba usando el inspector
de AngularJS para Chrome.
35
AngularJS
Esto es fácil de solucionar, haciendo uso de objetos. Podemos redeclarar el objeto myModel
de la siguiente manera:
function Ctrl($scope) {
$scope.show = true;
$scope.myModel = { value: 'hello'};
}
<div ng-app>
<form name="myForm" ng-controller="Ctrl">
Input dentro de un switch:
<span ng-switch="show">
<input ng-switch-when="true" type="text" ng-model="myModel.value" />
</span>
<br/>
Input fuera del switch:
<input type="text" ng-model="myModel.value" />
<br/>
Valor:
{{ myModel.value }}
</form>
</div>
Existe otra manera, que es hacer uso del elemento parent . Éste hace referencia al ámbito
padre, y lo podríamos llamar de la siguiente manera en el switch:
<span ng-switch="show">
<input ng-switch-when="true" type="text" ng-model="$parent.myModel" />
</span>
Sin embargo, esto resolvería nuestros problemas sólo si el dato estuviera en el ámbito padre,
no si el padre también lo hubiera heredado. Además, no podemos estar seguros al 100% que
parent va a ser el ámbito superior, ya que puede que estemos empleando alguna directiva
que haya creado un scope adicional.
8
Podemos encontrar más información acerca de esto en este enlace
9
También, en este vídeo , Miško Hevery hace una serie de reflexiones
sobre cómo trabajar con los scopes.
Propagación de eventos
Como hemos comentado, en nuestra aplicación se generará un árbol de objetos scope
similar a la estructura del DOM. En la raíz de este árbol se encuentra el objeto $rootScope
8
https://github.com/angular/angular.js/wiki/Understanding-Scopes
9
https://www.youtube.com/watch?v=ZhfUv0spHCY&t=29m19s
36
AngularJS
Podemos usar esta jerarquía para transmitir eventos dentro del árbol, tanto en dirección
ascendente con el método scope.$emit como descendente con scope.$broadcast .
37
AngularJS
Eventos de AngularJS
• $includeContentRequested
• $includeContentLoaded
• $viewContentLoaded
• $locationChangeStart
• $locationChangeSuccess
• $routeUpdate
• $routeChangeStart
• $routeChangeSuccess
• $routeChangeError
• $destroy
Podemos ver que se usan escasamemnte en el core de AngularJS. Pese a ser una manera
sencilla de intercambiar datos entre controladores, debemos evaluar si es la mejor opción. Por
ejemplo, en muchos casos puede ser útil el uso del two-way data binding para obtener una
solución más sencilla.
Esto nos lleva a otro método interesante del objeto scope . Es el método
watch(watchExpression, [listener], [objectEquality]) , que registra un
listener que se ejecuta cada vez que el resultado de la expresión watchExpression cambia.
38
AngularJS
El listener puede modificar el modelo si así lo desea, lo que podría implicar la activación
de otros listeners, re-lanzando los watchers hasta que no se detecta ningún cambio. Para
prevenir entrar en bucles infinitos, existe un límite de re-lanzado de iteraciones, que es 10.
El siguiente ejemplo hace uso de una expresión y de una función de evaluación para observar
una serie de cambios, y realizar una acción al respecto, que será contabilizar el número de
cambios realizados sobre la variable.
<!DOCTYPE html>
<html ng-app="ses03.watch">
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body ng-controller="WatchCtrl">
<p> <label>Nombre <input type="text" ng-model="name"/></label> </p>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/
angular.min.js"></script>
<script>
angular
.module('ses03.watch', [])
.controller('WatchCtrl', function ($scope) {
$scope.name = 'alex';
$scope.counter = 0;
39
AngularJS
</html>
Cuando se hace esta llamada JavaScript al navegador, el código se ejecuta fuera del contexto
de ejecución de AngularJS, lo que significa que AngularJS no tiene ni idea de que se haya
modificado el modelo. Para poder procesar estas modificaciones en el modelo, hay que hacer
que todo lo que se ha hecho fuera del contexto de ejecución de AngularJS entre dentro de
él a través del método $apply . Sólo las modificaciones del modelo que hagamos dentro de
un método $apply serán tenidas en cuenta por AngularJS. Por ejemplo, una directiva como
ng-click que escucha eventos del DOM, debe evaluar la expresión dentro de un método
12
$apply . Así lo podemos ver En su código fuente .
Tras evaluar la expresión el método $apply realiza un $digest . En esta fase, el scope
examina todas las expresiones $watch y compara sus resultados con los valores previos de
manera asíncrona. Esto significa que una asignación como $scope.username = 'admin'
no lanzará inmediatamente el listener de $watch('username') . En lugar de eso, se
retrasará hasta la fase de $digest , de manera que se unifican las actualizaciones del
modelo, y se garantiza que se ejecute una función $watch a la vez. Si un $watch cambia
el modelo, forzará un ciclo $digest adicional.
Esto podemos resumirlo en las cinco fases por las que pasa una aplicación en AngularJS:
Creación
El $injector crea el objeto rootScope durante el application bootstrap. Cuando se
produce el linkado de plantillas, algunas directivas crearán nuevos scopes.
Registro de watchers
Durante el linkado de plantillas, las directivas suelen registrar watchers. Éstos se usarán para
propagar los valores del modelo al DOM.
Observación de la mutación
Al final de $apply , AngularJS realiza un ciclo $digest en el rootScope que se
propagará posteriormente a todos los hijos. Durante este ciclo, todas las expresiones en un
$watch se evaluarán para observar cambios en el modelo y, si esta se detecta, se invocará
al listener.
12
https://github.com/angular/angular.js/blob/master/src/ng/directive/ngEventDirs.js#L50
40
AngularJS
Destrucción
Cuando no se necesita más un scope hijo, su creador tiene la responsabilidad de destruirlo
mediante una llamada a scope.destroy() . Esto detendrá la propagación llamadas
$digest al hijo, y permitirá la llamada al recolector de basura para eliminar la memoria
usada.
3.3. Ejercicios
Aplica el tag scopes a la versión que quieres que se corrija.
Completa el siguiente código para implementar una calculadora que haga sumas, restas,
multiplicaciones y divisiones.
<div ng-app>
<h1>Calculadora</h1>
<div ng-controller="CalcController">
<div>
<label>Primer operando <input type="number" /></label>
</div>
<div>
<label>Segundo operando operando <input type="number" /></label>
</div>
<div>
<button ng-click="">Suma</button>
<button ng-click="">Resta</button>
<button ng-click="">Multiplicación</button>
<button ng-click="">División</button>
</div>
<h2>Resultado: XXX</h2>
</div>
</div>
function CalcController($scope) {
$scope.add = function() {
};
$scope.substract = function(a,b) {
};
$scope.divide = function() {
};
$scope.multiply = function(a,b) {
};
41
AngularJS
Vamos a hacer uso de la función $watch que nos ofrece el scope para implementar un
sencillo carro de la compra.
Tendremos que implementar la función addToCart , para que añada ítems al carro. Además,
implementaremos un watcher que observará cambios en el tamaño de dicho array. Cuando
éstos se produzcan, actualizaremos la variable $scope.totalItems al número de ítems
del carro. También, actualizaremos el valor de la variable $scope.total , con el importe total
14
de los productos. Se recomienda hacer uso de la función Array.prototype.reduce
para calcular este total.
<div ng-app>
<div ng-controller="CartCtrl">
<h2>{{totalItems}} ítems en la cesta ({{total}} €)</h2>
<div ng-repeat="product in products" style="float:left">
<div>
<img ng-src="{{product.picture}}" alt="" />
<p>
{{product.brand}} {{product.name}}
<br/>
{{product.price}} €
</p>
<button ng-click="addToCart(product)">Añadir a la cesta</button>
</div>
</div>
</div>
</div>
function CartCtrl($scope) {
$scope.total = 0;
$scope.totalItems = 0;
$scope.cart = [];
$scope.addToCart = function(product){
//TODO: ADD TO CART
};
//TODO: WATCH
13
http://beta.json-generator.com/OpatzFu
14
https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/reduce
42
AngularJS
$scope.products = [
{
"_id": "54c688af49814edb036a2c33",
"price": 145.4,
"picture": "http://lorempixel.com/200/200/food/?id=846.9758",
"brand": "adidas",
"name": "Zone Job",
"rating": 4
},
{
"_id": "54c688afeffb1bad23bea9f7",
"price": 137.24,
"picture": "http://lorempixel.com/200/200/technics/?id=155.1389",
"brand": "nike",
"name": "Zoomair",
"rating": 0
},
{
"_id": "54c688af5f342fa2c06d2f3b",
"price": 80.47,
"picture": "http://lorempixel.com/200/200/food/?id=927.9926",
"brand": "reebok",
"name": "Volit",
"rating": 2
},
{
"_id": "54c688afbe7e2107363d0945",
"price": 93.53,
"picture": "http://lorempixel.com/200/200/animals/?id=387.7504",
"brand": "nike",
"name": "Konkis",
"rating": 4
},
{
"_id": "54c688af92223b7f877f096f",
"price": 94.82,
"picture": "http://lorempixel.com/200/200/nightlife/?id=296.9771",
"brand": "adidas",
"name": "Stockstring",
"rating": 3
},
{
"_id": "54c688af24de4b9fc39e0d48",
"price": 109.24,
"picture": "http://lorempixel.com/200/200/city/?id=427.4133",
"brand": "adidas",
"name": "Dong-Phase",
"rating": 1
},
{
"_id": "54c688afee99272b911e93fd",
"price": 92.19,
"picture": "http://lorempixel.com/200/200/nature/?id=580.5475",
"brand": "adidas",
"name": "Duozoozap",
"rating": 0
},
{
43
AngularJS
"_id": "54c688af3593d3f6a34bc2a4",
"price": 82.37,
"picture": "http://lorempixel.com/200/200/nightlife/?id=366.9091",
"brand": "reebok",
"name": "X-dom",
"rating": 3
},
{
"_id": "54c688af804d847b847935ac",
"price": 76.53,
"picture": "http://lorempixel.com/200/200/nature/?id=971.7978",
"brand": "nike",
"name": "Konkis",
"rating": 3
},
{
"_id": "54c688af96ba1759662c4274",
"price": 90.01,
"picture": "http://lorempixel.com/200/200/city/?id=37.581",
"brand": "reebok",
"name": "Ecooveit",
"rating": 4
},
{
"_id": "54c688afa4ee53c977c9ca3a",
"price": 81.28,
"picture": "http://lorempixel.com/200/200/transport/?id=752.8523",
"brand": "adidas",
"name": "Superstrong",
"rating": 1
},
{
"_id": "54c688af85e103fa79c83752",
"price": 134.79,
"picture": "http://lorempixel.com/200/200/fashion/?id=358.5133",
"brand": "reebok",
"name": "Superstrong",
"rating": 5
},
{
"_id": "54c688af04a986612841b7dc",
"price": 76.37,
"picture": "http://lorempixel.com/200/200/cats/?id=912.0469",
"brand": "reebok",
"name": "Fresh-Home",
"rating": 0
},
{
"_id": "54c688af5605253556078cff",
"price": 147.47,
"picture": "http://lorempixel.com/200/200/transport/?id=884.8266",
"brand": "adidas",
"name": "Touch-Hold",
"rating": 2
},
{
"_id": "54c688afa71c0978b878efd6",
"price": 106.83,
44
AngularJS
"picture": "http://lorempixel.com/200/200/fashion/?id=598.1251",
"brand": "nike",
"name": "Saorunlab",
"rating": 2
},
{
"_id": "54c688af0450426ca2d7680a",
"price": 72.76,
"picture": "http://lorempixel.com/200/200/food/?id=831.3375",
"brand": "adidas",
"name": "Konkis",
"rating": 1
},
{
"_id": "54c688afe30ff32f443e7c20",
"price": 83.46,
"picture": "http://lorempixel.com/200/200/sports/?id=604.4555",
"brand": "adidas",
"name": "Rank Sololax",
"rating": 5
},
{
"_id": "54c688afd24500b5cbc85148",
"price": 77.19,
"picture": "http://lorempixel.com/200/200/abstract/?id=333.8619",
"brand": "reebok",
"name": "Ecooveit",
"rating": 0
},
{
"_id": "54c688afb08a3950c86aa47f",
"price": 82.05,
"picture": "http://lorempixel.com/200/200/food/?id=118.5947",
"brand": "nike",
"name": "Zonedex",
"rating": 5
},
{
"_id": "54c688af234056a9e3f5c902",
"price": 100.76,
"picture": "http://lorempixel.com/200/200/technics/?id=20.7657",
"brand": "reebok",
"name": "Saorunlab",
"rating": 0
}
];
}
<div ng-app>
45
AngularJS
<style>
.ng-scope {
border: 1px dotted red;
margin: 5px;
}
</style>
<div ng-controller="Controller1">
<div ng-init="ping = 0"></div>
<div ng-init="pong = 0"></div>
<div ng-controller="Controller2">
<button ng-click="">Emit event</button>
<button ng-click="">Broadcast event</button>
<div ng-controller="Controller2">
<button ng-click="">Emit event</button>
<button ng-click="">Broadcast event</button>
</div>
</div>
</div>
function Controller1($scope) {
$scope.$on('ping', function(){
$scope.ping``;
});
$scope.$on('pong', function(){
$scope.pong``;
});
}
function Controller2($scope) {
function Controller3($scope) {
46
AngularJS
Si ahora pulsamos en cualquiera de los dos primeros botones, veremos que los contadores
ping y pong adquieren los mismos valores.
Deberemos:
No se puede renombrar ninguno de los nombres de variable de la vista (todos deben llamarse
ping y pong ).
Se ha introducido un poco de CSS para que se delimiten bien los tres scope que hay en
la aplicación.
4. Módulos y servicios
Desde el inicio de las sesiones, hemos visto que se han usado en varias ocasiones funciones
globales para definir controladores. Esto acaba aquí, definirlo de esta manera es poco
elegante, afecta a la estructura de la aplicación y hace el código más difícil de mantener y
testear. Es por ello que AngularJS dispone de una serie de APIs que nos permiten definir
módulos, y definir objetos dentro de ésto, de una manera muy sencilla.
y digamos hola a
El objeto angular define una serie de utilidades. Una de ellas es module, que permite
definir módulos. Un módulo es como una especie de contenedor de objetos gestionados por
AngularJS, como por ejemplo los controladores.
4.2. Module
Para que el inyector de AngularJS sepa cómo crear y conectar todos estos objetos, neceista
un registro de recetas. Cada receta tiene un identificador del objeto y la descripción de cómo
crearlo.
Una receta debe pertenecer a un módulo de AngularJS, que es un saco que contiene una o
más recetas. Y además, un módulo también puede contener información sobre otros módulos.
47
AngularJS
Cuando se inicia una aplicación de AngularJS con un módulo, AngularJS crea una instancia
de un injector, que es quien crea el registro de las recetas como una unión de las existentes
en el core, el módulo de la aplicación y sus dependencias El inyector consulta al registro de
recetas cuando ve que tiene que crear un objeto para la aplicación.
Para definir un módulo, indicamos su nombre como primer argumento. El segundo argumento
es un array, en el que incluiremos otros módulos, en caso de tener alguna dependencia. Ya
vimos algo de esto en la sesión de introducción cuando introdujimos las rutas.
Una vez hemos definido un módulo, tenemos que informar a AngularJS de su existencia, cosa
que haremos dando un valor a la directiva ng-app :
<body ng-app="helloApp">
4.3. Servicios
Una vez hemos declarado un módulo, hemos dicho que podemos usarlo para registrar una
serie de recetas para la creación de objetos de diversa índole.
Pero aunque sean de diversa índole, podríamos categorizar estos objetos en dos grandes
grupos: servicios y objetos especializados.
Un servicio es un objeto cuya API está definida por el desarrollador que escribe dicho servicio.
Por su parte, un objeto especializado se ajusta a una API específica de AngularJS. Estos
objetos pueden ser: controladores, directivas, filtros o animaciones.
El inyector de AngularJS necesita saber cómo crear estos objetos, y se lo decimos tipificando
nuestro objeto a la hora de crearlo. Hay cinco tipos de recetas.
La más verbosa, pero también la más comprensible, es la del Provider . El resto ( Value ,
factory , Service y constant ) son sólo azúcar sintáctico sobre la definición de un
Provider .
48
AngularJS
Value
Digamos que queremos un servicio muy sencillo, llamado clientId , que nos devuelve un
String que representa el identificador de usuario que usamos para alguna API remota. Lo
podríamos definir de esta manera
<html ng-app="myApp">
<body ng-controller="DemoController">
Client ID: {{clientId}}
</body>
</html>
En este ejemplo, hemos usado la receta Value para dársela a DemoCtrl cuando invoca
al servicio clientId .
Factory
Un Value es fácil de escribir, pero se echan de menos unos elementos importantes que
necesitamos a menudo a la hora de escribir servicios. Así, pasaremos a conocer un elemento
más complejo, llamado factory . Una factory nos permite:
Una factory construye un nuevo servicio mediante una función con cero o más argumentos.
Estos argumentos son dependencias con otros servicios, que se inyectarán en tiempo de
creación.
Una factory no es más que una versión más potente de un Value , de manera que
podemos reescribir el servicio clientId de la siguiente manera:
49
AngularJS
return 'a12345654321x';
});
Pero dado que el token no es más que un a cadena, quizá crear un Value sería más
apropiado en este caso, y el código sería además más sencillo de interpretar.
Digamos que, por ejemplo, queremos crear un servicio encargado de calcular un token para
autenticarse contra una API remota. Este token se llamará apiToken y se calculará en
función del valor de clientId , y de una clave secreta guardada en el almacenamiento local
del navegador:
return apiToken;
}]);
En el código anterior, vemos cómo se define el servicio apiToken con la receta de una
factory que depende del servicio clientId . Entonces crea un token de autenticación a
16
través de una encriptación hiperpotente indescifrable por la NSA .
De igual manera que un Value una factory puede crear un servicio de cualquier tipo, ya
sea una primitiva, un objeto, una función o una instancia de un tipo de dato propio. primitive,
object literal, function, or even an instance of a custom type.
Service
Los desarrolladores JavaScript usan tipos de datos custom para escribir código OO. Veamos
17
cómo podríamos lanzar un Unicornio al espacio mediante un servicio unicornLauncher, que
es una instancia del siguiente objeto:
function UnicornLauncher(apiToken) {
this.launchedCount = 0;
this.launch = function() {
// make a request to the remote api and include the apiToken
...
this.launchedCount++;
}
16
http://en.wikipedia.org/wiki/National_Security_Agency
17
http://en.wikipedia.org/wiki/Unicorn
50
AngularJS
Ya podemos lanzar unicornios al espacio, pero démonos cuenta que nuestro lanzador requiere
un apiToken . Lo bueno es que ya habíamos creado una factory que resolvía este
problema.
La receta de un Service produce un servicio, de igual manera que habíamos visto con
Value y factory , pero lo hace invocando un constructor mediante el operador new. El
constructor puede recibir cero o más argumentos, que representan dependencias que necesita
la instancia de este tipo.
Provider
Como se ha dicho anteriormente, el Provider es la base sobre la que se crean el resto
de servicios que hemos visto en los apartados anteriores. Precisamente porque es la base
sobre la que se asientan el resto, no será difícil comprender que es la que nos ofrece
mayores posibilidades. Pero en la mayoría de los casos todo lo que ofrece es excesivo, y será
recomendable hacer uso de los otros servicios.
La receta de un Provider se define como un tipo propio que implementa un método $get .
18
Este método es una función factoría , como el que se usa en una factory . De hecho, si
definimos una factory , lo que se hace es crear un Provider vacío cuyo método $get
apunta directamente a nuestra función.
Deberíamos usar un Provider cuando queremos introducir cierta configuración que esté
disponible a para toda la aplicación. Para asegurar esto, esto debe hacerse antes de que
se ejecute la aplicación, en una fase llamada fase de configuración. De esta manera,
podemos crear servicios reutilizables cuyo comportamiento podría cambiar ligeramente entre
aplicaciones.
Por ejemplo, nuestro lanzador de unicornios es tan potente y útil que lo van a usar muchas de
las aplicaciones del parque de aplicaciones de nuestra empresa. Por defecto, el lanzador de
unicornios los proyecta al espacio sin ningún tipo de protección ni escudo. Pero en algunos
planetas, la atmósfera es tan pesada que debemos proteger a nuestros unicornios con papel
de plata para evitar que se incineren al atravesar la atmósfera y así evitar su extinción. Estaría
muy bien que pudiéramos configurar esto nuestro lanzador, y usarlo en la app que haga falta.
Lo haríamos configurable de la siguiente manera:
18
http://en.wikipedia.org/wiki/Factory_method_pattern
51
AngularJS
this.useTinfoilShielding = function(value) {
useTinfoilShielding = !!value;
};
Para activar el traje espacial de papel de plata, necesitamos crear una función config en la
API de nuestro módulo, e inyectar en ella el unicornLauncherProvider :
myApp.config(["unicornLauncherProvider", function(unicornLauncherProvider)
{
unicornLauncherProvider.useTinfoilShielding(true);
}]);
Durante la inicialización de la aplicación, antes de que AngularJS haya creado ningún servicio,
configura e instancia los providers. A esto lo llamamos la fase de configuración del ciclo de
vida de la aplicación. Durante esta fase, como hemos dicho, los servicios no son accesibles
porque aún no se han creado.
Constant
Hemos visto cómo AngularJS divide el ciclo de vida de una aplicación en las fases de
configuración y ejecución, y cómo se puede dotar de configuración a la aplicación a través
de la función config . Dado que la función config se ejecuta en una fase en la que no
tenemos servicios disponibles, no se tiene acceso ni siquiera a objetos sencillos creados con
la utilidad Value .
Sin embargo, podemos tener valores tan simples como un prefijo de una url, que no necesiten
dependencias o configuración, y que sean útiles en las fases de configuración y ejecución.
Para esto sirve la utilidad constant .
52
AngularJS
del planeta es específico para cada aplicación, y también lo usan muchos controladores en
tiempo de ejecución. Podemos definir entonces el nombre del planeta como una constante:
Y dado que una constant hace que el valor también esté disponible en la fase de ejecución,
podemos usarla también en nuestros controladores:
<html ng-app="myApp">
<body ng-controller="DemoController as demo">
Client ID: {{demo.clientId}}
<br>
Planet Name: {{demo.planetName}}
</body>
</html>
A excepción del objeto controller , el inyector usa la receta de una factory para crear
estos objetos.
Ya hemos visto algo de estos objetos en las sesiones anteriores, y profundizaremos más en
los siguietnes capítulos.
4.5. En resumen
Un inyector usa una serie de recetas para crear dos tipos de objetos: servicios y objetos de
propósito especial. Para crear servicios, usamos cinco tipos de receta dinstintos: Value ,
factory , Service , Provider y constant .
De ellos, los más comunes són factory y Service , y sólo se distinguen en que los
Service funcionan mejor con tipos de objetos ya definidos, y una factory devuelve
53
AngularJS
4.6. Ejercicios
Aplica el tag modules a la versión que quieres que se corrija.
5. Filtros
Un filtro se encarga de formater el valor de una expresión, para ofrecérsela al usuario sin
modificar el valor original. Puede usarse en vistas, controladores o servicios y son muy
sencillos de declarar y programar.
En una vista
Para usar un filtro en una vista, podemos aplicar una expresión con la siguiente sintaxis:
{{ expression | filter }}
{{ 12 | currency }} <!-- 12€ -->
54
AngularJS
Los filtros, además, pueden encadenarse. En el siguiente ejemplo, la salida del primer filtro
se pasa como entrada del segundo.
angular.module('FilterInControllerModule', []).
controller('FilterController',
['$scope', 'numberFilter', function($scope, numberFilter) {
$scope.filteredText = numberFilter(12,2);
}]);
Otra manera consiste en inyectar el servicio $filter en nuestro código javascript. Con él,
podemos llamar a todos los filtros de la siguiente manera:
angular.module('FilterInControllerModuleV2', []).
controller('FilterController', ['$scope', '$filter', function($scope,
$filter) {
$scope.filteredText = $filter('number')(12,2);
}]);
55
AngularJS
"ejemplo" para hacer las búsquedas en el array). El último parámetro es opcional nos permite
implementar una función de comparación customizada.
Ejemplo:
<div ng-app>
<div ng-init="friends = [
{name:'John', phone:'555-1276'},
{name:'Mary', phone:'800-BIG-MARY'},
{name:'Mike', phone:'555-4321'},
{name:'Adam', phone:'555-5678'},
{name:'Julie', phone:'555-8765'},
{name:'Juliette', phone:'555-5678'}
]"></div>
currency
20
El filtro currency nos permite expresar un número en formato moneda. En una plantilla
HTML lo usaremos de la forma:
56
AngularJS
21
a una región en concreto, lo mejor es instalar el módulo ngLocale que corresponda,
en nuestro caso sería angular-locale_es-es.js . Según la versión de AngularJS que
estemos utilizando, tendremos que añadirlo a nuestro módulo principal.
angular.module('myModule', ['ngLocale'])
Ejemplo:
<script src="http://path/to/angular-locale_es-es.js"></script>
<div ng-app>
{{ 1234567890.25 | currency }}
</div>
22
• Ejemplo de moneda con locale por defecto de AngularJs
23
• Ejemplo de moneda con locale español
date
24
El filtro date nos permite formatear una fecha de la manera deseada. En una plantilla
HTML lo usaremos de la forma:
El parámetro date puede ser de varios tipos, aunque lo más habitual es que sea un objeto
Date o una fecha en milisegundos. Tanto format como timezone son opcionales. En la
referencia de este filtro en la web de AngularJS podemos ver todas las formas que acepta.
<script src="http://path/to/angular-locale_es-es.js"></script>
<div ng-app>
<span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>:
<span>{{1288323623006 | date:'medium'}}</span><br>
<span ng-non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</
span>:
<span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br>
<span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</
span>:
<span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br>
<span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</
span>:
<span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br>
21
https://github.com/angular/angular.js/tree/master/src/ngLocale
22
http://codepen.io/alexsuch/pen/EaXWyK?editors=101
23
http://codepen.io/alexsuch/pen/gbWgZe?editors=101
24
https://docs.angularjs.org/api/ng/filter/date
57
AngularJS
</div>
25
• Ejemplo de date con locale español
26
• Ejemplo de date con locale por defecto
json
27
El filtro json recibe un objeto como entrada, y devuelve una cadena representando dicho
objeto en formato JSON. En nuestro código HTML se usa de la siguiente forma:
$filter('json')(object, spacing)
<div ng-app>
<h3>Default spacing</h3>
<pre id="default-spacing">{{ {nombre:'Alejandro', asignatura:'Frameworks
JavaScript: AngularJS'} | json }}</pre>
<h3>Custom spacing</h3>
<pre id="custom-spacing">{{ {nombre:'Domingo', asignatura: 'Frameworks de
persistencia: JPA'} | json:4 }}</pre>
</div>
limitTo
29
El filtro limitTo recibe como entrada un array, del que tomará un número de elementos
igual al parámetro recibido ( limit ). Estos elementos se tomaran del principio si el número
es positivo, y del final si es negativo. En nuestro código HTML se usa de la siguiente forma:
$filter('limitTo')(input, limit)
30
Ejemplo :
25
http://codepen.io/alexsuch/pen/LELWRW?editors=101
26
http://codepen.io/alexsuch/pen/wBeJzY?editors=101
27
https://docs.angularjs.org/api/ng/filter/json
28
http://codepen.io/alexsuch/pen/pvweNM
29
https://docs.angularjs.org/api/ng/filter/limitTo
30
http://codepen.io/alexsuch/pen/Pwjpmd
58
AngularJS
<script>
angular.module('limitToExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.numbers = [1,2,3,4,5,6,7,8,9];
$scope.letters = "abcdefghi";
$scope.longNumber = 2345432342;
$scope.numLimit = 3;
$scope.letterLimit = 3;
$scope.longNumberLimit = 3;
}]);
</script>
<div ng-app="limitToExample">
<div ng-controller="ExampleController">
Limit {{numbers}} to: <input type="number" step="1" ng-
model="numLimit">
<p>Output numbers: {{ numbers | limitTo:numLimit }}</p>
Limit {{letters}} to: <input type="number" step="1" ng-
model="letterLimit">
<p>Output letters: {{ letters | limitTo:letterLimit }}</p>
Limit {{longNumber}} to: <input type="number" step="1" ng-
model="longNumberLimit">
<p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p>
</div>
</div>
lowercase
31
El filtro lowercase convierte una cadena a minúsculas. En nuestro código HTML se usa
de la siguiente forma:
{{ lowercase_expression | lowercase}}
$filter('lowercase')(lowercase_expression)
Ejemplo:
uppercase
32
El filtro uppercase convierte una cadena a mayúsculas. En nuestro código HTML se usa
de la siguiente forma:
{{ uppercase_expression | uppercase}}
31
https://docs.angularjs.org/api/ng/filter/lowercase
32
https://docs.angularjs.org/api/ng/filter/uppercase
59
AngularJS
$filter('uppercase')(uppercase_expression)
Ejemplo:
number
33
El filtro number formatea un número en el locale que hayamos importado, y con el número
de decimales indicado en su segundo parámetro, que es opcional.
$filter('number')(number, fractionSize)
34
Ejemplo :
<script>
angular.module('numberFilterExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.val = 1234.56789;
}]);
</script>
<div ng-app="numberFilterExample">
<div ng-controller="ExampleController">
Enter number: <input ng-model='val'><br>
Default formatting: <span id='number-default'>{{val | number}}</
span><br>
No fractions: <span>{{val | number:0}}</span><br>
Negative number: <span>{{-val | number:4}}</span>
</div>
<div>
orderBy
35
El filtro orderBy nos permite ordenar un array por una propiedad, en sentido normal o
inverso. En nuestro código HTML se usa de la siguiente forma:
33
https://docs.angularjs.org/api/ng/filter/number
34
http://codepen.io/alexsuch/pen/XJgMad
35
https://docs.angularjs.org/api/ng/filter/orderBy
60
AngularJS
36
Veamos un ejemplo de cómo funciona, y lo sencillo que es el uso de expression :
<script>
angular.module('orderByExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.friends =
[{name:'John', phone:'555-1212', age:10},
{name:'Mary', phone:'555-9876', age:19},
{name:'Mike', phone:'555-4321', age:21},
{name:'Adam', phone:'555-5678', age:35},
{name:'Julie', phone:'555-8765', age:29}];
$scope.predicate = '-age';
}]);
</script>
<div ng-app="orderByExample">
<div ng-controller="ExampleController">
<pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
<hr/>
[ <a href="" ng-click="predicate=''">unsorted</a> ]
<table class="friend">
<tr>
<th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a>
(<a href="" ng-click="predicate = '-name'; reverse=false">^</
a>)</th>
<th><a href="" ng-click="predicate = 'phone'; reverse=!
reverse">Phone Number</a></th>
<th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</
a></th>
</tr>
<tr ng-repeat="friend in friends | orderBy:predicate:reverse">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
</tr>
</table>
</div>
</div>
La documentación de AngularJs dice que expression puede ser una función que podemos
37
getionar, por ejemplo, en nuestro controlador. Ejemplo :
angular.module('orderByExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.friends =
36
http://codepen.io/alexsuch/pen/XJgMzK
37
http://codepen.io/alexsuch/pen/gbRmXd
61
AngularJS
$scope.predicate = predicate;
$scope.reverse = reverse;
$scope.setPredicate = function(_predicate){
if(predicate === _predicate) {
reverse = !reverse;
} else {
predicate = _predicate;
reverse = false;
}
$scope.predicate = predicate;
$scope.reverse = reverse;
};
$scope.getPredicate = function(){
return (reverse?'-':'') + predicate;
};
}]);
<div ng-app="orderByExample">
<div ng-controller="ExampleController">
<pre>Ordenando por = {{predicate}}; reverse = {{reverse}}</pre>
<hr/>
[ <a href="" ng-click="setPredicate(null)">unsorted</a> ]
<table class="friend">
<tr>
<th><a href="" ng-click="setPredicate('name')">Name</a></th>
<th><a href="" ng-click="setPredicate('phone')">Phone Number</a></
th>
<th><a href="" ng-click="setPredicate('age')">Age</a></th>
</tr>
<tr ng-repeat="friend in friends | orderBy:getPredicate():reverse">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
</tr>
</table>
</div>
</div>
62
AngularJS
angular
.module('telephoneSample', [])
.filter('telephone', function(){
return function(input) {
var number = input || '';
number = number.trim().replace(/[-\s\(\)]/g, '');
if(number.length === 9) {
var local = [number.substr(0, 3), number.substr(3, 3),
number.substr(6, 11)].join('-');
return local
}
return number;
};
});
<div ng-app="telephoneSample">
<div>{{'965123' | telephone}}</div>
<div>{{'965123456' | telephone}}</div>
<div>{{'34965123456' | telephone}}</div>
</div>
angular
.module('textOrDefaultSample', [])
.controller('mainCtrl', function($scope){
$scope.data = {
nullValue : null,
notNullValue: 'Hello world'
};
38
http://codepen.io/alexsuch/pen/prqtn
39
http://codepen.io/alexsuch/pen/cnmlJ
63
AngularJS
})
.filter('textOrDefault', function () {
return function (input, defaultValue) {
defaultValue = defaultValue || '-';
if (!input) {
return defaultValue;
}
if (!angular.isString()) {
if (input.toString) {
input = input.toString();
} else {
return defaultValue;
}
}
if (input.trim().length > 0) {
return input;
}
return defaultValue;
};
});
Vemos que hemos hecho uso de la función angular.isString . El objeto angular nos
proporciona una serie de utilidades que pueden sernos de gran ayuda, y que podemos ver
listados en https://docs.angularjs.org/api/ng/function.
angular
.module('app', [])
.controller('mainCtrl', function($scope){
$scope.teachers = [
'Domingo',
'Otto',
'Eli',
'Miguel Ángel',
'Aitor',
'Fran',
'José Luís',
'Álex'
40
Demo en http://codepen.io/alexsuch/pen/qJCpd
64
AngularJS
];
});
El resultado de profesores filtrados, filtremos los ítems que filtremos, siempre será 8.
Una cosa que se nos podría ocurrir para corregir esto, sería volver a filtrar el resultado:
Es una opción válida, pero no óptima: recordemos que un filtro es una operación que puede
resultar bastante costosa, en función de lo que hayamos programado. Además, podemos
encadenar filtros, lo que hace que su complejidad aumente en cada filtro:
Lo ideal para este caso sería emplear el mismo array filtrado para ambos casos. Y lo podemos
41
conseguir, simplemente inicializando una variable al valor de los elementos filtrados :
Para el caso de los filtros encadenados que hemos visto antes, sería exactamente igual:
41
Demo en http://codepen.io/alexsuch/pen/olJnD
65
AngularJS
5.5. Ejercicios
Aplica el tag filters a la versión que quieres que se corrija.
Recibirá como parámetro un objeto de tipo producto, con lo que en la vista lo pasaremos de
la siguiente manera:
<p>
{{ product | productFullName }}
<br/>
{{ product.price }} €
</p>
Este filtro mostrará, concatenados, la marca y el nombre del producto. La marca deberá
mostrarse en mayúsculas, y no deberemos usar la función toUpperCase() de JavaScript,
sino inyectar el filtro upperCaseFilter en nuestro filtro y hacer uso de él.
Para ello, vamos a crear un filtro de propósito general. Éste, por tanto, no recibirá un objeto
como parámetro sino un valor. También, recibirá un parámetro adicional, que será el máximo
de puntuación del producto. En este caso la puntuación máxima son cinco estrellas, pero de
esta manera e filtro también nos valdrá para cuando puntuemos sobre diez.
<p>
{{ product | productFullName }}
<br/>
{{ product.price }} €
<br/>
{{ product.rating | rating:5 }}
</p>
66
AngularJS
<p>
{{ product | productFullName }}
<br/>
{{ product.price }} €
<br/>
<span ng-bind-html="product.rating | rating:5"></span>
</p>
.filter('rating', function($sce){
return function(rating,maxVal){
var result = '';
var solidStar = "★"; //★
var outlineStar = "☆" //★
return $sce.trustAsHtml(result);
};
})
42
http://www.edlazorvfx.com/ysu/html/ascii.html
43
https://docs.angularjs.org/api/ng/service/$sce
44
https://docs.angularjs.org/api/ng/directive/ngBindHtml
67
AngularJS
Supongamos una serie de URLS de tipo CRUD. Idealmente, tendremos una URL para una
lista de ítems, un formulario de edición, a lo mejor otro de creación, etc. Así, podrían ser:
Podríamos traducir estas URLs parciales a URLs con fragmentos para una SPA, usando el
truco del hashbang que hemos comentado:
• http://www.miaplicaciondegestion.com/#/admin/users/list
• http://www.miaplicaciondegestion.com/#/admin/users/new
45
• userId
La directiva ng-view se puede usar a modo de elemento, o bien como atributo de un elemento
div . Sin embargo, por cuestiones de compatibiliad con IE7 se recomienda su uso como
atributo.
En el siguiente ejemplo se muestran las dos variantes, habiendo dejado comentado su uso
como elemento.
45
http://www.miaplicaciondegestion.com/#/admin/users/
68
AngularJS
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/
bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body ng-app="routing">
<div class="container">
<div ng-include="'templates/common/header.html'"></div>
<div ng-view></div>
<!-- <ng-view></ng-view> -->
<div ng-include="'templates/common/footer.html'"></div>
</div>
<script src="https://code.angularjs.org/1.2.22/angular.js"></script>
<script src="https://code.angularjs.org/1.2.22/angular-route.js"></script>
<script src="app.js"></script>
</body>
</html>
ngInclude
En el fragmento de código anterior, hemos visto dos bloques que hacen uso de la directiva
ngInclude . Ésta se encarga de obtener un fragmento de HTML, compilarlo e introducirlo
en nuestra aplicación.
Es muy útil para insertar elementos parciales que se van a repetir a lo largo de nuestra
aplicación. Por ejemplo, en el bloque anterior se ha utilizado para añadir la cabecera y el pie
de la aplicación. Son dos elementos que van a estar siempre ahí, y no queremos "ensuciar"
nuestro fichero index.html con su código.
46
Aquí tenemos un enlace de una aplicación en ese estado.
Fijáos que la directiva recibe como parámetro una expresión, por eso su valor está entre
comillas simples.
69
AngularJS
Por su parte, el método otherwise suele recibir un parámetro, consistente en un objeto con
un atributo redirectTo , indicando la URL a la que redirigir cuando no se encuentra ninguna
ruta concordante.
Supongamos una aplicación de dos páginas. Consiste en una aplicación de pedidos, donde en
la primera página tenemos un listado de pedidos, y en la segunda un formulario para realizar
nuevos pedidos. La página por defecto de nuestra aplicación será el listado de pedidos.
angular
.module('ordersapp', ['ngRoute'])
.config(function($routeProvider){
$routeProvider
.when('/orders', {
templateUrl:'orders/tpl/list.html',
controller: 'OrdersCtrl'
})
.when('/orders/new', {
templateUrl: 'new-order/tpl/new.html',
controller: 'NewOrderCtrl'
})
.otherwise({ redirectTo : '/orders'});
});
Si nos vamos a una de las vistas, por ejemplo orders/tpl/list.html vemos que no se
ha introducido la directiva ng-controller al haber definido el controlador en la definición
de rutas.
<h2>Order list</h2>
<div class="row" ng-repeat="order in orders">
<div class="col col-xs-12">
<p>{{ order }}</p>
</div>
</div>
<div class="row">
<div class="col col-xs-12">
<a href="#/orders/new" class="btn btn-default">New order</a>
</div>
48
https://bitbucket.org/alejandro_such/angularjs-routing-examples/
src/22fe7b8441e74be3e2352e1c0034f0376f6014b6/?at=v2.0
70
AngularJS
</div>
/users/edit/id?=1
/users/edit/id?=2
/users/edit/id?=114
/users/edit/1
/users/edit/2
/users/edit/114
Hacer esto con el sistema de routing de AngularJS es bastante sencillo: podemos declarar
elementos variables simplemente poniendo el símbolo de los dos puntos ( : ) delante de éste.
Así, en nuestra aplicación de pedidos, introduciremos una ruta nueva para ver el detalle de
un pedido.
angular
.module('ordersapp', ['ngRoute'])
.config(function($routeProvider){
$routeProvider
.when('/orders', {
templateUrl:'orders/tpl/list.html',
controller: 'OrdersCtrl'
})
.when('/orders/new', {
templateUrl: 'new-order/tpl/new.html',
controller: 'NewOrderCtrl'
})
.when('/orders/edit/:idx', { //Ruta parametrizada
templateUrl: 'view-order/tpl/view.html',
controller: 'ViewOrderCtrl'
})
.otherwise({ redirectTo : '/orders'});
});
Para recibir los parámetros en nuestro controlador, deberemos hacer uso del servicio
$routeParams . Éste nos permite recibir todos los parámetros que hayamos pasado a la
URL.
angular
.module('ordersapp')
.controller('ViewOrderCtrl', function ($scope, OrdersService,
$routeParams){
$scope.order = OrdersService.getOrder($routeParams.idx);
71
AngularJS
});
Lo bueno del servicio $routeParams es que combina los parámetros que llegan tanto por
URL como por search parameters. Con lo cual, hubiera funcionado exactamente igual, y sin
tocar nada, de haber utilizado una url de la forma orders/edit?idx=3
49
Aquí tenemos el código completo de la aplicación de pedidos.
6.5. Redirección
En el código de ejemplo ya hay bloques que muestran cómo realizar una redirección de
una página a otra. No obstante lo mencionaremos más detenidamente, con un par de
recomendaciones.
Para referenciar una ruta variable, podemos hacerlo de igual manera, haciendo uso de las
variables de AngularJS:
Sin embargo, cuando utilizamos código dinámico (variables, funciones, etc.) en un enlace, la
manera más correcta de hacerlo es utilizando la directiva ngHref .
En algunas ocasiones al generar el enlace dinámico anterior, podría darse el caso de que el
usuario hace clic antes de que AngularJS haya tenido la oportunidad de establecer el valor
dinámico. Al usar la directiva ngHref , AngularJS no establece el valor del atributo href
hasta que ha podido interpretar el valor completo de la cadena. Así, lo que hace es traducir
esto:
a esto otro:
72
AngularJS
que hacerse desde un controlador u otro servicio. Para hacerlo desde aquí, recurriremos al
servicio $location .
50
El servicio $location+ parsea la URL de la barra de direcciones del
navegador (según los valores de +window.location ), y hace que la URL esté
accesible para nuestra aplicación. Todo cambio que se haga en la URL se verá reflejado en
el servicio $location y viceversa.
El servicio $location :
• Expone la URL actual de la barra de direcciones del navegador, para que podamos:
# Observar la URL
# Modificar la URL
• Sincroniza la URL de la barra de direcciones del navegador cuando el usuario:
50
https://docs.angularjs.org/api/ng/service/$location
73
AngularJS
Estos métodos actúan a la vez como getters y setters, en función de si tienen o no parámetros.
Así hemos podido ver cómo en el controlador NewOrderCtrl , redirigíamos a la página
principal una vez insertado un pedido mediante la orden:
$location.path('/orders');
Tendremos de igual manera dos pantallas: una de listado (ruta: /list ) y otra de detalle de
producto (ruta: /detail/:id_producto ). La ruta por defecto será la del listado.
Como vemos en los siguientes mockups, la cabecera tendrá un título genérico, o bien el
nombre del producto. Además, tendrá la cesta de la compra indicando el número de productos
y el importe total.
74
AngularJS
Hemos visto que AngularJS nos proporciona un mecanismo de routing que, pese a ser
totalmente válido, tiene ciertas limitaciones. Entre ellas, en la clase anterior hemos visto la
necesidad de incluir, en cada una de las vistas, la cabecera y el pie con directivas ngInclude .
Además, las redirecciones se hacían directamente contra la ruta de manera que, si esta
cambia, debemos ir a cada etiqueta a y cada llamada a $location.path() a modificarla.
El concepto de routing es un poco distinto, pero a la larga acaba gustando más que el de
ngRoute
• Cabecera
• Cuerpo
75
AngularJS
• Pie
<script src="https://code.angularjs.org/1.2.22/angular-route.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/
angular-ui-router.js"></script>
angular
.module('ordersapp', ['ui.router'])
Sin embargo, una de las ventajas de ui-router es que nos permite definir más de un bloque
de este tipo, por lo que vamos a definir tres de ellos:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/
bootstrap/3.2.0/css/bootstrap.min.css"/>
</head>
<body ng-app="ordersapp">
<div class="container">
<div ui-view name="header"></div>
<div ui-view name="content"></div>
<div ui-view name="footer"></div>
</div>
<script src="https://code.angularjs.org/1.2.22/angular.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/
angular-ui-router.js"></script>
<script src="app.js"></script>
<script src="components/services/OrdersService.js"></script>
<script src="orders/controllers/OrdersCtrl.js"></script>
<script src="new-order/controllers/NewOrderCtrl.js"></script>
<script src="view-order/controllers/ViewOrderCtrl.js"></script>
</body>
76
AngularJS
</html>
Al realizar este cambio, vamos a olvidarnos de tener que realizar un ng-include en cada
una de las vistas.
Al igual que teníamos una plantilla llamada header y otra llamada footer , crearemos
ahora una tercera plantilla, content.html , cuyo contenido será:
<div class="row">
<div class="col col-xs-12" ui-view>
<h4>Welcome to the orders app</h4>
</div>
</div>
Vemos que éste también tiene una directiva ui-view . En seguida veremos por qué.
Al igual que en el capítulo anterior hacíamos uso del servicio $routeProvider para la
definición de rutas, aquí utilizaremos el servicio $stateProvider , ya que hemos dicho que
consideraremos nuestro sistema de routing como una máquina de estados.
Para ello, el servicio $stateProvider dispone de un método llamado state , que recibe
como primer parámetro un nombre de estado (el que nosotros queramos), y como segundo
parámetro un objeto con los atributos:
angular
.module('ordersapp', ['ui.router'])
.config(function ($stateProvider) {
$stateProvider
.state('orders', {
url: '/orders',
views: {
header: {
templateUrl: 'components/templates/common/
header.html'
},
content: {
templateUrl: 'components/templates/common/
content.html'
},
footer: {
templateUrl: 'components/templates/common/
footer.html'
}
77
AngularJS
}
});
});
.state('orders.list', {
url: '/list',
controller: 'OrdersCtrl',
templateUrl: 'orders/tpl/list.html'
})
A priori, nos llamará la atención varias cosas. La primera de ellas, es que en nuestro estado
hemos definido la url como list , y en nuestra aplicación aparece /orders/list como
URL.
Esto se debe a que, por definición, todo estado que tenga un nombre dado, precedido por el
nombre de otro estado y un punto ( orders.list ) se considera un estado anidado (nested
state).
78
AngularJS
Un estado anidado hereda todo lo definido en el estado padre. Su URL, además, será
composición de la URL del padre, más la URL que en el estado definamos. Es por ello que la
URl es /orders/list . Una gran ventaja que aporta es que, si queremos renombrar la URL
a order (por poner un ejemplo), únicamente debemos hacerlo en un punto. Además, todo
el layout del padre se hereda, por eso no hemos tenido que definir la cabecera ni el pie.
¿Pero cómo sabe ui-router dónde colocar la vista? Muy sencillo. Si volvemos a ver el
código de la plantilla content.html veremos que ahí se había definido un objeto div con
un atributo ui-view . Éste es el punto que aprovecha ui-router para introducir la nueva
plantilla, dejando el resto intacto.
Además, como a este nivel ya disponemos sólo de un ui-view , no es necesario jugar con el
objeto views como habíammos hecho en la definición del estado anterior: podemos definir el
controller (aquí sí que necesitamos ya uno) y el templateUrl a nivel de raíz del objeto.
.state('orders.new', {
url: '/new',
templateUrl: 'new-order/tpl/new.html',
controller: 'NewOrderCtrl'
})
.state('orders.edit', {
url: '/edit/:idx',
templateUrl: 'view-order/tpl/view.html',
controller: 'ViewOrderCtrl'
})
angular
.module('ordersapp', ['ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
// DEFINICIÓN DE ESTADOS
$urlRouterProvider.otherwise('/orders/list');
});
79
AngularJS
sino a través de alguna de las clases que lo extienden. Podemos declarar un estado como
abstracto añadiéndole el atributo abstract:true :
.state('orders', {
abstract: true,
url: '/orders',
views: {
header: {
templateUrl: 'components/templates/common/header.html'
},
content: {
templateUrl: 'components/templates/common/content.html'
},
footer: {
templateUrl: 'components/templates/common/footer.html'
}
}
})
angular
.module('ordersapp')
.controller('ViewOrderCtrl', function ($scope, OrdersService,
$stateParams){
$scope.order = OrdersService.getOrder($stateParams.idx);
});
7.8. Redirección
A nivel de redirección, tendremos que hacer unos cambios mayores. En ui-router , en lugar
de la ruta, indicaremos al estado al que queremos realizar la transición. Es muy fácil querer
cambiar el nombre de una ruta. Sin embargo, los estados tienen una nomenclatura con un
significado semántico, que no querremos cambiar. Ahora, si decidimos traducir nuestras URLs
a español, sólo tendremos que hacerlo a nivel de configuración.
<div>
80
AngularJS
<h2>Order list</h2>
<div class="row" ng-repeat="order in orders">
<div class="col col-xs-12">
<p>{{ order }} <a ui-sref="orders.edit({idx:$index})">[Edit]</
a></p>
</div>
</div>
<div class="row">
<div class="col col-xs-12">
<a ui-sref="orders.new" class="btn btn-default">New order</a>
</div>
</div>
</div>
Desde un controlador
Desde un controlador, haremos uso del servicio state , que dispone del método
go(stateName) . La variación a realizar sobre el controlador NewOrderCtrl sería:
angular
.module('ordersapp')
.controller('NewOrderCtrl', function ($scope, OrdersService, $state) {
$scope.order = null;
$scope.saveOrder = function(){
OrdersService.addOrder($scope.order);
$state.go('orders.list');
};
});
En caso de querer redirigir a una ruta con parámetros, los pasaremos en un objeto JSON:
51
Aquí tenéis acceso al código de la aplicación de pedidos modificada y adaptada a ui-
router .
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.
51
https://bitbucket.org/alejandro_such/angularjs-routing-examples/
src/44799f9217ea37318564eae04194481d70eb43e0/?at=v4.0
81
AngularJS
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.
82
AngularJS
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.
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:
<h1>{{model.header.title}}</h1>
podemos usar:
<h1 ng-bind="model.header.title"></h1>
83
AngularJS
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,
52
además, la directiva ngModel . Veamos el siguiente ejemplo :
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.
84
AngularJS
<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>
53
http://codepen.io/alexsuch/pen/DkBmn
85
AngularJS
</div>
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:
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:
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:
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á:
<div>
sexo:
<label>
hombre <input type="radio" value="h" ng-model="user.sex" required />
</label>
86
AngularJS
<label>
mujer <input type="radio" value="m" ng-model="user.sex" required />
</label>
</div>
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:
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:
$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 :
Aunque esta es la forma más habitual de trabajar con un select en AngularJS, éste nos
54
permite hacerlo de muchas más maneras. La ayuda de AngularJS , nos explica cómo hacerlo
de todas las maneras posibles cuando esta fuente es un array de cadenas, o un array de
objetos.
54
https://docs.angularjs.org/api/ng/directive/select
87
AngularJS
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.
Ya hemos visto algunos, pero hay más que merece la pena conocer. Repasémoslos todos.
text
El tipo estándar que ya conocemos todos.
55
http://codepen.io/alexsuch/pen/yKbre
88
AngularJS
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.
89
AngularJS
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.
90
AngularJS
number
Nos permite introducir números y símbolos.
91
AngularJS
password
Conocido por todos, oculta los caracteres de una contraseña de la vista de curiosos.
92
AngularJS
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.
93
AngularJS
month
El type="month" es similar al date , permitiéndonos seleccionar un mes y un año.
94
AngularJS
datetime
Otro selector de fechas, esta vez más completo ya que el type="datetime" nos permite
seleccionar una fecha y una hora.
95
AngularJS
search
El input type="search" reemplaza el botón ok de nuestros teclados por un botón buscar.
96
AngularJS
97
AngularJS
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.
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.
98
AngularJS
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:
99
AngularJS
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;
}
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.
56
Por ejemplo, podemos hacer uso de la directiva `ng-class` para mostrar los elementos que
no son válidos:
<label>
56
https://docs.angularjs.org/api/ng/directive/ngClass
100
AngularJS
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 }"
/>
$scope.getCssClasses = function(ngModelCtrl){
return {
invalidelement: ngModelCtrl.$invalid && ngModelCtrl.$dirty,
validelement: ngModelCtrl.$valid && ngModelCtrl.$dirty
};
};
<label>
nombre:
<input type="text" ng-model="user.name" required ng-minlength="3" ng-
maxlength="25" name="userName" ng-class="getCssClasses(userForm.userName)"
/>
</label>
57
Para mostrar los errores de validación, haremos uso de la directiva ngShow :
<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>
101
AngularJS
todos los campos correctamente introducidos. Es por ello que haremos uso de la directiva
58
ngDisabled para deshabilitar el botón si el formulario no es válido.
Crea una nueva ruta en nuestra página del carrito, llamada /edit/:productId , donde
mostraremos un formulario donde editar nuestro producto. Tendrá los campos:
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. <<<
9. Custom directives
A lo largo de los distintos capítulos hemos visto que casi todo lo que utilizamos en nuestras
plantillas HTML es una directiva. Las directivas son los elementos que nos permiten extender
el DOM, generando componentes con el comportamiento que nosotros queramos.
Aunque AngularJS trae un conjunto de directivas muy potente en su core, en alguna ocasion
querremos crear elementos con cierta funcionalidad propia y reusable. En este capítulo
veremos cómo podemos hacerlo a través de la creación de nuevas directivas.
<div ng-app="directives1">
58
https://docs.angularjs.org/api/ng/directive/ngDisabled
102
AngularJS
<login-button></login-button>
</div>
Al introducir esta directiva en nuestro módulo, AngularJS compilará el HTML e invocará esta
directiva. El DDO de la directiva es:
angular
.module('directives1', [])
.directive('loginButton', function(){
return {
restrict : 'E',
template : '<a class="btn btn-primary btn-lg" ng-href="#/login"><span
class="glyphicon glyphicon-log-in"></span> Acceder</a>'
}
});
59
Inspeccionando el código de la aplicación , vemos que el contenido de la etiqueta se ha
reemplazado por el atributo template de nuestro DDO.
<body>
<div ng-app="directives1" class="ng-scope">
<login-button>
<a class="btn btn-primary btn-lg" ng-href="#/login" href="#/login">
<span class="glyphicon glyphicon-log-in"></span>
Acceder
</a>
</login-button>
</div>
</body>
Pero una directiva no tiene por qué ser de un tipo únicamente. Podemos definir varios tipos
60
el atributo restrict . En el siguiente ejemplo , nuestra directiva será capaz de funcionar
como atributo, elemento o clase.
restrict : 'EAC'
<login-button></login-button>
59
http://codepen.io/alexsuch/pen/nyuGp
60
http://codepen.io/alexsuch/pen/kjDFL
103
AngularJS
<div login-button></div>
<div class="login-button"></div>
Quizá sería mejor refactorizar nuestra directiva para que soporte estas posibilidades, y pasarle
estos datos como atributos de la siguiente manera:
<div login-button
login-path="#/login"
login-text="Área de usuario">
</div>
Aunque podríamos haber cogido estos datos directamente del $scope o $rootScope ,
esto puede acarrear problemas si el dato se elimina. Para solucionar esto, Angular nos
permite crear un scope hijo, o lo que se conoce como un isolate scope. Este segundo está
completamente separado del scope padre en el DOM, y se crea de una manera sencilla:
62
simplemente definiremos un atributo scope en nuestro DDO :
angular
.module('directives3', [])
.directive('loginButton', function(){
return {
restrict : 'A',
scope: {
loginPath : '@',
loginText : '@'
},
61
https://docs.angularjs.org/guide/ie
62
http://codepen.io/alexsuch/pen/cdDJA
104
AngularJS
La convención de nombrado por defectoes que el atributo y la propiedad del scope se llamen
igual. En algunas ocasiones podríamos querer que la variable del scope tuviese un nombre
distinto. Para ello especificaríamos los nombres de la siguiente manera:
scope : {
loginPath : '@uri',
loginText : '@customText'
}
y, en nuestra plantilla:
<div login-button
uri="#/login"
custom-text="Área de usuario">
</div>
Aquí, estamos diciendo que se establezca el valor de la variable loginPath del isolate scope
con lo que pasamos como atributo uri .
Ahora, imaginemos que no queremos tener la URL hardcodeada, ya que tenemos un servicio
en nuestra aplicación que nos proporciona todas las URLs de la misma. Nuestro controlador
pasa dicho servicio a la vista:
<div login-button
login-path="urls.login"
login-text="Área de usuario">
</div>
Tras este cambio, si observamos el fuente de nuestra aplicación, veremos que no tenemos el
resultado que esperábamos, y en lugar de un esperado a href="#/login" , nuestro enlace
es a href="urls.login" .
Para obtener el resultado esperado, tenemos que hacer una ligera modificación en el scope
de nuestra directiva:
scope : {
loginPath : '=',
105
AngularJS
loginText : '@'
}
Vemos que hemos cambiado la primera @ por un = . Este símbolo determina la estrategia
de binding:
• @ lee el valor de un atributo. El valor final siempre será una cadena. Al leerse tras la
evaluación del DOM, también podemos usarlo de la forma title="{{title}}" , el valor
del atributo es el que hayamos establecido en el $scope para la variable title .
• = nos permite realizar el two-way data binding en nuestra directiva, bindando la propiedad
del scope de la directiva a una propiedad del scope padre. Cuando utilicemos = ,
usaremos el nombre de la propiedad sin las llaves {{}} .
• & permite realizar referencias a funciones en el scope padre.
Es justo lo que vamos a hacer en nuestra directiva: queremos que el fragmento Área de usuario
se introduzca en una zona concreta de la plantilla.
En primer lugar, nuestra directiva tiene que permitir la transclusión. Para ello, añadir el atributo
transclusion al DDO.
{
restrict : 'A',
scope : {
loginPath : '='
},
106
AngularJS
transclude : true,
restrict
Como hemos visto, permite determinar cómo puede usarse una directiva:
• `A`tributo
• `E`lemento
• `C`lase
• Co`M`entario
scope
Lo utlizamos para crear un scope hijo ( scope : true ) o un isolate scope ( scope : {} ).
template
Define el contenido de la directiva. Puede incluir código HTML, data binding expressions y
otras directivas.
templateUrl
Al igual que en el routing, podemos definir un path para la plantilla de nuestra directiva.
Definir un templateUrl puede ser útil en componentes muy específicos para una aplicación.
Sin embargo, cuando desarrollamos componentes reutilizables, lo mejor es definir la plantilla
dentro de la directiva como un atributo template .
controller
Nos permite definir un controlador, que se asociará a la plantilla de la directiva de igual manera
que hacíamos en el routing.
angular
.module('exampleModule', [])
.directive('exampleDirective', function(){
107
AngularJS
return {
restrict : 'A',
controller : function($scope, $element, $attrs, $transclude) {
//Código de nuestro controlador
}
};
})
$scope
Hace referencia al objeto scope asociado a la directiva.
$element
Hace referencia al objeto jqLite (similiar a un objeto jQuery) de la directiva.
===== $attrs Hace referencia a los atributos del elemento. Por ejemplo para un elemento
{
id : 'myId',
class : 'blue-bordered'
}
$transclude
Esta función crea un clon del elemento a transcluir, permitiéndonos manipular el DOM.
En teoría, aunque podemos manipular el DOM desde un controlador, el lugar adecuado donde
hacerlo es en el código de una directiva.
El siguiente ejemplo crea un enlace vacío con el texto a transcluir a continuación de nuestro
elemento:
var a = angular.element('<span>');
a.text(clone.text());
$element.append(a);
});
},
transclude
Nos permite realizar la transclusión del bloque HTML que queramos, combinando su uso con
la directiva ngTransclude .
108
AngularJS
replace
Si inspeccionamos el código de nuestra aplicación, veremos que cuando introducimos una
directiva se crea un elemento padre con la definición de la directiva, y dentro de él se desarrolla
la directiva.
Cuando se establece con el valor true , reemplazamos el elemento padre por el valor de la
directiva, en lugar de introducirlo como hijo.
Es decir, pasamos de
link
El template o templateUrl no tiene utilidad hasta que se compila contra un scope .
Hemos visto que una directiva no tiene un scope por defecto, y utilizará el del padre a no
ser que se lo indiquemos.
Para hacer uso del scope, utilizaremos la función link , que recibe tres argumentos:
• scope : el scope que se pasa a la directiva, pudiendo ser propio o el del padre.
• element : un elemento jQLite (un subset de jQuery) donde se aplica nuestra directiva.
Si tenemos jQuery instalado en nuestra aplicación, será un elemento jQuery en lugar de
un lQLite.
• attrs : un objeto que contiene todos los atributos del elemento donde aplicamos nuestra
directiva, de igual manera que vimos con el controlador.
109
AngularJS
El uso principal de la función link es para asociar listeners a elementos del DOM, observar
cambios en propiedades del modelo, y validación de elementos.
require
La opción require puede ser una cadena array de cadenas, correspondoentes a nombres
de directivas. Al usarla, se asume que esas directivas indicadas en el array han sido
previamente aplicadas en el propio elemento, o en su elemento padre (si se ha marcado con
un ^ ). Se utiliza para inyectar el controlador de la directiva requerida como cuarto argumento
de la función link de nuestra directiva.
Esta cadena o conjunto de cadenas se corresponde con el nombre de las directivas cuyo
controlador queremos utilizar.
// ...
restrict : 'EA',
require : 'ngModel' // el elemento debe tener la directiva ngModel para
poder utilizar su controlador
// ...
// ...
restrict : 'EA',
require : '^ngModel' // el elemento o su padre, deben tener la directiva
ngModel
// ...
compile
Utilizaremos la función compile para realizar transformaciones en el DOM antes de que se
ejecute la función link . Esta función recibe dos elementos:
La función compile no tiene acceso al scope , y debe devolver una función link .
angular
.module('compileSkel', [])
.directive('sample', function(){
return {
compile : function(element, attrs) {
//realizar transformaciones sobre el DOM
return function(scope, element, attrs){
//función link normal y corriente
};
}
};
});
110
AngularJS
Para el ejemplo, introduciremos una directiva que valide DNIs. Como todos sabremos, la letra
del DNI es un dígito de control que se obtiene al aplicar una fórmula matemática sobre el
número. Nuestra directiva validará que la longitud del DNI y el dígito de control sean correctos.
Tendrá una forma similar a:
Internet está lleno de sitios donde encontrar la fórmula de validación del DNI. Nosotros la
63
hemos obtenido de aquí , porque también acepta NIEs.
Para este tipo de directivas, vamos a tener que hacer, sí o sí, una restricción de tipo atributo.
63
http://www.yporqueno.es/blog/javascript-validar-dni
111
AngularJS
Lo que haremos en nuestra directiva será introducir nuestro validador como primer
elemento del array de parsers, y como último elemento del array de formatters para el
ngModelController .
angular
.module('validator.nif', [])
.directive('isNif', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
// Acepta NIEs (Extranjeros con X, Y o Z al principio)
// http://www.yporqueno.es/blog/javascript-validar-dni
var validarNif = function (value) {
var numero, letraDni, letra;
var expresion_regular_dni = /^[XYZ]?\d{5,8}[A-Z]$/;
var result;
value = value.toUpperCase();
if (letra != letraDni) {
result = false;
} else {
result = true;
}
} else {
result = false;
}
ngModelCtrl.$setValidity('isNif', result);
return value;
};
ngModelCtrl.$parsers.unshift(validarNif);
ngModelCtrl.$formatters.push(validarNif);
}
}
});
Como podemos ver en la función validarNif , el validador tiene una doble responsabilidad:
• Devolver el nuevo valor del modelo. En nuestro caso estamos devolviendo siempre el
valor del modelo, pero puede haber circunstancias en que queramos devolver otra cosa.
112
AngularJS
angular
.module('org.expertojava.carrito')
.directive('product', function(){
return {
require: '', //¿requiere algo?
restrict: '', //¿qué restrict le ponemos?
scope: {
product: '' //ver qué ponemos aquí.
},
templateUrl: '' //crear plantilla
};
})
113
AngularJS
angular
.module('org.expertojava.carrito')
.directive('maxDecimals', function(){
return {
require: 'ngModel',
restrict: '', //TODO: ¿Qué ponemos aquí?
link: function(scope, element, attrs, ngModelCtrl) {
var validationFn = function(value){
var maxDecimals = attrs.maxDecimals;
var validity = true;
ngModelCtrl.$setValidity('maxDecimals', validity);
return value;
};
114
AngularJS
navigator.geolocation.getCurrentPosition(successFn, errorFn);
Otro ejemplo es el objeto XMLHttpRequest, que utilizamos para realizar peticiones AJAX.
Tiene una función de callback llamada onReadyStateChange , que se llama cuando cambia
el atributo readyState .
En nuestro día a día nos encontraremos con una infinidad de usos y ejemplos. Aunque puede
parecer sencillo de manejar, puede volverse un infierno cuando tenemos que encadenar
funciones de sincronización.
64
https://github.com/kriskowal/q
65
http://codepen.io/alexsuch/pen/xgzCn
115
AngularJS
La imagen ilustra un ejemplo de la famosa callback pyramid of doom. Aunque hay maneras
más elegantes de escribir y refactorizar el código, siempre será difícil de leer y mantener.
La promesa está asociada a un deferred object, cuyo estado será "pendiente", y no tiene
ningún resultado. Cuando invoquemos los métodos resolve() o reject() , este estado
pasará a "resuelto" o "rechazado". Además, podemos coger la promesa una vez inicializada
y definir operaciones con su resultado futuro, que se llevarán a cabo cuando se cambie a los
estados "resuelto" o "rechazado" que acabamos de mencionar.
Mientras el deferred object tiene métodos para cambiar el estado de una operación, la promesa
sólo expone métodos para operar con el resultado. Es por ello que es una buena práctica
devolver una promesa y no un deferred object.
116
AngularJS
async(
function(val){
deferred.resolve(val);
},
function(err){
deferred.reject(err);
}
);
Incluso podemos simplificar la llamada, ya que los métodos resolve y reject no precisan
de un contexto:
async(deferred.resolve(val), deferred.reject(err));
Ahora, asignar operaciones una vez haya habido éxito o error es bastante sencillo:
promise
.then(
function(data){ alert('Success! ' + data); },
function(data){ alert('Error! ' + data); }
)
Podemos asignar tantas funciones de éxito o error como queramos. En el siguiente ejemplo,
tanto las funciones asignadas antes de la llamada a async como las que se realizan después
se ejecutarán al resultado:
async(deferred.resolve, deferred.reject);
deferred.promise
.then(function(data) {
console.log('Success asignado tras invocar async()', data);
}, function(error) {
console.log('Error asignado tras invocar async()', error);
});
Hemos visto que el método then recibe dos funciones, una de éxito y una de error. Sin
embargo, podemos usar then para asignar funciones de éxito, y utilizar catch cuando se
117
AngularJS
produce un error. Además, existe una función finally , que se ejecutará siempre, se haya
resuelto correctamente o no. Gracias a finally , no tendremos que duplicar código que se
podría ejecutar tanto en la parte del éxito como la del error.
deferred.catch(function(){ console.log('Error!')});
promise.then(function(x) {
console.log(x);
});
La promesa empieza con llamando a async(8) , que resuelve con el valor 4 . Este valor
pasa por todas las funciones then secuencialmente, hasta pintar el valor 9 , ya que hace
(8 / 2 + 1) * 2 - 1 .
67
Como hemos visto antes que las funciones no necesitan contexto, podemos refactorizarlo
de la siguiente manera:
66
http://codepen.io/alexsuch/pen/dlrID
67
http://codepen.io/alexsuch/pen/jDrxu
118
AngularJS
.then(mult2)
.then(minusOne)
.then(paintValue);
El ejemplo es muy optimista y asume que todo va a ir bien. Pero si no es así, ¿dónde colocamos
nuestro catch ? Bien, en caso de operaciones encadenadas, catch y finally se colocan
en último lugar:
$q.when
Similar a $q.reject , pero devuelve un valor correctamente resuelto. Un ejemplo muy claro
de uso es cuando tenemos que pedir un dato al servidor si no lo tenemos cacheado.
function getElement(key){
if(!!$localStorage.key) {
return $q.when($localStorage.key);
} else {
return getFromServer(key);
119
AngularJS
}
}
$q.all
En algunas ocasiones podríamos querer tener una serie de elementos de manera asíncrona,
sin importarnos el orden, y ser notificados al terminar. Para ello, hacemos uso de
$q.all(promisesArray) . Devuelve una promesa que se resuelve sólo cuando todas las
promesas del array se han resuelto. Si al menos una de las promesas del array se rechaza,
también lo hará el resultado de $q.all .
allPromises.then(function(values){
var value1 = values[0];
var value2 = values[1];
var value3 = values[2];
...
var valueN = values[N+1];
console.log('end');
});
Lo normal en una aplicación web es que, tarde o temprano, haya que comunicarse con el
servidor para traernos algún tipo de dato, o bien para persistirlo. Es más, existen muchas
aplicaciones que únicamente hacen CRUD, con lo que la comunicación con el servidor se
convierte en algo esencial.
AngularJS dispone de una serie de APIs para comunicarse con cualquier backend a realizando
peticiones XMLHttpRequest (XHR), o bien peticiones JSONP a través del servicio $http .
120
AngularJS
http://en.wikipedia.org/wiki/JSONP
El servicio $http ofrece una serie de funciones que reciben como parámetros una URL y un
objeto de configuración, para generar una petición HTTP. Devuelve una promesa de resultados
con dos métodos: success y error .
Los métodos son equivalentes a los que podríamos hacer en una petición HTTP.
68
Para hacer las pruebas haremos uso de los servicios situados en JSONPlaceholder , que
permite hacer uso del servicio $http sobre sus servidores, ya que tiene habilitado el soporte
para CORS
$http.get
Realiza una petición GET , para obtener datos.
Parámetros:
69
El siguiente ejemplo pide el detalle de un usuario:
angular
68
http://jsonplaceholder.typicode.com/
69
http://codepen.io/alexsuch/pen/wsAgj
121
AngularJS
.module('httpModule', [])
.controller('MainCtrl', function($scope, $http){
$http.get(
'http://jsonplaceholder.typicode.com/posts',{ params: {id:1} }
)
.success(function(data){
$scope.resultdata = data;
})
.error(function(data){
alert('Se ha producido un error')
});
});
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que realice una
petición GET y devuelva el listado de comentarios para el post con ID 1.
POST
Realiza una petición POST , para dar de alta algún dato en el servidor.
Parámetros:
70
El siguiente ejemplo se encarga de dar de alta un usuario:
angular
.module('httpModule', [])
.controller('MainCtrl', function($scope, $http){
$http.post(
'http://jsonplaceholder.typicode.com/users',
{
"name": "Winchester McFly",
"username": "wmcfly",
"email": "[email protected]",
"address": {
"street": "Calle del atún 22",
},
70
http://codepen.io/alexsuch/pen/xCEeJ
122
AngularJS
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que realice una
petición POST realice el alta de una imagen.
PUT
Realiza una petición PUT , para actualizar algún elemento en el servidor.
Parámetros:
angular
.module('httpModule', [])
.controller('MainCtrl', function($scope, $http){
$http.put(
'http://jsonplaceholder.typicode.com/users/1',
{
"name": "Winchester McFly",
"username": "wmcfly",
"email": "[email protected]",
123
AngularJS
"address": {
"street": "Calle del atún 22",
},
"phone": "666 112233",
"website": "http://winchester-mcfly.com/"
}
)
.success(function(data){
$scope.data = data;
})
.error(function(data){
alert('Se ha producido un error')
});
});
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que realice una
petición PUT para actualizar el título del POST con id=1.
DELETE
Realiza una petición DELETE , para solicitar el borrado de algún elemento en el servidor.
Parámetros:
71
En el siguiente ejemplo , eliminaremos un usuario.
angular
.module('httpModule', [])
.controller('MainCtrl', function($scope, $http){
$http.delete(
'http://jsonplaceholder.typicode.com/users/1')
.success(function(data){
alert('Se ha eliminado el usuario con éxito')
})
.error(function(data){
alert('Se ha producido un error')
});
71
http://codepen.io/alexsuch/pen/jhfdB
124
AngularJS
});
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que contenga un
botón y, al presionarlo, realice una petición DELETE para eliminar el
POST con id=1.
JSONP
Realiza una petición JSONP .
Parámetros:
• url : URL destino. El nombre del callback debe ser, obligatoriamente, JSON_CALLBACK
• config : objeto de configuración opcional.
72
En este ejemplo haremos uso del servicio $http.jsonp para obtener los datos de un post.
angular
.module('httpModule', [])
.controller('MainCtrl', function($scope, $http){
$http.jsonp( 'http://jsonplaceholder.typicode.com/posts/1?
callback=JSON_CALLBACK')
.success(function(data){
$scope.data = data;
})
.error(function(data){
alert('Se ha producido un error')
});
});
EJERCICIO
Genera un pequeño programa, similar al del ejemplo, que contenga un
botón y, al presionarlo, realice una petición JSONP para obtener los datos
del usuario con id=1.
72
http://codepen.io/alexsuch/pen/cyavD
125
AngularJS
El servicio $http nos da la posibilidad de interactuar con este tipo de servicios de manera
sencilla. Sin embargo, disponemos de otro servicio, $resource , que nos permite hacer lo
mismo eliminando además el código redundante.
A partir de esta URL, el servicio $resource creará par nosotros una serie de métodos para
interactuar con el servicio RESTful.
El primero es obligatorio, y consiste en una URL que puede estar parametrizada. Los
parámetros irán siempre prefijados por el símbolo de los dos puntos : , de igual manera que
hacíamos con los servicios de routing.
Si alguno de los parámetros es una función, se ejecutará siempre antes de cada uso.
En caso de que en la URL parametrizada no tenga alguno de los parámetros, se pasará como
parametro de búsqueda en la URL. Ejemplo: para la URL /camera/:brand y los parámetros
{brand:'canon', filter:'EOS 1100d'} , obrendríamos la URL /camera/canon?
filter=EOS%201100D
Si el valor del parámetro va precedido por una arroba @ , entonces el valor de ese parámetro se
extraerá del objeto que pasemos cuando invoquemos una acción, como veremos más adelante
en los ejemplos.
El servicio acepta también un tercer parámetro, que veremos tras los ejemplos.
126
AngularJS
Query
Forma: User.query(params, successCallback, errorCallback)
Realiza una petición GET , y espera recibir un array de ítems en la respuesta JSON.
73
Como vemos en el siguiente ejemplo , el atributo params es opcional, así como el la función
de callback de error:
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var User = $resource(
'http://jsonplaceholder.typicode.com/users/:id',
{id:'@id'}
);
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var User = $resource(
'http://jsonplaceholder.typicode.com/users/:id',
{id:'@id'}
);
Get
Forma: User.get(params, successCallback, errorCallback)
73
http://codepen.io/alexsuch/pen/wystx
74
http://codepen.io/alexsuch/pen/oHCKD
127
AngularJS
Realiza una petición GET al servidor, y espera recibir un objeto como resultado de la respuesta
JSON.
75
Ejemplo :
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var User = $resource(
'http://jsonplaceholder.typicode.com/users/:id',
{id:'@id'}
);
En este caso, hemos pasado un objeto como primer parámetro que tiene el atributo id . Éste
reemplazará el valor en el template de la url por el valor 1 .
Save
Forma: User.save(params, payloadData, successCallback, errorCallback) .
Envía una petición POST al servido. El cuerpo de la petición será el objeto que pasemos en
el atributo payloadData .
76
Ejemplo :
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var userToSave = {
"id": 1,
"name": "Winchester McFly",
"username": "wmf",
"email": "[email protected]",
};
User.save(
userToSave,
75
http://codepen.io/alexsuch/pen/lCsmA
76
http://codepen.io/alexsuch/pen/pEcjw
128
AngularJS
function(){
$scope.message = 'usuario guardado con éxito';
},
function(){
$scope.message = 'error al guardar';
}
);
});
En este caso, hemos introducido función de callback de error, ya que la API no nos permite
realizar peticiones POST.
Delete
Formas:
77
Realiza una petición HTTP DELETE al servidor. Ejemplo :
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var User = $resource(
'http://jsonplaceholder.typicode.com/users/:id',
{id:'@id'}
);
User.delete(
{id:1},
function(){
$scope.message = 'Usuario eliminado correctamente'
},
function(){
$scope.message = 'Eror al eliminar'
}
);
});
77
http://codepen.io/alexsuch/pen/wAGpq
129
AngularJS
Pero, ¿qué pasa si me comunico con una API que usa POST para guardar ítems nuevos,
mientras espera PUT para actualizar ítems existentes? ¿Ya no es válido el servicio
$resource ?
Aunque no viene un método PUT por defecto en el servicio, sí que tenemos la posibilidad de
crearlo. Es aquí donde entra en juego el tercer parámetro que habíamos obviado hasta ahora
en la creación del servicio.
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var userToUpdate = {
"id": 1,
"name": "Winchester McFly",
"username": "wmf",
"email": "[email protected]",
};
User.update(
userToUpdate,
function(data){
$scope.message = data;
},
function(){
$scope.message = 'error al actualizar';
}
);
});
78
https://docs.angularjs.org/api/ngResource/service/$resource
79
http://codepen.io/alexsuch/pen/sgJlI
130
AngularJS
EJERCICIO
Adapta los ejemplos para conseguir aplicaciones que hagan lo mismo con
comentarios ( query , get , update ).
Sin embargo, también podemos crear instancias de la clase User , lo que expone métodos
a nivel de dicha instancia. Los métodos serán los mismos, pero prefijados por el símbolo del
dólar $ .
80
Ejemplo :
angular.module('restful', ['ngResource'])
.controller('MainCtrl', function($scope, $resource){
var data = {
"id": 1,
"name": "Winchester McFly",
"username": "wmf",
"email": "[email protected]",
};
u1.$delete(
function(res){ $scope.message1 = res; },
function(res){ $scope.message1 = 'error al eliminar'; }
);
u1.$save(
function(res){ $scope.message2 = res; },
function(res){ $scope.message2 = 'error al guardar'; }
);
u1.$update(
function(res){ $scope.message3 = res; },
function(res){ $scope.message3 = 'error al actualizar'; }
);
});
<h3>Save</h3>
80
http://codepen.io/alexsuch/pen/icwyd
131
AngularJS
<h3>Update</h3>
<pre>{{ message3 | json}}</pre>
</div>
11.3. Interceptores
El servicio $http de AngularJS nos permite registrar interceptores que se ejecutarán en cada
petición. Éstos resultan muy útiles cuando queremos realizar algún tipo de procesamiento
sobre todas, o prácticamente todas las peticiones.
Supongamos que queremos comprobar cuándo tenemos permisos para realizar una petición.
Para ello, podemos definir un interceptor que comprueba el código de estado de la respuesta
y, si es un 401 (HTTP 401 Unauthorized), relanza lanza un evento indicando que se
está realizando una operación no autorizada. Además, modificará todas las peticiones que
enviemos, añadiendo las cabeceras de autorización básica.
angular
.module('auth', [])
.factory('AuthService', ['$log', function ($log) {
var instance = {};
var authServiceLastDate = new Date();
var userData = null;
var authToken = null;
instance.getUserData = function () {
doCheck();
return userData;
};
instance.deleteUserData = function () {
userData = null;
};
132
AngularJS
instance.getToken = function () {
doCheck();
return authToken;
};
instance.deleteToken = function () {
authToken = null;
};
return instance;
}]);
.factory('AuthInterceptor', ['$rootScope', '$q', 'AuthService', function
($rootScope, $q, AuthService) {
var instance = {};
instance.request = function(config) {
config.headers = config.headers || {};
if (!!AuthService.getToken()) {
config.headers.Authorization = 'Basic ' + AuthService.getToken();
} else {
delete config.headers.Authorization;
}
return config;
};
instance.response = function(response) {
if (response.status === 401) {
AuthService.deleteUserData();
AuthService.deleteToken();
$rootScope.$emit('auth.unauthorized', []);
}
return response;
};
return instance;
}])
.config(function($httpProvider){
$httpProvider.interceptors.push('AuthInterceptor');
});
133
AngularJS
• request : estos interceptores reciben como parámetro un objeto http config . Podemos
modificar este objeto config , o bien crear uno nuevo. Se espera que esta función
devuelva un objeto config (bien sea el existente o el nuevo) o una promesa que contenga
el objeto config .
• requestError : este interceptor se llama cuando un interceptor previo lanza un error o
se resuelve con un rechazo.
• response : estos interceptores reciben como parámetro un objeto http response .
Podemos modificar este objeto response o crear uno nuevo. Se espera que esta función
devuelva un objeto response (bien sea el existente o el nuevo) o una promesa que
contenga el objeto response .
• responseError : este interceptor se llama cuando un interceptor previo lanza un error
o se resuelve con un rechazo.
134
AngularJS
Bower se instala de manera similar a como hemos hecho para Grunt CLI:
$ npm init
81
http://nodejs.org/
82
https://www.npmjs.org/
83
http://bower.io/
135
AngularJS
{
"name": "angular-automation-testing",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
$ bower init
? name: angular-automation-testing
? version: 0.0.0
? description:
? main file:
? what types of modules does this package expose?:
? keywords:
? authors: Alejandro Such Berenguer <[email protected]>
? license: MIT
? homepage:
? set currently installed components as dependencies?: Yes
? add commonly ignored files to ignore list?: Yes
? would you like to mark this package as private which prevents it from
being accidentally published to the registry?: No
136
AngularJS
name: 'angular-automation-testing',
version: '0.0.0',
authors: [
'Alejandro Such Berenguer <[email protected]>'
],
license: 'MIT',
ignore: [
'**/.*',
'node_modules',
'bower_components',
'test',
'tests'
]
}
Aunque vamos a querer introducir en nuestro sistema de control de versiones estos ficheros
de configuración, vamos a querer ignorar las dependencias descargadas por bower y npm:
Ahora las dependencias. En la parte de front-end vamos a instalar las dependencias vistas en
las sesiones de este módulo:
• AngularJS
• ui-router
{
"name": "angular-automation-testing",
"version": "0.0.0",
"authors": [
"Alejandro Such Berenguer <[email protected]>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"angular": "~1.3.2",
"angular-ui-router": "~0.2.11"
}
137
AngularJS
También podemos ver que esas dependencias han aparecido en el fichero package.json :
{
"name": "angular-automation-testing",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"angular-mocks": "^1.3.2",
"grunt": "^0.4.5",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^0.6.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-exec": "^0.4.6",
"grunt-karma": "^0.9.0",
"jquery": "^2.1.3",
"karma": "^0.12.24",
"karma-jasmine": "^0.2.3",
"karma-phantomjs-launcher": "^0.1.4",
"load-grunt-tasks": "^1.0.0"
}
}
Utilizaremos la opción --save-dev para indicar que todas estas dependendencias son
depenedencias de desarrollo, y no las utilizaremos nunca en un entorno de producción, ni son
necesarias para que la aplicación se ejecute.
138
AngularJS
84
Muy importante la librería angular-mocks . Ésta nos dará soporte para inyectar y mockear
servicios de AngularJS en nuestros tests unitarios. También extiende varios servicios del
core de AngularJS para que sean controlados de manera síncrona en nuestros tests (como
veremos, por ejemplo, a la hora de hacer mocks de servicios HTTP).
'use strict';
Como su nombre indica, la función beforeEach es llamada antes de que se ejecute cada
test dentro del describe .
84
https://docs.angularjs.org/api/ngMock
139
AngularJS
Utilizaremos esta función beforeEach para cargar el el módulo donde se encuentra nuestro
filtro.
Definimos cada uno de nuestros tests dentro de una función if . Y ahí utilizaremos
expectations, con la función expect . Ésta recibe un valor, llamado valor real, que se
encadenará con una función de matching para compararlo con el valor esperado.
85
La página de introducción a Jasmine dispone de ejemplos de todos y cada uno de los
matchers por defecto.
86
Ahora, debemos establecer la configuración de Karma , para poder lanzar el test. Para ello,
en la raíz del proyecto lanzaremos el comando karma init :
$ karma init
Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes
85
http://jasmine.github.io/edge/introduction.html
86
http://karma-runner.github.io/0.12/index.html
140
AngularJS
files: [
'bower_components/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
'bower_components/angular-ui-router/release/angular-ui-router.js',
'src/**/*.js',
'test/**/*Spec.js'
],
Si lanzamos los tests con karma start karma.conf.js , veremos cómo se levanta
el navegador PhantomJS y nos devuelve un error. Esto se debe a que el test se ha
lanzado, pero no se encuentra el módulo a testear. Crearemos el fichero src/filters/
testOrDefault.js :
(function () {
'use strict';
angular.module('filters.textordefault', [])
.filter('textOrDefault', function () {
return function (input, defaultValue) {
return input;
};
});
})();
Volviendo a lanzar los tests, veremos que ahora éstos fallan porque el filtro no está devolviendo
los valores que esperábamos.
(function () {
'use strict';
angular.module('filters.textordefault', [])
.filter('textOrDefault', function () {
return function (input, defaultValue) {
defaultValue = defaultValue || '-';
if (!input)
return defaultValue;
if (!angular.isString(input)) {
if (input.toString) {
input = input.toString();
} else {
return defaultValue;
}
}
if (input.trim().length > 0) {
return input;
}
return defaultValue;
};
});
})();
141
AngularJS
Una cosa que podemos ver es que se detectan los cambios "en caliente". A medida que
modificamos el código de nuestro filtro, si salvamos, se volverán a lanzar los tests.
Testing de servicios
Veamos ahora cómo testear un servicio de cualquier tipo (provider, factory o service). Haremos
nuestro ejemplo con un provider, ya que tiene un componente de configuración que el resto
de servicios no tiene.
La idea es crear un servicio de validaciones. Tendremos por una parte una serie de
validaciones predefinidas, y además podremos añadir los validadores custom al servicio.
'use strict';
beforeEach(module('providers.validation'));
beforeEach(function () {
// Creamos un módulo de pega, al que inyectamos el provider y
definimos una función de configuración
var fakeModule = angular
.module('test.app.config', function(){}).config(function
(expertoJeeValidationProvider) {
validationProvider = expertoJeeValidationProvider;
return true;
});
});
beforeEach(
//Inyectar los servicios en los tests
inject(function(_expertoJeeValidation_){
validationService = _expertoJeeValidation_;
})
);
142
AngularJS
expect(validationProvider).not.toBeUndefined();
expect(validationService).not.toBeUndefined();
});
expect(blankConstraint('', true)).toBe(true);
expect(blankConstraint('', false)).toBe(false);
expect(blankConstraint(undefinedVar, true)).toBe(true);
expect(blankConstraint(undefinedVar, false)).toBe(false);
expect(blankConstraint(null, true)).toBe(true);
expect(blankConstraint(null, false)).toBe(false);
expect(blankConstraint('hello', true)).toBe(true);
expect(blankConstraint('hello', false)).toBe(true);
});
expect(emailConstraint('', true)).toBe(false);
expect(emailConstraint('', false)).toBe(true);
expect(emailConstraint(undefinedVar, true)).toBe(false);
expect(emailConstraint(undefinedVar, false)).toBe(true);
expect(emailConstraint(null, true)).toBe(false);
expect(emailConstraint(null, false)).toBe(true);
expect(emailConstraint('admin', true)).toBe(false);
expect(emailConstraint('admin', false)).toBe(true);
expect(emailConstraint('admin@', true)).toBe(false);
expect(emailConstraint('admin@', false)).toBe(true);
expect(emailConstraint('admin@admin', true)).toBe(true);
expect(emailConstraint('admin@admin', false)).toBe(true);
expect(emailConstraint('admin@admin.', true)).toBe(false);
expect(emailConstraint('admin@admin.', false)).toBe(true);
expect(emailConstraint('[email protected]', true)).toBe(true);
expect(emailConstraint('[email protected]', false)).toBe(true);
});
143
AngularJS
144
AngularJS
matchesConstraint([], emailRegex);
};
expect(matchesConstraint('', emailRegex)).toBe(false);
expect(matchesConstraint('admin', emailRegex)).toBe(false);
expect(matchesConstraint('admin@', emailRegex)).toBe(false);
expect(matchesConstraint('admin@admin', emailRegex)).toBe(true);
expect(matchesConstraint('admin@admin.', emailRegex)).toBe(false);
expect(matchesConstraint('[email protected]',
emailRegex)).toBe(true);
});
expect(testFn).toThrow(throwErr);
expect(testFn2).toThrow(throwErr);
expect(testFn3).toThrow(throwErr);
expect(testFn4).toThrow(throwErr);
expect(testFn5).toThrow(throwErr);
145
AngularJS
expect(maxConstraint(1, 4)).toBe(true);
expect(maxConstraint(4, 4)).toBe(true);
expect(maxConstraint(5, 4)).toBe(false);
});
expect(testFn).toThrow(throwErr2);
expect(testFn2).toThrow(throwErr);
expect(testFn3).toThrow(throwErr);
expect(testFn5).toThrow(throwErr);
expect(maxSizeConstraint('hello', 4)).toBe(false);
expect(maxSizeConstraint('hello', 5)).toBe(true);
expect(maxSizeConstraint('hello', 6)).toBe(true);
146
AngularJS
expect(testFn).toThrow(throwErr);
expect(testFn2).toThrow(throwErr);
expect(testFn3).toThrow(throwErr);
expect(testFn4).toThrow(throwErr);
expect(testFn5).toThrow(throwErr);
expect(minConstraint(1, 4)).toBe(false);
expect(minConstraint(4, 4)).toBe(true);
expect(minConstraint(5, 4)).toBe(true);
});
expect(testFn).toThrow(throwErr2);
expect(testFn2).toThrow(throwErr);
expect(testFn3).toThrow(throwErr);
expect(testFn5).toThrow(throwErr);
expect(minSizeConstraint('hello', 4)).toBe(true);
expect(minSizeConstraint('hello', 5)).toBe(true);
expect(minSizeConstraint('hello', 6)).toBe(false);
147
AngularJS
expect(nullableConstraint('', true)).toBe(true);
expect(nullableConstraint('', false)).toBe(true);
expect(nullableConstraint(null, true)).toBe(true);
expect(nullableConstraint(null, false)).toBe(false);
expect(nullableConstraint(undefinedVar, true)).toBe(true);
expect(nullableConstraint(undefinedVar, false)).toBe(false);
});
expect(testFn).toThrow(throwErr);
expect(numericConstraint(5, true)).toBe(true);
expect(numericConstraint(5, false)).toBe(true);
expect(numericConstraint(null, true)).toBe(false);
expect(numericConstraint(null, false)).toBe(true);
expect(numericConstraint(undefinedVar, true)).toBe(false);
expect(numericConstraint(undefinedVar, false)).toBe(true);
expect(numericConstraint('5', true)).toBe(false);
expect(numericConstraint('5', false)).toBe(true);
expect(numericConstraint([], true)).toBe(false);
expect(numericConstraint([], false)).toBe(true);
expect(numericConstraint({}, true)).toBe(false);
expect(numericConstraint({}, false)).toBe(true);
});
148
AngularJS
rangeConstraint('a', '1');
};
expect(testFn).toThrow(throwErr);
expect(testFn2).toThrow(throwErr2);
expect(rangeConstraint(5, 0, 10)).toBe(true);
expect(rangeConstraint(0, 0, 10)).toBe(true);
expect(rangeConstraint(10, 0, 10)).toBe(true);
expect(rangeConstraint(-1, 0, 10)).toBe(false);
expect(rangeConstraint(11, 0, 10)).toBe(false);
});
expect(testFn).toThrow(throwErr);
expect(testFn2).toThrow(throwErr2);
expect(testFn3).toThrow(throwErr3);
expect(sizeConstraint("hello", 0, 5)).toBe(true);
expect(sizeConstraint("", 0, 5)).toBe(true);
expect(sizeConstraint("hi", 0, 5)).toBe(true);
expect(sizeConstraint("hello world", 0, 5)).toBe(false);
149
AngularJS
expect(testFn).toThrow(throwErr);
});
expect(testFn).toThrow(throwErr);
expect(testFn2).toThrow(throwErr2);
expect(testFn3).toThrow(throwErr3);
expect(urlConstraint('www.ua.es', false)).toBe(true);
expect(urlConstraint('asdf', true)).toBe(false);
expect(urlConstraint('www.ua.es', true)).toBe(false);
expect(urlConstraint('http://www.ua.es', true)).toBe(true);
});
expect(customConstraint(5, true)).toBe(true);
expect(customConstraint(4, false)).toBe(true);
expect(customConstraint(54, true)).toBe(false);
});
return true;
});
};
150
AngularJS
expect(testFn).toThrow(throwErr);
});
});
La función module se utiliza para indicar al test que deberían prepararse los servicios del
módulo indicado. El rol de este método es similar al de la directiva ng-app en una vista.
87
La función inject tiene la responsabilidad de inyectar los servicios en nuestros tests.
(function () {
'use strict';
angular
.module('providers.validation')
.provider('expertoJeeValidation', function () {
var instance = {};
/**
* Validates that a String value is not blank
* @param value
* @param blank
* @returns {boolean}
*/
instance.blank = function (value, blank) {
if (typeof value !== 'undefined' && value !== null
&& typeof value !== 'string' && !(value instanceof String)) {
throw 'Blank constraint only applies to strings';
}
if (!blank) {
return !isBlank;
}
return true;
};
/**
* Validates that a String value is a valid credit card number
* @param value
* @param creditCard
* @returns {boolean}
*/
instance.creditCard = function (value, creditCard) {
throw 'CreditCard constraint: Not implemented yet';
// return false;
};
/**
87
https://docs.angularjs.org/api/ngMock/function/angular.mock.inject
151
AngularJS
if (email) {
return emailRegex.test(value);
}
return true;
};
/**
* Validates that a value is within a range or collection of
constrained values.
* @param value
* @param array
* @returns {boolean}
*/
instance.inList = function (value, array) {
if (!(array instanceof Array)) {
throw 'InList constraint only applies to Arrays';
}
/**
* Validates that a String value matches a given regular
expression.
* @param value
* @param expr
* @returns {boolean}
*/
instance.matches = function (value, expr) {
if (typeof value !== 'string' && !
(value instanceof String)) {
throw 'Matches constraint only applies to Strings';
}
return regexp.test(value);
};
/**
* Validates that a value does not exceed the given maximum
value.
* @param value
* @param max
* @returns {boolean}
*/
152
AngularJS
/**
* Validates that a value's size does not exceed the given
maximum value.
* @param value
* @param maxSize
* @returns {boolean}
*/
instance.maxSize = function (value, maxSize) {
if (value instanceof Array || value instanceof String
|| typeof value === 'string') {
return value.length <= maxSize;
}
/**
* Validates that a value does not fall below the given
minimum value.
* @param value
* @param min
* @returns {boolean}
*/
instance.min = function (value, min) {
if (typeof value !== 'number' && !
(value instanceof Number)) {
throw 'Min constraint only applies to numbers';
}
/**
* Validates that a value's size does not fall below the given
minimum value.
* @param value
* @param minSize
* @returns {boolean}
*/
instance.minSize = function (value, minSize) {
153
AngularJS
/**
* Validates that that a property is not equal to the
specified value
* @param value
* @param otherValue
* @returns {boolean}
*/
instance.notEqual = function (value, otherValue) {
throw 'NotEqual constraint: Not implemented yet';
/**
* Allows a property to be set to null - defaults to true.
Undefined is considered null in this constraint
* @param value
* @param nullable
* @returns {boolean}
*/
instance.nullable = function (value, nullable) {
if (arguments.length !== 2) {
throw 'Constraint error. Must provide a boolean value
for nullable';
}
if (!nullable) {
return value !== null && typeof value !== 'undefined';
}
return true;
};
/**
* Ensures that the given value should be numeric
* @param value
* @param numeric
*/
instance.numeric = function (value, numeric) {
if (arguments.length !== 2) {
throw 'Numeric constraint expects two arguments';
}
154
AngularJS
if (numeric) {
return isNumeric;
}
return true;
};
/**
* Ensures that a property's value occurs within a specified
range
* @param value
* @param start
* @param end
* @returns {boolean}
*/
instance.range = function (value, start, end) {
if (arguments.length !== 3) {
throw 'Range constraint expects three arguments';
}
if (!instance.numeric(value, true) || !
instance.numeric(start, true) || !instance.numeric(end, true)) {
throw 'All three values must be numbers';
}
/**
* Restricts the size of a collection or the length of a
String.
* @param value
* @param start
* @param end
* @returns {boolean}
*/
instance.size = function (value, start, end) {
if (arguments.length !== 3) {
throw 'Size constraint expects three arguments';
}
if (!instance.numeric(start, true) || !
instance.numeric(end, true)) {
throw 'Start and end values must be numbers';
}
155
AngularJS
/**
* Constrains a property as unique at the database level
* @param value
* @param unique
* @returns {boolean}
*/
instance.unique = function (value, unique) {
throw 'Unique constraint: not implemented yet';
// return false;
};
/**
* Validates that a String value is a valid URL.
* @param value
* @param url
* @returns {boolean}
*/
instance.url = function (value, url) {
if(arguments.length !== 2) {
throw 'Url constraint: expected 2 arguments';
}
if (url) {
return urlRegex.test(value);
}
return true;
};
156
AngularJS
instance[constraintName] = fn;
};
Lanzando ahora el test nos dará error. Esto se debe a que el módulo del provider no está
correctamente definido. Lo corregiremos para que todo funcione correctamente:
angular
.module('providers.validation', [])
Así estos dos tests serían equivalentes, solo que en un caso mantendríamos el nombre del
servicio en lugar de una variable con otro nombre:
beforeEach(module('providers.validation'));
beforeEach(
//Inyectar los servicios en los tests
inject(function(_expertoJeeValidation_){
expertoJeeValidation = _expertoJeeValidation_;
})
);
157
AngularJS
beforeEach(module('providers.validation'));
beforeEach(
//Inyectar los servicios en los tests
inject(function(expertoJeeValidation){
theService = expertoJeeValidation;
})
);
Si se produjera algún error, existe una variable en el scope llamada hasError que pasaría
a tener un valor cierto. Los usuarios se guardarán en una variable del scope llamada users .
'use strict';
// Inicializamos el controlador
controller('usersCtrl', {'$scope': scope });
158
AngularJS
httpBackend.flush();
// Inicializar el controlador
controller('usersCtrl', {'$scope': scope });
A destacar que cada test se define con la función iit en lugar de it . Si en algún momento
introducimos alguna función iit el resto de funciones definidas con it serán ignoradas.
Esto es cómodo si nos queremos centrar en algún test en concreto.
159
AngularJS
(function(){
'use strict';
angular
.module('expertojee.controllers', [])
.controller('usersCtrl', function($scope, $http){
$scope.hasError = false;
$scope.users = null
$scope.getUsers = function(){
$http
.get('/users')
.success(function(users){
$scope.users = users;
$scope.hasError = false;
})
.catch(function(){
$scope.users = null
$scope.hasError = true;
});
};
});
})();
Testing de directivas
Finalmente, veremos cómo podemos realizar tests unitarios de directivas. Aunque pueda
parecer más difícil, veremos como el proceso es bastante similar a lo que hemos hecho hasta
ahora.
El truco está en que necesitaremos compilar el código HTML. Para ello utilizaremos el servicio
89
$compile . Compilar consiste en introducir una cadena HTML en el ciclo de AngularJS,
asociándole un scope.
Para testear una directiva vamos a tener que compilarla, realizar la tarea que tenemos que
realizar (si fuese necesario), y finalmente invocar al método $apply() o $digest() del
scope para que procesar los cambios.
(function () {
'use strict';
angular.module('directives.schedule', [])
.directive('scheduleEvent', function () {
return {
restrict: 'E',
scope: {
event: '=',
deleteAction: '&'
},
89
https://docs.angularjs.org/api/ng/service/$compile
160
AngularJS
scope.$on('$destroy', function(){
angular.element(element).remove();
});
}
};
});
})();
Ésta consiste en una entrada de agenda, que puede estar o no asociada a un contacto.
Mostrará un botón "Eliminar" Acepta los siguientes atributos:
• event: Entrada de agenda. Objeto con los atributos title y contact. contact tiene, a su vez,
los atributos firstName, middleName y lastName.
• hideContact: Ocultar el nombre del contacto.
• isToday: el evento es del día de hoy. Acepta los valores "true" o "false". _ deleteAction:
acción a realizar cuando se hace click en el botón de eliminar
Como hemos comentado, en el test habrá que compilar primero un bloque HTML. Para ello,
necesitaremos inyectar el servicio $compile antes de cada test. Como hemos dicho que
este servicio asocia una cadena HTML a un scope, también haremos uso del $rootScope ,
donde definiremos la acción a realizar cuando hagamos click en el botón delete:
scope.deleteEvent = function () {
console.log('deleting event');
};
}));
En cada uno de nuestros tests, compilaremos el código HTML que deseemos probar:
161
AngularJS
//Compilar la plantilla
element = compile(element)(scope);
scope.$apply();
Una batería de tests para esta directiva podría ser la siguiente, donde iremos probando
distintas combinaciones de atributos para ver si hace lo que queremos ( test/directive/
scheduleEventSpec.js ):
'use strict';
beforeEach(function(){
module('directives.schedule')
});
scope.deleteEvent = function () {
console.log('deleting event');
};
}));
162
AngularJS
scope.$apply();
var i = element.find('i');
expect(i.hasClass('ion-ios7-time-outline')).toBe(true);
});
expect(element.find('p')[0]).not.toBeUndefined();
expect(element.text()).toContain('Juan Perez Perez');
expect(element.text()).toContain('Entrega ejercicios sesión 1');
});
163
AngularJS
expect(element.find('p')[0]).toBeUndefined();
expect(element.text()).toContain('Entrega ejercicios sesión 1');
});
expect(element.find('p')[0]).toBeUndefined();
expect(element.text()).toContain('Entrega ejercicios sesión 1');
});
scope.event = {
date: (new Date()).getTime(),
title: 'Entrega ejercicios sesión 1',
contact: {
name: 'Juan',
middleName: 'Perez',
lastName: 'Perez'
}
};
Cabe destacar el último test, donde utilizamos un spy, una funcionalidad de Jasmine que nos
permite determinar si una función en concreto ha sido llamada.
164
AngularJS
grunt.initConfig({});
grunt.registerTask('default', []);
}
Verificación de código
Dado que JavaScript es un lenguaje tan permisivo, siempre es importante establecer unas
90
convenciones de código. Es ahí donde entra JSHint , una herramienta open source que
detecta errores y problemas potenciales en nuestro código JavaScript, y establece una serie
de convenciones. Es muy restrictivo, y podemos relajarlo en base a nuestras necesidades y
las de nuestro proyecto.
grunt.initConfig({
'jshint' : {
options: {
curly: true,
eqeqeq: true,
eqnull: true,
browser: true,
globals: {
jQuery: true
},
},
default : ['src/**/*.js']
}
});
grunt.registerTask('default', ['jshint']);
}
90
http://jshint.com/
165
AngularJS
Si ahora lanzamos el comando grunt en nuestra terminal, se ejecutará la tarea default, que
realiza la validación de todos los ficheros con extensión .js en alguna de las subcarpetas
de src .
Testing
Una vez verificado el código, haremos que los tests se lancen automáticamente con karma.
Para ello, añadiremos karma a nuestro workflow:
grunt.initConfig({
'jshint' : {
//...
},
'karma' : {
'default' : {
'configFile' : 'karma.conf.js',
'options': {
singleRun: true
}
}
}
});
Como véis, hemos sobreescrito la opción singleRun para asegurarnos de que no se queda
a la espera de cambios para volver a lanzar la batería de tests.
166
AngularJS
Como esto no lo realizaremos siempre, registraremos una tarea, que llamaremos dist , que
realizará esta labor. Es muy importante que los tests pasen correctamente antes de generar
un fichero de distribución, con lo que repetiremos las vistas anteriormente.
grunt.initConfig({
'pkg': grunt.file.readJSON('package.json'),
'jshint' : {
// ...
},
'karma' : {
// ...
},
'concat': {
'dist' : {
'src' : ['src/**/*.js'],
'dest': 'dist/<%=pkg.name%>-<%=pkg.version%>.js'
}
},
'uglify': {
'options': {
'mangle':false
},
'dist':{
'files': {
'dist/<%=pkg.name%>-<%=pkg.version%>.min.js' : ['dist/<
%=pkg.name%>-<%=pkg.version%>.js']
}
}
}
});
Lanzando el comando grunt dist , veremos que se ejecuta todo, y finalmente se habrá
creado una carpeta dist con dos nuevos ficheros:
.
├── Gruntfile.js
├── bower_components
├── dist
│ ├── angular-automation-testing-0.0.0.js
│ └── angular-automation-testing-0.0.0.min.js
├── karma.conf.js
├── node_modules
├── src
167
AngularJS
│ ├── controller
│ ├── directive
│ ├── filters
│ └── providers
└── test
├── controller
├── directive
├── filters
└── providers
grunt.initConfig({
// ...
watch: {
scripts: {
files: ['src/**/*.js', 'test/**/*.js'],
tasks: ['jshint', 'karma'],
options: {
spawn: false,
},
},
},
});
Lanzando ahora el comando grunt watch , veremos que la terminal se pone en espera.
Modificando cualquier fichero, vemos cómo se registra el cambio y se lanzan los tests.
92
• grunt-angular-injector : modifica nuestro código AngularJS para ayudarnos a
solventar el problema de la minificación de código y la inyección de dependencias.
92
https://github.com/alexgorbatchev/grunt-angular-injector
168
AngularJS
93
• grunt-html2js : introduce en el servicio $templateCache todas nuestras vistas.
De esta manera, ya están cacheadas y no se tienen que pedir por AJAX.
94
• grunt-groundskeeper : elimina los console.log y debugger de nuestro código.
95
• grunt-contrib-cssmin : al igual que minificamos javascript, también podemos
hacerlo con nuestros CSS.
96
• grunt-contrib-less : compila nuestro código LESS a CSS.
97
• grunt-contrib-imagemin : optimiza nuestras imágenes para un entorno web.
98
• grunt-contrib-htmlmin : minifica nuestro código HTML.
99
• grunt-open : podría usarse, por ejemplo, para abrir el navegador una vez generado
el código.
100
• grunt-concurrent : nos permite lanzar tareas de grunt de manera concurrente.
101
• grunt-conventional-changelog : genera un changelog a partir de los metadatos
de Git.
93
https://www.npmjs.com/package/grunt-html2js
94
https://github.com/Couto/grunt-groundskeeper
95
https://www.npmjs.com/package/grunt-contrib-cssmin
96
https://www.npmjs.com/package/grunt-contrib-less
97
https://www.npmjs.com/package/grunt-contrib-imagemin
98
https://www.npmjs.com/package/grunt-contrib-htmlmin
99
https://github.com/jsoverson/grunt-open
100
https://github.com/sindresorhus/grunt-concurrent
101
https://github.com/btford/grunt-conventional-changelog
102
https://travis-ci.org/
169