Subido por psalud-sgsst

AngularJs Paso a Paso ( PDFDrive )

Anuncio
AngularJs Paso a Paso
La primera guía completa en español para adentrarse paso a paso en
el mundo de AngularJS
Maikel José Rivero Dorta
Este libro está a la venta en http://leanpub.com/angularjs-paso-a-paso
Esta versión se publicó en 2016-02-02
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook using
lightweight tools and many iterations to get reader feedback, pivot until you have the
right book and build traction once you do.
© 2014 - 2016 Maikel José Rivero Dorta
¡Twitea sobre el libro!
Por favor ayuda a Maikel José Rivero Dorta hablando sobre el libro en Twitter!
El tweet sugerido para este libro es:
”AngularJS Paso a Paso” un libro de @mriverodorta para empezar desde cero. Adquiere
tu copia en http://bit.ly/AngularJSPasoAPaso
El hashtag sugerido para este libro es #AngularJS.
Descubre lo que otra gente está diciendo sobre el libro haciendo click en este enlace
para buscar el hashtag en Twitter:
https://twitter.com/search?q=#AngularJS
Dedicado a
En primer lugar este libro esta dedicado a todos los que de alguna forma u otra me han apoyado
en llevar a cabo la realización de este libro donde plasmo mis mejores deseos de compartir mi
conocimiento.
En segundo lugar a toda la comunidad de desarrolladores de habla hispana que en múltiples
ocasiones no encuentra documentación en su idioma, ya sea como referencia o para aprender
nuevas tecnologías.
v
Índice general
Dedicado a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
v
Agradecimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
i
Traducciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ii
Prólogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Para quien es este libro . . . . . . . . . . . . . . . . . . . . . .
Que necesitas para este libro . . . . . . . . . . . . . . . . . . .
Entiéndase . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Errata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Preguntas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Recursos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. iii
. iii
. iii
. iii
. iv
. iv
. iv
. iv
Alcance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 1: Primeros pasos . . . . . . . . . . . . . . . . . . . .
Capítulo 2: Estructura . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 3: Módulos . . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 4: Servicios . . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 5: Peticiones al servidor . . . . . . . . . . . . . . . . .
Capítulo 6: Directivas . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 7: Filtros . . . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 8: Rutas . . . . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 9: Eventos . . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 10: Recursos . . . . . . . . . . . . . . . . . . . . . . .
Capítulo 11: Formularios y Validación . . . . . . . . . . . . . .
Extra: Servidor API RESTful . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
vi
vi
vi
vi
vi
vii
vii
vii
vii
vii
vii
viii
viii
Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ix
Segunda Edición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
x
Entorno de desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Seleccionando el editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
ÍNDICE GENERAL
Preparando el servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gestionando dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
4
AngularJS y sus características . . . . . . . . . . . . . . . . .
Plantillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Estructura MVC . . . . . . . . . . . . . . . . . . . . . . . . . .
Vinculación de datos . . . . . . . . . . . . . . . . . . . . . . . .
Directivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Inyección de dependencia . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
6
6
7
7
7
Capítulo 1: Primeros pasos . . . . . . . . . . . . . . . . . . .
Vías para obtener AngularJS . . . . . . . . . . . . . . . . . . .
Incluyendo AngularJS en la aplicación . . . . . . . . . . . . . .
Atributos HTML5 . . . . . . . . . . . . . . . . . . . . . . . . .
La aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Tomando el Control . . . . . . . . . . . . . . . . . . . . . . . .
Bindings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Bind Once Bindings . . . . . . . . . . . . . . . . . . . . . . . .
Observadores . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Observadores para grupos . . . . . . . . . . . . . . . . . . . . .
Controladores como objetos . . . . . . . . . . . . . . . . . . . .
Controladores Globales . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
9
10
10
13
17
18
19
20
22
23
Capítulo 2: Estructura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Estructura de ficheros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Estructura de la aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Capítulo 3: Módulos . . . . . . . . . . . . . . . . . . . . . . . .
Creando módulos . . . . . . . . . . . . . . . . . . . . . . . . .
Minificación y Compresión . . . . . . . . . . . . . . . . . . . .
Inyectar dependencias mediante $inject . . . . . . . . . . . . .
Inyección de dependencia en modo estricto . . . . . . . . . . .
Configurando la aplicación . . . . . . . . . . . . . . . . . . . .
Método run . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 30
. 30
. 31
. 32
. 33
. 33
. 34
Capítulo 4: Servicios . . . . . . . . . . . . . . . . . . . . . . .
Factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Constant y Value . . . . . . . . . . . . . . . . . . . . . . . . . .
Decorators . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
$provide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Promesas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Varias promesas a la vez . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 35
. 36
. 40
. 42
. 44
. 45
. 45
. 46
. 50
ÍNDICE GENERAL
El constructor de las promesas . . .
Desplazamiento con $anchorScroll .
Cache . . . . . . . . . . . . . . . . .
Log . . . . . . . . . . . . . . . . . .
Manejando Excepciones . . . . . . .
Retrasando funcionalidades . . . . .
Creando repeticiones con intervalos
Anotaciones en el DOM . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
51
53
56
59
61
62
64
65
Capítulo 5: Peticiones al servidor . . . . . . . . . . . . . . .
Objeto de configuración del servicio $http . . . . . . . . . . . .
Métodos de acceso rápido . . . . . . . . . . . . . . . . . . . . .
Provider del servicio $http . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 67
. 68
. 71
. 72
Capítulo 6: Directivas . . . . . . . . . . . . . . . . . . . . . .
ng-class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-style . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-non-bindable . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-repeat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-include . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-cloak . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-href . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-src y ng-srcset . . . . . . . . . . . . . . . . . . . . . . . . .
ng-blur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-copy, ng-cut y ng-paste . . . . . . . . . . . . . . . . . . . .
ng-dblclick . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-keydown, ng-keypress y ng-keyup . . . . . . . . . . . . . .
Eventos del mouse . . . . . . . . . . . . . . . . . . . . . . . . .
ng-change . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-checked . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-disabled . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-readonly . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-selected . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-submit . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-focus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-strict-di . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ng-model-options . . . . . . . . . . . . . . . . . . . . . . . . .
Creando las directivas . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 76
. 76
. 79
. 79
. 80
. 80
. 85
. 86
. 86
. 87
. 87
. 87
. 87
. 88
. 88
. 88
. 89
. 90
. 90
. 91
. 91
. 91
. 92
. 92
. 92
. 92
Capítulo 7: Filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Currency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
ÍNDICE GENERAL
Number . . . . . . . . .
Uppercase y Lowercase
limitTo . . . . . . . . .
Date . . . . . . . . . .
OrderBy . . . . . . . .
Creando filtros . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
110
110
111
111
112
114
Capítulo 8: Rutas . . . . . . . . . . . . . . . . . . . . . . . . .
El módulo ngRoute . . . . . . . . . . . . . . . . . . . . . . . .
Definiendo las rutas con $routeProvider . . . . . . . . . . . . .
Uniendo los componentes . . . . . . . . . . . . . . . . . . . . .
Plantillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Plantillas en cache . . . . . . . . . . . . . . . . . . . . . . . . .
Precargando plantillas . . . . . . . . . . . . . . . . . . . . . . .
El servicio $route . . . . . . . . . . . . . . . . . . . . . . . . .
Cambio de parámetros en la ruta . . . . . . . . . . . . . . . . .
Eventos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
El servicio $location . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
116
116
117
121
131
133
134
136
138
141
145
Capítulo 9: Eventos . . . . . . . . . . . . . . . . . . . . . . . .
Propagando eventos hacia los scopes padres . . . . . . . . . . .
Propagando eventos hacia los scopes hijos . . . . . . . . . . . .
Escuchando eventos . . . . . . . . . . . . . . . . . . . . . . . .
Objeto Evento de Angular . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
148
148
149
150
151
Capítulo 10: Recursos . . . . . . . . . . . . . . . . . . . . . . .
Obteniendo ngResource . . . . . . . . . . . . . . . . . . . . . .
Primera petición al servidor REST . . . . . . . . . . . . . . . .
Parámetros del servicio $resource . . . . . . . . . . . . . . . . .
El objeto de respuesta . . . . . . . . . . . . . . . . . . . . . . .
Instancia de un recurso . . . . . . . . . . . . . . . . . . . . . .
Trailing Slash . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
152
152
153
155
157
163
165
Capítulo 11: Formularios y Validación . . . . . . . . . . . .
Reglas de Validación . . . . . . . . . . . . . . . . . . . . . . . .
Creando una regla de validación . . . . . . . . . . . . . . . . .
Mejoras creando reglas de validación . . . . . . . . . . . . . . .
Ejecutando validación asíncrona . . . . . . . . . . . . . . . . .
El formulario . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Estados del formulario . . . . . . . . . . . . . . . . . . . . . . .
Estilos en el formulario . . . . . . . . . . . . . . . . . . . . . .
Mostrando errores de validación . . . . . . . . . . . . . . . . .
Estado de los elementos de formulario . . . . . . . . . . . . . .
Mostrando errores con ngMessages . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
167
167
168
170
172
174
174
175
177
180
181
ÍNDICE GENERAL
Reusando mensajes de validación . . . . .
Soporte para nuevos elementos de HTML5
Validación de HTML5 . . . . . . . . . . .
Otras formas de validación . . . . . . . .
Resetear elementos de formulario . . . . .
Nombre de elementos interpolables . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
184
185
187
187
191
193
Servidor API RESTful . . . . . . . . . . . . . . . . . . . . . .
Requerimientos . . . . . . . . . . . . . . . . . . . . . . . . . .
Instalando dependencias . . . . . . . . . . . . . . . . . . . . . .
Configurando el servidor . . . . . . . . . . . . . . . . . . . . .
Iniciando el servidor . . . . . . . . . . . . . . . . . . . . . . . .
Uso del servidor . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
195
195
196
196
196
197
Agradecimientos
Quisiera agradecer a varias personas que me han ayudado en lograr este proyecto.
Primero que todo a Jasel Morera por haber revisado el libro y corregido mucho de los
errores de redacción ya que no soy escritor y en ocasiones no sé cómo expresarme y
llegar a las personas de una manera correcta. También agradecer a Anxo Carracedo por
la foto de los pasos que aparece en la portada. A Wilber Zada Rosendi @wil63r¹ por el
diseño de la portada. También a todos los demás que de una forma u otra me han ayudado
a hacer realidad esta idea de escribir para la comunidad.
¹http://twitter.com/wil63r
i
Traducciones
Si te gustaría traducir este libro a otro lenguaje, por favor escríbeme a @mriverodorta
con tus intenciones. Ofreceré el 35% de las ganancias por cada libro vendido en tu
traducción, la cual será vendida al mismo precio que el original. Además de una página
en el libro para la presentación del traductor.
Nótese que el libro ha sido escrito en formato markdown con las especificaciones de
Leanpub, las traducciones deberán seguir los mismos pasos.
ii
Prólogo
AngularJs paso a paso cubre el desarrollo de aplicaciones con el framework AngularJs.
En este libro se tratarán temas esenciales para el desarrollo de aplicaciones web del
lado del cliente. Además, trabajaremos con peticiones al servidor, consumiendo servicios
REST y haciendo que nuestro sistema funcione en tiempo real sin tener que recargar la
página de nuestro navegador.
Para quien es este libro
Está escrito para desarrolladores de aplicaciones que posean un modesto conocimiento
de Javascript, así como de HTML5 y que necesiten automatizar las tareas básicas en el
desarrollo de una aplicación web, específicamente en sistemas de una sola página, manejo
de rutas, modelos, peticiones a servidores mediante Ajax, manejo de datos en tiempo real
y otros.
Que necesitas para este libro
Para un correcto aprendizaje de este libro es necesario una serie de complementos que te
permitirán ejecutar los ejemplos y construir tu propia aplicación. Si estaremos hablando
sobre el framework AngularJS es esencial que lo tengas a tu alcance, lo mismo usando
el CDN de Google o mediante una copia en tu disco duro. También necesitarás un
navegador para ver el resultado de tu aplicación, recomiendo Google Chrome por su gran
soporte de HTML5 y sus herramientas para el desarrollo. Además de lo anteriormente
mencionado necesitarás un editor de código. Más adelante estaremos hablando sobre
algunas utilidades que harían el desarrollo más fácil pero que no son estrictamente
necesarias.
Entiéndase
Se emplearán diferentes estilos de texto, para distinguir entre los diferentes tipos de
información. Aquí hay algunos ejemplos de los estilos y explicación de su significado.
Lo ejemplos de los códigos serán mostrado de la siguiente forma:
iii
Prólogo
1
2
3
4
5
6
7
8
9
10
iv
<!DOCTYPE html>
<html lang="es" ng-app="MiApp">
<head>
<meta charset="UTF-8">
<title>Titulo</title>
</head>
<body>
<div ng-controller="MiCtrl">Hola Mundo!</div>
</body>
</html>
Feedback
El feedback de los lectores siempre es bienvenido. Me gustaría saber qué piensas acerca
de este libro que te ha gustado más y que no te ha gustado. Lo tendré presente para
próximas actualizaciones. Para enviar un feedback envía un tweet a @mriverodorta.
Errata
Este es el primer libro que escribo así que asumo que encontraran varios errores. Tú
puedes ayudarme a corregirlos enviándome un tweet con el error que has encontrado a
@mriverodorta junto con los detalles del error.
Los errores serán solucionados a medida que sean encontrados. De esta forma estarán
arreglados en próximas versiones del libro.
Preguntas
Si tienes alguna pregunta relacionada con algún aspecto del libro puedes hacerla a
@mriverodorta con tus dudas.
Recursos
AngularJS posee una gran comunidad a su alrededor además del equipo de Google
que trabaja dedicado a este framework. A continuación, mencionaré algunos de los
sitios donde puedes encontrar recursos y documentación relacionada al desarrollo con
AngularJS.
Prólogo
v
Sitios de referencia
•
•
•
•
•
•
Sitio web oficial http://www.angularjs.org²
Google+ https://plus.google.com/u/0/communities/115368820700870330756³
Proyecto en Github https://github.com/angular/angular.js⁴
Grupo de Google [email protected]
Canal en Youtube http://www.youtube.com/user/angularjs⁵
Twitter @angularjs
Extensiones
La comunidad alrededor de AngularJS ha desarrollado gran cantidad de librerías y
extensiones adicionales que agregan diferentes funcionalidades al framework y tienen
sitio en: http://ngmodules.org⁶.
IDE y Herramientas
Si eres un desarrollador web, para trabajar con AngularJS no es necesario que utilices
algo diferente de lo que ya estés acostumbrado, puedes seguir usando HTML y Javascript
como lenguajes y si estás dando tus primeros pasos en este campo podrás utilizar un
editor de texto común. Aunque te recomendaría usar un ⁷IDE que al comienzo te será de
mucha ayuda con alguna de sus funciones como el auto-completamiento de código, hasta
que tengas un mayor entendimiento de las propiedades y funciones. A continuación,
recomendare algunos:
• WebStorm: Es un potente IDE multiplataforma que podrás usar lo mismo en Mac,
Linux o Windows. Además, se le puede instalar un Plugin para el trabajo con
AngularJS que fue desarrollado por la comunidad.
• SublimeText: También multiplataforma y al igual posee un plugin para AngularJS
pero no es un IDE es sólo un editor de texto.
• Espreso: Sólo disponible en Mac enfocado para su uso en el frontend.
Navegador
Nuestra aplicación de AngularJS funciona a través de los navegadores más populares
en la actualidad (Google Chrome, Safari, Mozilla Firefox). Aunque recomiendo Google
Chrome ya que posee una extensión llamada Batarang para inspeccionar aplicaciones
AngularJS y la misma puede ser instalada desde Chrome Web Store.
²http://www.angularjs.org
³https://plus.google.com/u/0/communities/115368820700870330756
⁴https://github.com/angular/angular.js
⁵http://www.youtube.com/user/angularjs
⁶http://ngmodules.org
⁷Integrated Development Environment
Alcance
Este libro abarcará la mayoría de los temas relacionados con el framework AngularJS.
Está dirigido a aquellos desarrolladores que ya poseen conocimientos sobre el uso de
AngularJS y quisieran indagar sobre algún tema en específico. A continuación, describiré
por capítulos los temas tratados en este libro.
Capítulo 1: Primeros pasos
En este capítulo se abordarán los temas iniciales para el uso del framework, sus principales vías para obtenerlo y su inclusión en la aplicación. Además de la definición
de la aplicación, usos de las primeras directivas y sus ámbitos. La creación del primer
controlador y su vinculación con la vista y el modelo. Se explicarán los primeros pasos
para el uso del servicio $scope.
Capítulo 2: Estructura
Este capítulo se describirá la importancia de tener una aplicación organizada. La estructura de los directorios y archivos. Comentarios sobre el proyecto angular-seed
para pequeñas aplicaciones y las recomendaciones para aquellas de estructura medianas
o grandes. Además de analizar algunos de los archivos esenciales para hacer que el
mantenimiento de la aplicación sea sencillo e intuitivo.
Capítulo 3: Módulos
En este capítulo comenzaremos por aislar la aplicación del entorno global con la creación
del módulo. Veremos cómo definir los controladores dentro del módulo. También
veremos cómo Angular resuelve el problema de la minificación en la inyección de
dependencias y por último los métodos de configuración de la aplicación y el espacio
para tratar eventos de forma global con el método config() y run() del módulo.
Capítulo 4: Servicios
AngularJS dispone de una gran cantidad de servicios que hará que el desarrollo de la
aplicación sea más fácil mediante la inyección de dependencias. También comenzaremos
a definir servicios específicos para la aplicación y se detallarán cada una de las vías para
crearlos junto con sus ventajas.
vi
Alcance
vii
Capítulo 5: Peticiones al servidor
Otra de las habilidades de AngularJS es la interacción con el servidor. En este capítulo
trataremos lo relacionado con las peticiones a los servidores mediante el servicio $http.
Como hacer peticiones a recursos en un servidor remoto, tipos de peticiones y más.
Capítulo 6: Directivas
Las directivas son una parte importante de AngularJS y así lo reflejará la aplicación que
creemos con el framework. En este capítulo haremos un recorrido por las principales
directivas, con ejemplos de su uso para que sean más fáciles de asociar. Además, se
crearán directivas específicas para la aplicación.
Capítulo 7: Filtros
En este capítulo trataremos todo lo relacionado con los filtros, describiendo los que
proporciona angular en su núcleo. También crearemos filtros propios para realizar
acciones específicas de la aplicación. Además de su uso en las vistas y los controladores
y servicios.
Capítulo 8: Rutas
Una de las principales características de AngularJS es la habilidad que tiene para crear
aplicaciones de una sola página. En este capítulo estaremos tratando sobre el módulo
ngRoute, el tema del manejo de rutas sin recargar la página, los eventos que se procesan
en los cambios de rutas. Además, trataremos sobre el servicio $location.
Capítulo 9: Eventos
Realizar operaciones dependiendo de las interacciones del usuario es esencial para las
aplicaciones hoy en día. Angular permite crear eventos y dispararlos a lo largo de la
aplicación notificando todos los elementos interesados para tomar acciones. En este
capítulo veremos el proceso de la propagación de eventos hacia los $scopes padres e
hijos, así como escuchar los eventos tomando acciones cuando sea necesario.
Capítulo 10: Recursos
En la actualidad existen cada vez más servicios RESTful en internet, en este capítulo
comenzaremos a utilizar el servicio ngResource de Angular. Realizaremos peticiones a
un API REST y ejecutaremos operaciones CRUD en el servidor a través de este servicio.
Alcance
viii
Capítulo 11: Formularios y Validación
Hoy en día la utilización de los formularios en la web es masiva, por lo general todas
las aplicaciones web necesitan al menos uno de estos. En este capítulo vamos a ver
como emplear las directivas para validar formularios, así como para mostrar errores
dependiendo de la información introducida por el usuario en tiempo real.
Extra: Servidor API RESTful
En el Capítulo 10 se hace uso de una API RESTful para demostrar el uso del servicio
$resource. En este extra detallaré el proceso de instalación y uso de este servidor que a
la vez viene incluido con el libro y estará disponible con cada compra. El servidor esta
creado utilizando NodeJs, Express.js y MongoDB.
Introducción
A lo largo de los años hemos sido testigo de los avances y logros obtenidos en el desarrollo
web desde la creación de World Wide Web. Si comparamos una aplicación de aquellos
entonces con una actual notaríamos una diferencia asombrosa, eso nos da una idea
de cuan increíble somos los desarrolladores, cuantas ideas maravillosas se han hecho
realidad y en la actualidad son las que nos ayudan a obtener mejores resultados en la
creación de nuevos productos.
A medida que el tiempo avanza, las aplicaciones se hacen más complejas y se necesitan
soluciones más inteligentes para lograr un producto final de calidad. Simultáneamente
se han desarrollado nuevas herramientas que ayudan a los desarrolladores a lograr fines
en menor tiempo y con mayor eficiencia. Hoy en día las aplicaciones web tienen una gran
importancia, por la cantidad de personas que utilizan Internet para buscar información
relacionada a algún tema de interés, hacer compras, socializar, presentar su empresa o
negocio, en fin, un sin número de posibilidades que nos brinda la red de redes.
Una de las herramientas que nos ayudará mucho en el desarrollo de una aplicación web
es AngularJS, un framework desarrollado por Google, lo que nos da una idea de las bases
y el soporte del framework por la reputación de su creador. En adición goza de una
comunidad a su alrededor que da soporte a cada desarrollador con soluciones a todo
tipo de problemas.
Por estos tiempos existen una gran cantidad de frameworks que hacen un increíble
trabajo a la hora de facilitar las tareas de desarrollo. Pero AngularJS viene siendo como
el más popular diría yo, por sus componentes únicos, los cuales estaremos viendo más
adelante.
En este libro estaremos tratando el desarrollo de aplicaciones web con la ayuda de
AngularJS y veremos cómo esta obra maestra de framework nos hará la vida más fácil a
la hora de desarrollar aplicaciones web.
ix
Segunda Edición
En esta segunda edición se cubrirán los cambios y nuevas funcionalidades de la versión
1.3 de AngularJS en adelante. Esta nueva versión del framework tiene gran cantidad de
cambios en las funcionalidades ya existentes. Además, tiene algunos cambios que debes
considerar antes de cambiar de versión ya que podría poner en riesgo la cobertura de tu
aplicación con respecto a los navegadores.
En esta revisión del libro encontrarás la información necesaria para sacar un mejor
provecho de las nuevas funcionalidades. Si estas a punto de comenzar a crear una nueva
aplicación puedes hacer uso del contenido sin preocupaciones. Si ya tienes una aplicación
y deseas migrar a la nueva versión de Angular, antes de hacerlo debes estar consciente
de los problemas que podría presentar.
En esta segunda edición del libro describiré los cambios relacionados en cada capítulo
del libro donde hablare al detalle sobre las modificaciones del framework para esta nueva
versión.
En la versión 1.3 de AngularJS hay grandes mejoras en el rendimiento. Con solo cambiar
de una versión anterior a la nueva versión, sin hacer cambios en el código de la aplicación,
el rendimiento será mucho mejor. Esta versión incluye mejoras en el procesamiento y en
el manejo de operaciones con el DOM. Además, se incluyen nuevas funcionalidades con
un API más sencillo que permitirá utilizar las nuevas funcionalidades de forma más fácil
y con menos código.
Estas nuevas funcionalidades brindan más control sobre los elementos como los formularios, mensajes de validación, modelos, controladores y directivas. Todos estos cambios
están orientados a hacerte más productivo con la nueva versión del framework.
Aunque tiene muchas partes buenas podría tener algunos inconvenientes. En esta versión
AngularJS ha retirado el soporte para la versión 8 de Internet Explorer. Esto quiere decir
que si tu aplicación está enfocada para usuarios de Windows XP no sería una buena idea
hacer un cambio a esta nueva versión sin considerar la pérdida de usuarios. Aunque este
es uno de los cambios que nos hace pensar en cambiar de versión, es uno de los que ha
hecho que el rendimiento de angular se haya mejorado considerablemente además de la
reducción del código base del framework.
Otro de los cambios importantes es que el framework ha dejado el soporte de jQuery con
versiones menores a la 2.1.1, esto afecta a los desarrolladores que hacen uso del jQuery
en sus aplicaciones en sustitución a la versión jqLite.
Para finalizar con los cambios inconvenientes debemos agregar que en esta versión se
ha eliminado la posibilidad de utilizar funciones globales como controladores. Aunque
x
Segunda Edición
xi
Este último cambio no debería afectarte ya que es una mala práctica el uso de funciones
globales como controladores y deberías evitar su uso, aunque uses una versión anterior
a la 1.3 de Angular.
Entorno de desarrollo
Es esencial que para sentirnos cómodos con el desarrollo tengamos a la mano cierta
variedad de utilidades para ayudarnos a realizar las tareas de una forma más fácil y en
menor tiempo. Esto lo podemos lograr con un buen editor de texto o un IDE. No se
necesita alguno específicamente, podrás continuar utilizando el que estás acostumbrado
si ya has trabajado Javascript anteriormente.
Seleccionando el editor
Existen una gran variedad de editores e IDE en el mercado hoy en día, pero hay algunos
que debemos prestar especial atención. Me refiero a editores como Visual Studio Code
o Sublime Text 2/3 y al IDE JetBrains WebStorm, los tres son multi plataforma.
Personalmente uso Visual Studio Code para mi desarrollo de día a día, con este editor
podremos escribir código de una forma muy rápida gracias a las posibilidades que brinda
el uso de las referencias a los archivos de definición.
Visual Studio Code
Para Sublime Text existen plugins que te ayudarán a aumentar la productividad. El
primer plugin es AngularJs desarrollado por el grupo de Angular-UI, solo lo uso para el
auto completamiento de las directivas en las vistas así que en sus opciones deshabilito el
auto completamiento en el Javascript. El segundo plugin es AngularJS Snippets el cual
uso para la creación de controladores, directivas, servicios y más en el Javascript. Estos
dos plugins aumentan en gran cantidad la velocidad en que escribes código.
1
2
Entorno de desarrollo
Sublime Text
Por otra parte WebStorm es un IDE con todo tipo de funcionalidades, auto completamiento de código, inspección, debug, control de versiones, refactorización y además
también tiene un plugin para el desarrollo con AngularJS que provee algunas funcionalidades similares a los de Sublime Text.
WebStorm
Preparando el servidor
Habiendo seleccionado ya el editor o IDE que usarás para escribir código el siguiente
paso es tener listo un servidor donde poder desarrollar la aplicación. En esta ocasión
también tenemos varias opciones, si deseas trabajar online Plunker⁸ es una buena opción
y Cloud9⁹ es una opción aún más completa donde podrás sincronizar tu proyecto
⁸http://plnkr.co
⁹http://cloud9.io
Entorno de desarrollo
3
mediante git y trabajar en el pc local o en el editor online.
En caso de que quieras tener tu propio servidor local para desarrollo puedes usar NodeJs
con ExpressJS para crear una aplicación. Veamos un ejemplo.
Archivo: App/server.js
1
2
3
4
5
6
7
var express = require('express'),
app
= express();
app.use(express.static(__dirname+'/public'))
.get('*', function(req, res){
res.sendFile('/public/index.html', {root:__dirname});
}).listen(3000);
Después de tener este archivo listo ejecutamos el comando node server.js y podremos
acceder a la aplicación en la maquina local por el puerto 3000 (localhost:3000). Todas las
peticiones a la aplicación serán redirigidas a index.html que se encuentra en la carpeta
public. De esta forma podremos usar el sistema de rutas de AngularJS con facilidad.
Otra opción es usar el servidor Apache ya sea instalado en local en el pc como servidor
http o por las herramientas AMP. Para Mac MAMP, windows WAMP y linux LAMP.
Con este podremos crear un host virtual para la aplicación. En la configuración de los
sitios disponibles de apache crearemos un virtualhost como el ejemplo siguiente.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<VirtualHost *:80>
# DNS que servirá a este proyecto
ServerName miapp.dev
# La direccion de donde se encuentra la aplicacion
DocumentRoot /var/www/miapp
# Reglas para la reescritura de las direcciones
<Directory /var/www/miapp>
RewriteEngine on
# No reescribir archivos o directorios.
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Reescribir todo lo demás a index.html para usar el modo de rutas HTML5
RewriteRule ^ index.html [L]
</Directory>
</VirtualHost>
Entorno de desarrollo
4
Después de haber configurado el host virtual para la aplicación necesitamos crear el
dns local para que responda a nuestra aplicación. En Mac y Linux esto se puede lograr
en el archivo /etc/hosts y en Windows está en la carpeta dentro de la carpeta del
sistema C:Windows\system32Drivers\etc\hosts. Escribiendo la siguiente línea al final
del archivo.
1
127.0.0.1 miapp.dev
Después de haber realizado los pasos anteriores reiniciamos el servicio de apache para
que cargue las nuevas configuraciones y podremos acceder a la aplicación desde el
navegador visitando http://miapp.dev.
Gestionando dependencias
En la actualidad la comunidad desarrolla soluciones para problemas específicos cada vez
más rápido. Estas soluciones son compartidas para que otros desarrolladores puedan
hacer uso de ellas sin tener que volver a reescribir el código. Un ejemplo es jQuery,
LoDash, Twitter Bootstrap, Backbone e incluso el mismo AngularJS. Sería un poco
engorroso si para la aplicación que fuéramos a desarrollar necesitáramos un número
considerado de estas librerías y tuviéramos que buscarlas y actualizarlas de forma
manual.
Con el objetivo de resolver este problema Twitter desarrolló una herramienta llamada
bower que funciona como un gestor de dependencias y a la vez nos da la posibilidad
de compartir nuestras creaciones con la comunidad. Esta herramienta se encargará de
obtener todas las dependencias de la aplicación y mantenerlas actualizada por nosotros.
Para instalar bower necesitamos tener instalado previamente npm y NodeJs en el pc.
Ejecutando el comando npm install -g bower en la consola podremos instalar bower de
forma global en el sistema. Luego de tenerlo instalado podremos comenzar a gestionar
las dependencias de la aplicación. Lo primero que necesitamos es crear un archivo
bower.json donde definiremos el nombre de la aplicación y las dependencias. El archivo
tiene la siguiente estructura.
Entorno de desarrollo
5
Archivo: App/bower.json
1
2
3
4
5
6
{
"name": "miApp",
"dependencies": {
"angular": "~1.2.*"
}
}
De esta forma estamos diciendo a bower que nuestra aplicación se llama miApp y que
necesita angular para funcionar. Una vez más en la consola ejecutamos bower install en
la carpeta que tiene el archivo bower.json. Este creará una carpeta bower_components
donde incluirá el framework para que lo podamos usar en la aplicación.
La creación del archivo bower.json lo podemos lograr de forma interactiva. En la consola
vamos hasta el directorio de la aplicación y ejecutamos bower init. Bower nos hará una
serie de preguntas relacionadas con la aplicación y luego creará el archivo bower.json
con los datos que hemos indicado. Teniendo el archivo listo podemos proceder a instalar
dependencias de la aplicación ejecutando bower install --save angular lo que instalará
AngularJS como la vez anterior. El parámetro –save es muy importante porque es el
que escribirá la dependencia en el archivo bower.json de lo contrario AngularJS sería
instalado pero no registrado como dependencia.
Una de las principales ventajas que nos proporciona Bower es que podremos distribuir
la aplicación sin ninguna de sus dependencias. Podremos excluir la carpeta de las
dependencias sin problemas ya que en cada lugar donde se necesiten las dependencias
podremos ejecutar bower install y bower las gestionará por nosotros. Esto es muy útil a
la hora de trabajar en grupo con sistemas de control de versiones como Github ya que
en el repositorio solo estaría el archivo bower.json y las dependencias en las maquinas
locales de los desarrolladores.
Para saber más sobre el uso de Bower puedes visitar su página oficial y ver la documentación para conocer acerca de cada una de sus características.
AngularJS y sus características
Con este framework tendremos la posibilidad de escribir una aplicación de manera fácil,
que con solo leerla podríamos entender qué es lo que se quiere lograr sin esforzarnos
demasiado. Además de ser un framework que sigue el patrón MVC¹⁰ nos brinda otras
posibilidades como la vinculación de datos en dos vías y la inyección de dependencia.
Sobre estos términos estaremos tratando más adelante.
Plantillas
AngularJS nos permite crear aplicaciones de una sola página, o sea podemos cargar diferentes partes de la aplicación sin tener que recargar todo el contenido en el navegador.
Este comportamiento es acompañado por un motor de plantillas que genera contenido
dinámico con un sistema de expresiones evaluadas en tiempo real.
El mismo tiene una serie de funciones que nos ayuda a escribir plantillas de una forma
organizada y fácil de leer, además de automatizar algunas tareas como son: las iteraciones
y condiciones para mostrar contenido. Este sistema es realmente innovador y usa HTML
como lenguaje para las plantillas. Es suficientemente inteligente como para detectar
las interacciones del usuario, los eventos del navegador y los cambios en los modelos
actualizando solo lo necesario en el DOM¹¹ y mostrar el contenido al usuario.
Estructura MVC
La idea de la estructura MVC no es otra que presentar una organización en el código,
donde el manejo de los datos (Modelo) estará separado de la lógica (Controlador) de
la aplicación, y a su vez la información presentada al usuario (Vistas) se encontrará
totalmente independiente. Es un proceso bastante sencillo donde el usuario interactúa
con las vistas de la aplicación, éstas se comunican con los controladores notificando las
acciones del usuario, los controladores realizan peticiones a los modelos y estos gestionan la solicitud según la información brindada. Esta estructura provee una organización
esencial a la hora de desarrollar aplicaciones de gran escala, de lo contrario sería muy
difícil mantenerlas o extenderlas. Es importante aclarar mencionar que en esta estructura
el modelo se refiere a los diferentes tipos de servicios que creamos con Angular.
¹⁰(Model View Controller) Estructura de Modelo, Vista y Controlador introducido en los 70 y obtuvo su popularidad en el desarrollo de aplicaciones
de escritorio.
¹¹Doccument Object Model
6
AngularJS y sus características
7
Vinculación de datos
Desde que el DOM pudo ser modificado después de haberse cargado por completo,
librerías como jQuery hicieron que la web fuera más amigable. Permitiendo de esta
manera que en respuesta a las acciones del usuario el contenido de la página puede
ser modificado sin necesidad de recargar el navegador. Esta posibilidad de modificar
el DOM en cualquier momento es una de las grandes ventajas que utiliza AngularJS para
vincular datos con la vista.
Pero eso no es nuevo, jQuery ya lo hacía antes, lo innovador es, ¿Que tan bueno sería si
pudiéramos lograr vincular los datos que tenemos en nuestros modelos y controladores
sin escribir nada de código? Seria increíble verdad, pues AngularJS lo hace de una
manera espectacular. En otras palabras, nos permite definir que partes de la vista
serán sincronizadas con propiedades de Javascript de forma automática. Esto ahorra
enormemente la cantidad de código que tendríamos que escribir para mostrar los datos
del modelo a la vista, que en conjunto con la estructura MVC funciona de maravillas.
Directivas
Si vienes del dominio de jQuery esta será la parte donde te darás cuenta que el desarrollo
avanza de forma muy rápida y que seleccionar elementos para modificarlos posteriormente, como ha venido siendo su filosofía, se va quedando un poco atrás comparándolo
con el alcance de AngularJS. jQuery en si es una librería que a lo largo de los años
ha logrado que la web en general se vea muy bien con respecto a tiempos pasados. A
su vez tiene una popularidad que ha ganado con resultados demostrados y posee una
comunidad muy amplia alrededor de todo el mundo.
Uno de los complementos más fuertes de AngularJS son las directivas, éstas vienen a
remplazar lo que en nuestra web haría jQuery. Más allá de seleccionar elementos del
DOM, AngularJS nos permite extender la sintaxis de HTML. Con el uso del framework
nos daremos cuenta de una gran cantidad de atributos que no son parte de las especificaciones de HTML.
AngularJS tiene una gran cantidad de directivas que permiten que las plantillas sean
fáciles de leer y a su vez nos permite llegar a grandes resultados en unas pocas líneas.
Pero todo no termina ahí, AngularJS nos brinda la posibilidad de crear nuestras propias
directivas para extender el HTML y hacer que nuestra aplicación funcione mucho mejor.
Inyección de dependencia
AngularJS está basado en un sistema de inyección de dependencias donde nuestros
controladores piden los objetos que necesitan para trabajar a través del constructor.
AngularJS y sus características
8
Luego AngularJS los inyecta de forma tal que el controlador puede usarlo como sea
necesario. De esta forma el controlador no necesita saber cómo funciona la dependencia
ni cuáles son las acciones que realiza para entregar los resultados.
Así estamos logrando cada vez más una organización en nuestro código y logrando lo
que es una muy buena práctica: “Los controladores deben responder a un principio de
responsabilidad única”. En otras palabras, el controlador es para controlar, o sea recibe
peticiones y entregar respuestas basadas en estas peticiones, no genera el mismo las
respuestas. Si todos nuestros controladores siguen este patrón nuestra aplicación será
muy fácil de mantener incluso si su proceso de desarrollo es retomado luego de una
pausa de largo tiempo.
Si no estás familiarizado con alguno de los conceptos mencionados anteriormente o no
te han quedado claros, no te preocupes, todos serán explicados en detalle más adelante.
Te invito a que continúes ya que a mi modo de pensar la programación es más de código
y no de tantos de conceptos. Muchas dudas serán aclaradas cuando lo veas en la práctica.
Capítulo 1: Primeros pasos
En este capítulo daremos los primeros pasos para el uso de AngularJS. Debemos entender
que no es una librería que usa funciones para lograr un fin, AngularJS está pensado
para trabajar por módulos, esto le brida una excelente organización a nuestra aplicación.
Comenzaremos por lo más básico como es la inclusión de AngularJS y sus plantillas en
HTML.
Vías para obtener AngularJS
Existen varias vías para obtener el framework, mencionaré tres de ellas:
La primera forma es descargando el framework de forma manual desde su web oficial http://www.angularjs.org¹² donde tenemos varias opciones, la versión normal y la
versión comprimida. Para desarrollar te recomiendo que uses la versión normal ya que
la comprimida está pensada para aplicaciones en estado de producción además de no
mostrar la información de los errores.
La segunda vía es usar el framework directamente desde el CDN de Google. También
encontrará la versión normal y la comprimida. La diferencia de usar una copia local o
la del CDN se pone en práctica cuando la aplicación está en producción y un usuario
visita cualquier otra aplicación que use la misma versión de AngularJS de tu aplicación,
el CDN no necesitará volver a descargar el framework ya que ya el navegador lo tendrá
en cache. De esta forma tu aplicación iniciará más rápido.
En tercer lugar, es necesario tener instalado en el pc npm y Bower. Npm es el gestor de paquetes de NodeJS que se obtiene instalando Nodejs desde su sitio oficial
http://nodejs.org¹³. Bower es un gestor de paquetes para el frontend. No explicaré esta
vía ya que está fuera del alcance de este libro, pero esta opción esta explicada en varios
lugares en Internet, así que una pequeña búsqueda te llevara a obtenerlo.
Nosotros hemos descargado la versión normal desde el sitio oficial y la pondremos en
un directorio /lib/angular.js para ser usado.
Incluyendo AngularJS en la aplicación
Ya una vez descargado el framework lo incluiremos simplemente como incluimos un
archivo Javascript externo:
¹²http://www.angularjs.org
¹³http://nodejs.org
9
Capítulo 1: Primeros pasos
1
10
<script src="lib/angular.js"></script>
Si vamos a usar el CDN de Google seria de la siguiente forma:
1
2
3
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.21/angular.js">
</script>
De esta forma ya tenemos el framework listo en nuestra aplicación para comenzar a
usarlo.
Atributos HTML5
Como AngularJS tiene un gran entendimiento del HTML, nos permite usar las directivas
sin el prefijo data por ejemplo, obtendríamos el mismo resultado si escribiéramos el código data-ng-app que si escribiéramos ng-app. La diferencia está a la hora de que el código
pase por los certificadores que al ver atributos que no existen en las especificaciones de
HTML5 pues nos darían problemas.
La aplicación
Después de tener AngularJS en nuestra aplicación necesitamos decirle donde comenzar
y es donde aparecen las Directivas. La directiva ng-app define nuestra aplicación. Es un
atributo de clave=”valor” pero en casos de que no hayamos definido un módulo no será
necesario darle un valor al atributo. Más adelante hablaremos de los módulos ya que sería
el valor de este atributo, por ahora solo veremos lo más elemental.
AngularJS se ejecutará en el ámbito que le indiquemos, es decir abarcará todo el entorno
donde usemos el atributo ng-app. Si lo usamos en la declaración de HTML entonces
se extenderá por todo el documento, en caso de ser usado en alguna etiqueta como por
ejemplo en el body su alcance se verá reducido al cierre de la misma. Veamos el ejemplo.
Capítulo 1: Primeros pasos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{Respuesta}}</title>
</head>
<body ng-app>
<div class="container">
Entiendes el contenido de este libro?
<input type="checkbox" ng-model="respuesta">
<div ng-hide="respuesta">
<h2>Me esforzare más!</h2>
</div>
<div ng-show="respuesta">
<h2>Felicidades!</h2>
</div>
</div>
<script src="lib/angular.js"></script>
</body>
</html>
En este ejemplo encontramos varias directivas nuevas, pero no hay que preocuparse,
explicaremos todo a lo largo del libro. Podemos observar lo que analizábamos del ámbito
de la aplicación en el ejemplo anterior, en la línea 5 donde definimos el título de la página
hay unos {{ }}, en angular se usa para mostrar la información del modelo que declaramos
en la línea 10 con la directiva ng-model. Vamos a llamarlo variables para entenderlo
mejor, cuando definimos un modelo con ng-model creamos una variable y en el título
estamos tratando de mostrar su contenido con la notación {{ }}.
Podemos percatarnos que no tendremos el resultado esperado ya que el título está fuera
del ámbito de la aplicación, porque ha sido definida en la línea 7 que es el body. Lo que
quiere decir que todo lo que esté fuera del body no podrá hacer uso de nuestra aplicación.
Prueba mover la declaración de ng-app a la etiqueta de declaración de HTML en la línea
2 y observa que el resultado es el correcto ya que ahora el título está dentro del ámbito
de la aplicación.
Cuidado.
Sólo se puede tener una declaración de ng-app por página, sin importar que los
ámbitos estén bien definidos.
Ya has comenzado a escribir tu primera aplicación con AngularJS, a diferencia de los
clásicos Hola Mundo! esta vez hemos hecho algo diferente. Se habrán dado cuenta lo
Capítulo 1: Primeros pasos
12
sencillo que fue interactuar con el usuario y responder a los eventos del navegador,
y ni siquiera hemos escrito una línea de Javascript, interesante verdad, pues lo que
acabamos de hacer es demasiado simple para la potencia de AngularJS, veremos cosas
más interesantes a lo largo del Libro.
A continuación, se analizará las demás directivas que hemos visto en el ejemplo anterior.
Para entender el comportamiento de la directiva ng-model necesitamos saber qué son
los scopes en AngularJS. Pero lo dejaremos para último ya que en ocasiones es un poco
complicado explicarlo por ser una característica única de AngularJS y si vienes de usar
otros frameworks como Backbone o EmberJS esto resultará un poco confuso.
En el ejemplo anterior hemos hecho uso de otras dos directivas, ng-show y ng-hide
las cuales son empleadas como lo dice su nombre para mostrar y ocultar contenidos
en la vista. El funcionamiento de estas directivas es muy sencillo muestra u oculta un
elemento HTML basado en la evaluación de la expresión asignada al atributo de la
directiva. En otras palabras, evalúa a verdadero o falso la expresión para mostrar u
ocultar el contenido del elemento HTML. Hay que tener en cuenta que un valor falso se
considerara cualquiera de los siguientes resultados que sean devueltos por la expresión.
•
•
•
•
•
•
f
0
false
no
n
[]
Preste especial atención a este último porque nos será de gran utilidad a la hora de
mostrar u ocultar elementos cuando un arreglo esté vacío.
Esta directiva logra su función, pero no por arte de magia, es muy sencillo, AngularJS
tiene un amplio manejo de clases CSS las cuales vienen incluidas con el framework. Un
ejemplo es .ng-hide, que tiene la propiedad display definida como none lo que indica
a CSS ocultar el elemento que ostente esta clase, además tiene una marca !important
para que tome un valor superior a otras clases que traten de mostrar el elemento. Las
directivas que muestran y ocultan contenido aplican esta clase en caso que quieran
ocultar y la remueven en caso que quieran mostrar elementos ya ocultos.
Aquí viene una difícil, Scopes y su uso en AngularJS. Creo que sería una buena idea ir
viendo su comportamiento y su uso a lo largo del libro y no tratar de definir su concepto
ahora, ya que solo confundiría las cosas. Se explicará de forma sencilla según se vaya
utilizando. En esencia el scope es el componente que une las plantillas (Vistas) con los
controladores, creo que por ahora será suficiente con esto. En el ejemplo anterior en la
línea 10 donde utilizamos la directiva ng-model hemos hecho uso del scope para definir
una variable, la cual podemos usar como cualquier otra variable en Javascript.
Capítulo 1: Primeros pasos
13
Realmente la directiva ng-model une un elemento HTML a una propiedad del $scope
en el controlador. Si esta vez $scope tiene un $ al comienzo, no es un error de escritura,
es debido a que $scope es un servicio de AngularJS, otro de los temas que estaremos
tratando más adelante. En resumen el modelo respuesta definido en la línea 10 del ejemplo anterior estaría disponible en el controlador como $scope.respuesta y totalmente
sincronizado en tiempo real gracias a el motor de plantillas de AngularJS.
Tomando el Control
Veamos ahora un ejemplo un poco más avanzado en el cual ya estaremos usando
Javascript y definiremos el primer controlador.
Esta es la parte de la estructura MVC que maneja la lógica de nuestra aplicación.
Recibe las interacciones del usuario con nuestra aplicación, eventos del navegador, y las
transforma en resultados para mostrar a los usuarios.
Veamos el ejemplo:
1
2
3
4
5
6
7
8
9
10
11
<body ng-app>
<div class="container" ng-controller="miCtrl">
<h1>{{ mensaje }}</h1>
</div>
<script>
function miCtrl ($scope) {
$scope.mensaje = 'Mensaje desde el controlador';
}
</script>
<script src="lib/angular.js"></script>
</body>
En este ejemplo hemos usado una nueva directiva llamada ng-controller en la línea
2. Esta directiva es la encargada de definir que controlador estaremos usando para el
ámbito del elemento HTML donde es utilizada. El uso de esta etiqueta sigue el mismo
patrón de ámbitos que el de la directiva ng-app. Como has podido notar el controlador
es una simple función de Javascript que recibe un parámetro, y en su código sólo define
una propiedad mensaje dentro del parámetro.
Esta vez no es un parámetro lo que estamos recibiendo, AngularJS interpretará el código
con la inyección de dependencias, como $scope es un servicio del framework, creará
una nueva instancia del servicio y lo inyectará dentro del controlador haciéndolo así
disponible para vincular los datos con la vista. De esta forma todas las propiedades que
asignemos al objeto $scope estarán disponibles en la vista en tiempo real y completamente sincronizado. El controlador anterior hace que cuando usemos {{ mensaje }} en la
Capítulo 1: Primeros pasos
14
vista tenga el valor que habíamos definido en la propiedad con el mismo nombre del
$scope.
Habrán notado que al recargar la página primero muestra la sintaxis de {{ mensaje }} y
después muestra el contenido de la variable del controlador. Este comportamiento es
debido a que el controlador aún no ha sido cargado en el momento que se muestra esa
parte de la plantilla. Lo mismo que pasa cuando tratas de modificar el DOM y este aún no
está listo. Los que vienen de usar jQuery saben a qué me refiero, es que en el momento en
que se está tratando de mostrar la variable, aún no ha sido definida. Ahora, si movemos
los scripts hacia el principio de la aplicación no tendremos ese tipo de problemas ya que
cuando se trate de mostrar el contenido de la variable, esta vez si ya ha sido definido.
Veamos el siguiente ejemplo:
1
2
3
4
5
6
7
8
9
10
11
<body ng-app>
<script src="lib/angular.js"></script>
<script>
function miCtrl ($scope) {
$scope.mensaje = 'Mensaje desde el controlador';
}
</script>
<div class="container" ng-controller="miCtrl">
<h1>{{ mensaje }}</h1>
</div>
</body>
De esta forma el problema ya se ha resuelto, pero nos lleva a otro problema, que pasa
si tenemos grandes cantidades de código y todos están en el comienzo de la página. Les
diré que pasa, simplemente el usuario tendrá que esperar a que termine de cargar todos
los scripts para que comience a aparecer el contenido, en muchas ocasiones el usuario se
va de la página y no espera a que termine de cargar. Claro, no es lo que queremos para
nuestra aplicación, además de que es una mala práctica poner los scripts al inicio de la
página. Como jQuery resuelve este problema es usando el evento ready del Document,
en otras palabras, el estará esperando a que el DOM esté listo y después ejecutará las
acciones pertinentes.
Con AngularJS podríamos hacer lo mismo, pero esta vez usaremos algo más al estilo
de AngularJS, es una directiva: ng-bind=”expresion”. Esencialmente ng-bind hace que
AngularJS remplace el contenido del elemento HTML por el valor devuelto por la
expresión. Hace lo mismo que ** {{ }} ** pero con la diferencia de que es una directiva
y no se mostrara nada hasta que el contenido no esté listo.
Veamos el siguiente ejemplo:
Capítulo 1: Primeros pasos
1
2
3
4
5
6
7
8
9
10
11
15
<body ng-app>
<div class="container" ng-controller="miCtrl">
<h1 ng-bind="mensaje"></h1>
</div>
<script>
function miCtrl ($scope) {
$scope.mensaje = 'Mensaje desde el controlador';
}
</script>
<script src="lib/angular.js"></script>
</body>
Como podemos observar en el ejemplo anterior ya tenemos los scripts al final y no
tenemos el problema de mostrar contenido no deseado. Al comenzar a cargarse la página
se crea el elemento H1 pero sin contenido, y no es hasta que Angular tenga listo el
contenido en el controlador y vinculado al $scope que se muestra en la aplicación.
Debo destacar que con el uso de la etiqueta ng-controller estamos creando un nuevo
scope para su ámbito cada vez que es usada. Lo anterior, significa que cuando existan
tres controladores diferentes cada uno tendrá su propio scope y no será accesible a las
propiedades de uno al otro. Por otra parte, los controladores pueden estar anidados unos
dentro de otros, de esta forma también obtendrán un scope nuevo para cada uno, con la
diferencia de que el scope del controlador hijo tendrá acceso a las propiedades del padre
en caso de que no las tenga definidas en sí mismo.
Veamos el siguiente ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body ng-app>
<div class="container">
<div ng-controller="padreCtrl">
<button ng-click="logPadre()">Padre</button>
<div ng-controller="hijoCtrl">
<button ng-click="logHijo()">Hijo</button>
<div ng-controller="nietoCtrl">
<button ng-click="logNieto()">Nieto</button>
</div>
</div>
</div>
</div>
<script>
function padreCtrl ($scope) {
$scope.padre = 'Soy el padre';
$scope.logPadre = function(){
Capítulo 1: Primeros pasos
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
16
console.log($scope.padre);
}
}
function hijoCtrl ($scope) {
$scope.hijo = 'Soy el primer Hijo';
$scope.edad = 36;
$scope.logHijo = function(){
console.log($scope.hijo, $scope.edad);
}
}
function nietoCtrl ($scope) {
$scope.nieto = 'Soy el nieto';
$scope.edad = 4;
$scope.logNieto = function(){
console.log($scope.nieto, $scope.edad, $scope.hijo);
}
}
</script>
<script src="lib/angular.js"></script>
</body>
Ops, quizás se haya complicado un poco el código, pero lo describiremos a continuación.
Para comenzar veremos que hay una nueva directiva ng-click=”“. Esta directiva no
tiene nada de misterio, por si misma se explica sola, es la encargada de especificar el
comportamiento del evento Click del elemento y su valor es evaluado. En cada uno de
los botones se le ha asignado un evento Click para ejecutar una función en el controlador.
Como han podido observar también cada uno de los controladores están anidados uno
dentro de otros, el controlador nietoCtrl dentro de hijoCtrl y este a su vez dentro de
padreCtrl. Veamos el contenido de los controladores. En cada uno se definen propiedades
y una función que posteriormente es llamada por el evento Click de cada botón de la
vista. En el padreCtrl se ha definido la propiedad padre en el $scope y ésta es impresa a la
consola al ejecutarse la función logPadre.
En el hijoCtrl se ha definido la propiedad hijo y edad que igualmente serán impresas a
la consola. En el nietoCtrl se han definido las propiedades nieto y edad, de igual forma
se imprimen en la consola. Pero en esta ocasión trataremos de imprimir también la
propiedad hijo la cual no está definida en el $scope, así que AngularJS saldrá del
controlador a buscarla en el $scope del padre.
El resultado de este ejemplo se puede ver en el navegador con el uso de las herramientas
de desarrollo en su apartado consola.
Quizás te habrás preguntado si el $scope del padreCtrl tiene un scope padre. Pues la
respuesta es si el $rootScope. El cual es también un servicio que puede ser inyectado
Capítulo 1: Primeros pasos
17
en el controlador mediante la inyección de dependencias. Este rootScope es creado con
la aplicación y es único para toda ella, o sea todos los controladores tienen acceso a este
rootScope lo que quiere decir que todas las propiedades y funciones asignadas a este
scope son visibles por todos los controladores y este no se vuelve a crear hasta la página
no es recargada.
Estarás pensando que el rootScope es la vía de comunicación entre controladores. Puede
ser usado con este fin, aunque no es una buena práctica, para cosas sencillas no estaría
nada mal. Pero no es la mejor forma de comunicarse entre controladores, ya veremos de
qué forma se comunican los controladores en próximos capítulos.
Bindings
El uso del $scope para unir la vista con el controlador y tener disponibilidad de los
datos en ambos lugares es una de las principales ventajas que tiene Angular sobre otros
frameworks. Aunque no es un elemento único de Angular si es destacable que en otros
es mucho más complicado hacer este tipo de vínculo. Para ver lo sencillo que sería
recoger información introducida por el usuario, y a la vez mostrarla en algún lugar de la
aplicación completamente actualizada en tiempo real, veamos el siguiente ejemplo.
1
2
3
4
5
6
7
8
9
10
11
12
<body ng-app>
<div ng-controller="ctrl">
<p ng-bind="mensaje"></p>
<input type="text" ng-model="mensaje">
</div>
<script src="lib/angular.js"></script>
<script>
function ctrl($scope) {
$scope.mensaje = '';
}
</script>
</body>
En el ejemplo anterior podemos observar que a medida que escribimos en la caja de texto,
automáticamente se va actualizando en tiempo real en el controlador como en la vista.
Como todas las cosas esta funcionalidad viene con un costo adicional, y es que ahora
Angular estará pendiente de los cambios realizados por el usuario. Esto significa que en
cada interacción del usuario angular ejecutara un $digest para actualizar cada elemento
necesario.
En cada ocasión que necesitemos observar cambios en algún modelo, Angular colocara
un observador ($watch) para estar al tanto de algún cambio y poder actualizar la vista
Capítulo 1: Primeros pasos
18
correctamente. Esta funcionalidad es especialmente útil cuando estamos pidiendo datos
a los usuarios o esperando algún tipo de información desde un servidor remoto.
También podremos colocar nuestros propios observadores ya que $watch es uno de los
métodos del servicio $scope. Más adelante explicare como establecer observadores y
tomar acciones cuando estos se ejecuten.
El método $digest procesa todos los observadores ($watch) declarados en el $scope y sus
hijos. Debido a que algún $watch puede hacer cambios en el modelo, $digest continuará
ejecutando los observadores hasta que se deje de hacer cambios. Esto quiere decir que
es posible entrar en un bucle infinito, lo que llevaría a un error. Cuando el número
de iteraciones sobrepasa 10 este método lanzara un error ‘Maximum iteration limit
exceeded’.
En la aplicación mientras más modelos tenemos más $watch serán declarados y a la vez
más largo será el proceso de $digest. En grandes aplicaciones es importante mantener
el control de los ciclos ya que este proceso podría afectar de manera sustancial el
rendimiento de la aplicación.
Bind Once Bindings
Una de las nuevas funcionalidades de la versión 1.3 del framework es la posibilidad
de crear bind de los modelos sin necesidad de volver a actualizarlos. Es importante
mencionar que el uso de esta nueva funcionalidad debe utilizarse cuidadosamente ya
que podría traer problemas para la aplicación. Como explique anteriormente en cada
ocasión que esperamos cambios en el modelo, es registrado un nuevo $watch para ser
ejecutado en el $digest.
Con el nuevo método de hacer binding al modelo Angular simplemente imprimirá el
modelo en la vista y se olvidará que tiene que actualizarlo. Esto quiere decir que no estará
pendiente de cambios en el modelo para ejecutar el $digest. De esta forma la aplicación
podría mejorar en rendimiento drásticamente. Esto es de gran utilidad ya que muchas
de las ocasiones donde utilizamos el modelo no tienen cambios después de que se carga
la vista, y aun así Angular está observando los cambios en cada uno de ellos.
Es importante que esta funcionalidad se utilice de manera sabia en los lugres que estás
seguro que no es necesario actualizar. Por lo general esta funcionalidad tendrá mejor
utilidad en grandes aplicaciones donde el $digest ralentiza la ejecución dado la gran
cantidad de modelos y ciclos que necesita en las actualizaciones.
Para hacer “one time binding” es muy sencillo solo necesitas poner ‘::’ delante del modelo.
Vamos a verlo en una nueva versión del ejemplo anterior.
Capítulo 1: Primeros pasos
1
2
3
4
5
6
7
8
9
10
11
12
13
19
<body ng-app='app'>
<div ng-controller="ctrl">
<p ng-bind="::mensaje"></p>
<input type="text" ng-model="mensaje">
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', function($scope){
$scope.mensaje = 'Primer mensaje';
});
</script>
</body>
Al hacer cambios en la caja de texto podrás notar que en la parte superior no se actualiza
el valor. Como podrás darte cuenta esta nueva funcionalidad es muy útil. Existen otros
lugares donde podemos hacer uso de esta funcionalidad, como son dentro de la directiva
ng-repeat para transformar una colección en ‘one time binding’. Algo que destacar en
el uso con la directiva ng-repeat es que los elementos de la colección no se convertirán
en ‘one time binding’. En otro de los lugares donde podemos hacer uso es dentro de las
directivas propias que crees para tu aplicación.
Observadores
Es muy sencillo implementar nuestros propios observadores para actuar cuando se
cambia el modelo de alguno de los elementos que observamos. Primero, el servicio $scope
tiene un método $watch que es el que utilizaremos para observar cambios. Este método
recibe varios parámetros, primero es una cadena de texto especificando el modelo al que
se quiere observar. El segundo parámetro es una función que se ejecutara cada vez que el
modelo cambie, esta recibe el nuevo valor y el valor anterior. Y existe un tercer parámetro
que es utilizado para comprobar referencias de objetos, pero este no lo utilizaremos muy
a menudo.
Vamos a crear un ejemplo con una especie de validación muy sencilla a través del
uso de $watch. Crearemos un elemento input de tipo password y comprobaremos si
la contraseña tiene un mínimo de 6 caracteres. De no cumplir con esa condición se
mostrará un mensaje de error al usuario. Para empezar, crearemos el HTML necesario.
Capítulo 1: Primeros pasos
1
2
3
4
5
6
20
<div ng-controller="Controlador">
<form action="#">
Contraseña: <input type="password" ng-model="password">
<p ng-show="errorMinimo">Error: No cumple con el mínimo de caracteres (6)</p>
</form>
</div>
Con la directiva ng-model estamos vinculando el password con el $scope para poder
observarlo. No te preocupes por la directiva que se muestra a continuación ng-show ya
que esta se explicará más adelante en el libro, solo necesitas saber que será la encargada
de mostrar y ocultar el mensaje de error. Ahora necesitamos crear el controlador para
observar los cambios.
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app', [])
.controller('Controlador', function ($scope) {
$scope.errorMinimo = false;
$scope.$watch('password', function (nuevo, anterior) {
if (!nuevo) return;
if (nuevo.length < 6) {
$scope.errorMinimo = true;
} else {
$scope.errorMinimo = false;
}
})
});
En el controlador inyectamos el servicio $scope y le asignamos una variable errorMinimo
que será la encargada de definir si se muestra o no el error de validación. Acto seguido
implementamos el observador mediante el método $watch del $scope. Como primer
parámetro le pasaremos la cadena que definimos como modelo con la directiva ng-model
en el HTML. Como segundo parámetro será una funciona anónima que recibirá como
parámetros el valor nuevo y el valor anterior. Dentro comprobamos si existe un valor
nuevo, de lo contrario salimos de la función. En caso de que exista un valor nuevo
comprobamos que este tenga 6 o más caracteres, y definimos el valor de la variable
errorMinimo.
Ahora podremos ver el ejemplo en funcionamiento. Cuando comencemos a escribir en
el veremos que el error aparece mientras no tenemos un mínimo de 6 caracteres en él.
Observadores para grupos
En la versión 1.3 de Angular se añadió una nueva opción para observar grupo de
modelos. En esencia el funcionamiento es el mismo al método $watch pero en esta ocasión
Capítulo 1: Primeros pasos
21
observará un grupo de modelos y ejecutará la misma acción para cualquier cambio
en estos. El nuevo método watchGroup recibe como primer parámetro un arreglo de
cadenas de texto con el nombre de cada uno de los elementos que se quieren observar.
Como segundo parámetro una función que se ejecutara cuando cualquiera de los elementos observados tenga un cambio. Como con el método watch esta función también
recibe los valores nuevos y los anteriores, pero en esta ocasión es un arreglo con los
nuevos y otro con los antiguos. Es importante mencionar que el orden en que aparecen
los valores en el arreglo es el mismo en el que se especificaron en el primer parámetro
de watchGroup.
Para ver un ejemplo de su uso, vamos a crear algo similar al ejemplo realizado para
watch pero en esta ocasión validaremos dos elementos password y comprobaremos que
el valor de uno coincida con el otro. De no coincidir los valores, mostraremos un error
anunciando al usuario que los valores no coinciden.
Primero comenzaremos creando el HTML necesario para mostrar dos elementos password y el mensaje de error. A cada uno de los elementos le daremos un modelo con la
directiva ng-model, los cuales serán los mismos que observaremos más adelante en el
controlador.
1
2
3
4
5
6
7
<div ng-controller="Controlador">
<form action="#">
Contraseña: <input type="password" ng-model="password"><br><br>
Rectificar: <input type="password" ng-model="password2">
<p ng-hide="coincidencia">Error: Las contraseñas no coinciden</p>
</form>
</div>
Ahora crearemos el controlador para observar los cambios en el modelo. Primero inyectamos el servicio $scope y le asignamos una variable coincidencia que será la encargada
de mostrar o no el error de validación. Después observaremos el grupo de elementos
pasándole como primer parámetro al método $watchGroup, un arreglo con los nombres
de los modelos que queremos observar.
Como segundo parámetro pasaremos una función anónima que recibirá los valores
nuevos y anteriores. Dentro comprobamos que existan valores nuevos, de lo contrario
salimos de la función. En caso de que haya valores nuevos, comprobaremos el primer
valor del arreglo nuevos contra el segundo valor. Si los valores coinciden marcaremos la
coincidencia como verdadero de lo contrario pasaremos un valor falso.
Capítulo 1: Primeros pasos
1
2
3
4
5
6
7
8
9
10
11
12
22
angular.module('app', [])
.controller('Controlador', function ($scope) {
$scope.coincidencia = false;
$scope.$watchGroup(['password', 'password2'], function (nuevos, anteriores) {
if (!nuevos) return;
if (nuevos[0] === nuevos[1]) {
$scope.coincidencia = true;
} else {
$scope.coincidencia = false;
}
})
});
Ahora que el ejemplo está completo puedes ponerlo en práctica y probar escribiendo en
los dos elementos password para comprobar su funcionalidad. En versiones anteriores,
para lograr un comportamiento similar a este, era necesario observar cada uno de los
elementos de forma individual.
Controladores como objetos
Debido a la herencia del $scope cuando tratamos con controladores anidados, en ocasiones terminamos remplazando elementos por error. Esto podría traer comportamientos
no deseados e inesperados en la aplicación, en muchas ocasiones costaría un poco de
trabajo encontrar el motivo de los errores. Para solucionar este tipo de problemas y
colisiones innecesarias podemos utilizar la sintaxis controller as. De esta forma no
estaremos utilizando el objeto $scope para exponer elementos de la vista, si no que se
utilizara el controlador como un objeto.
Esta sintaxis está disponible desde la versión 1.1.5 de Angular como beta y se hizo estable
en versiones posteriores. Para utilizar los controladores por esta vía debemos exponer
los elementos como propiedades del mismo controlador utilizando la palabra this. De
esta forma cuando necesitamos utilizar algún elemento del controlador lo haremos como
mismo accedemos a una propiedad de un objeto JavaScript. Veamos un ejemplo.
Capítulo 1: Primeros pasos
1
2
3
4
5
6
7
8
9
10
11
23
<body ng-app>
<div ng-controller="ctrl as lista">
{{ lista.elementos }}
</div>
<script src="lib/angular.js"></script>
<script>
function ctrl() {
this.elementos = 'uno, dos, tres, cuatro.';
}
</script>
</body>
Como podrás observar en la línea dos del ejemplo se utiliza la directiva ng-controller
y la sintaxis controller as de la que hablamos anteriormente. A este controlador le
asignamos un nombre lista para poder utilizarlo como objeto. En la línea tres del
ejemplo interpolamos la propiedad elementos del controlador. Después en la línea ocho
exponemos la propiedad elementos del controlador con una cadena de texto. De esta
forma la vista está conectada al controlador al igual que si utilizáramos el $scope.
Utilizando este tipo de sintaxis ganamos algunas posibilidades, pero a la vez también
perdemos. Si usamos el controlador como un objeto ganamos en cuanto a la organización
del código, ya que siempre sabremos de donde proviene el elemento que estamos
accediendo. Pero a la vez perdemos la herencia ya que no estaremos accediendo a
propiedades del $scope sino del objeto que exponemos en el controlador. Orta punto a
tener en cuenta es que al no usar el $scope para unir el controlador con la vista, perderás
la posibilidad de utilizar las demás bondades que brinda el objeto $scope en sí.
Controladores Globales
Si estas utilizando una versión de Angular 1.3.X los ejemplos anteriores no te funcionarán, ya que desde esa versión en adelante esta deshabilitado el uso de controladores
como funciones globales. Aunque no es recomendado utilizar este tipo de sintaxis para
definir los controladores, esta puede ser activada nuevamente mediante la configuración
de la aplicación. Para lograrlo debemos primero definir un módulo, en el código a
continuación se definirá un módulo para poder configurar la aplicación, este contenido
estará detallado en el capítulo tres, si no lo entiendes, no te preocupes, continua y más
adelante entenderás a la perfección.
Capítulo 1: Primeros pasos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
24
<body ng-app="app">
<div class="container" ng-controller="miCtrl">
<h1>{{ mensaje }}</h1>
</div>
<script src="lib/angular-1.3.js"></script>
<script>
var app = angular.module('app',[]);
app.config(function($controllerProvider){
$controllerProvider.allowGlobals();
});
function miCtrl ($scope) {
$scope.mensaje = 'Mensaje desde el controlador';
}
</script>
</body>
En el ejemplo anterior se ha utilizado el mismo controlador del primer ejemplo, pero
en esta ocasión se ha incluido el archivo de Angular 1.3 el cual no permite utilizar
controladores como funciones globales por defecto. Pero a través de la configuración
de la aplicación podemos re activar este comportamiento gracias al método allowGlobals
del $controllerProvider. Si no entiendes el código anterior no te preocupes, te parecerá
mucho más fácil en el futuro cuando expliquemos los módulos y configuración de la
aplicación.
Habiendo definido esta configuración ya podemos continuar utilizando funciones globales como controladores. Esta forma de definir controladores es considerada una mala
práctica y puede traer problemas graves a tu aplicación debido a la colisión de nombres
entre otros.
Capítulo 2: Estructura
AngularJs no define una estructura para la aplicación, tanto en la organización de los
ficheros como en los módulos, el framework permite al desarrollador establecer una
organización donde mejor considere y más cómodo se sienta trabajando.
Estructura de ficheros.
Antes de continuar con el aprendizaje del framework, creo que es importante desde un
principio, tener la aplicación organizada ya que cuando se trata de ordenar una aplicación
después de estar algo avanzado, se tiene que parar el desarrollo y en muchas ocasiones
hay que reescribir partes del código para que encajen con la nueva estructura que se
quiere usar.
Con respecto a este tema es recomendado organizar la aplicación por carpetas temáticas.
Los mismos desarrolladores de Angular nos proveen de un proyecto base para iniciar
pequeñas aplicaciones. Este proyecto llamado angular-seed está disponible para todos
en su página de Github: https://github.com/angular/angular-seed y a continuación veremos
una breve descripción de su organización.
A lo largo del tiempo que se ha venido desarrollando este proyecto, angular-seed ha
cambiado mucho en su estructura. En este momento en que estoy escribiendo este
capítulo la estructura es la siguiente.
App
├──
│
│
│
│
├──
│
│
├──
│
│
├──
├──
├──
components
├── version
│
├── interpolate-filter.js
│
├── version-directive.js
│
├── version.js
view1
├── view1.html
├── view1.js
view2
├── view2.html
├── view2.js
app.css
app.js
index.html
25
Capítulo 2: Estructura
26
Al observar la organización de angular-seed veremos que en su app.js declaran la
aplicación y además requieren como dependencias cada una de las vistas y la directiva. Si
la aplicación tomara un tamaño considerable, la lista de vistas en los requerimientos del
módulo principal sería un poco incómoda de manejar. Dentro de cada carpeta de vista
existe un archivo para manejar todo lo relacionado con la misma. En éste crean un nuevo
módulo para la vista con toda su configuración y controladores.
Realmente es una semilla para comenzar a crear una aplicación. Un punto de partida
para tener una idea de la organización que esta puede tomar. A medida que la aplicación
vaya tomando tamaño se puede ir cambiando la estructura. Si es una pequeña aplicación
podrás usar angular-seed sin problemas. Si tu punto de partida es una aplicación
mediana o grande más adelante se explican otras opciones para organizar tu aplicación.
A continuación, hablaremos sobre una de las posibles la organización que se pueden
seguir para las medianas aplicaciones. De esta forma los grupos de trabajo podrán
localizar las porciones de código de forma fácil.
App
├──
├──
├──
├──
│
│
│
│
│
│
│
├──
css
img
index.html
js
├── app.js
├── Config
├── Controllers
├── Directives
├── Filters
├── Models
├── Services
partials
En esencia ésta es la estructura de directorios para una aplicación mediana. En caso de
que se fuera a construir una aplicación grande es recomendable dividirla por módulos,
para ello se usaría esta estructura por cada módulo:
Capítulo 2: Estructura
App
├──
├──
├──
├──
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
├──
│
│
│
css
img
index.html
js
├── app.js
├── Registro
│
├── Registro.js
│
├── Config
│
├── Controllers
│
├── Directives
│
├── Filters
│
├── Models
│
├── Services
├── Blog
│
├── Blog.js
│
├── Config
│
├── Controllers
│
├── Directives
│
├── Filters
│
├── Models
│
├── Services
├── Tienda
│
├── Tienda.js
│
├── Config
│
├── Controllers
│
├── Directives
│
├── Filters
│
├── Models
│
├── Services
├── Ayuda
│
├── Ayuda.js
│
├── Config
│
├── Controllers
│
├── Directives
│
├── Filters
│
├── Models
│
├── Services
partials
├── Registro
├── Blog
├── Tienda
27
Capítulo 2: Estructura
│
28
├── Ayuda
Como podemos observar al establecer esta estructura en nuestro proyecto, no importa
cuánto este crezca, siempre se mantendrá organizado y fácil de mantener. En este libro
utilizaremos la estructura para una aplicación mediana, está disponible en el repositorio
de Github https://github.com/mriverodorta/ang-starter y viene con ejemplos.
Al observar el contenido de la estructura nos podemos percatar de lo que significa cada
uno de sus archivos. Ahora analizaremos algunos de ellos.
En el directorio app/js es donde se guarda todo el código de nuestra aplicación, con
excepción de la página principal de entrada a la aplicación y las plantillas (partials), que
estarán a un nivel superior junto a los archivos de estilos y las imágenes. En el archivo
app.js es donde declararemos la aplicación (módulo) y definiremos sus dependencias. Si
has obtenido ya la copia de ang-starter desde https://github.com/mriverodorta/ang-starter,
veras varios archivos con una configuración inicial para la aplicación. A continuación,
describiré el objetivo de cada uno de ellos.
Estructura de la aplicación
Por lo general en términos de programación al crear una aplicación y ésta ser iniciada
en muchas ocasiones, necesitamos definir una serie de configuraciones para garantizar
un correcto funcionamiento en la aplicación en general. En muchos casos se necesita
que estén disponibles desde el mismo inicio de la aplicación, para que los componentes
internos que se cargan después puedan funcionar correctamente.
AngularJS nos permite lograr este tipo de comportamiento mediante el método run()
de los módulos. Este método es esencial para la inicialización de la aplicación. Solo
recibe una función como parámetro o un arreglo si utilizamos inyección de dependencia.
Este método se ejecutará cuando la aplicación haya cargado todos los módulos. Para
el uso de esta funcionalidad se ha dispuesto el archivo Bootstrap.js, donde podremos
definir comportamientos al inicio de la aplicación. En caso de que se necesite aislar
algún comportamiento del archivo Bootstrap.js se puede hacer perfectamente ya que
AngularJS permite la utilización del método run() del módulo tantas veces como sea
necesario.
Un ejemplo del aislamiento lo veremos en el archivo Security.js, donde haremos uso del
método run() para configurar la seguridad de la aplicación desde el inicio de la misma.
En la mayoría de las aplicaciones se necesitan el uso de las constantes. Los módulos
de AngularJS proveen un método constant() para la declaración de constantes y son
un servicio con una forma muy fácil de declarar. Este método recibe dos parámetros,
nombre y valor, donde el nombre es el que utilizaremos para inyectar la constante en
cualquier lugar que sea necesario dentro de la aplicación, el valor puede ser una cadena
Capítulo 2: Estructura
29
de texto, número, arreglo, objeto e incluso una función. Las constantes las definiremos
en el archivo Constants.js.
Como he comentado en ocasiones, AngularJS tiene una gran cantidad de servicios que
los hace disponibles mediante la inyección de dependencias. Muchos de estos servicios
pueden ser configurados antes de ser cargando el módulo, para cuando este esté listo ya
los servicios estén configurados. Para esto existe el método config() de los módulos. Este
método recibe como parámetro un arreglo o función para configurar los servicios. Como
estamos tratando con aplicaciones de una sola página, el manejo de rutas es esencial
para lograrlo. La configuración del servicio $routeProvider donde se definen las rutas
debe ser configurado con el método config() del módulo, ya que necesita estar listo para
cuando el módulo este cargado por completo. Estas rutas las podremos definir en el
archivo Routes.js del cual hablaremos más adelante.
Las aplicaciones intercambian información con el servidor mediante AJAX, por lo que
es importante saber qué AngularJS lo hace a través del servicio $http. El mismo puede
ser configurado mediante su proveedor $httpProvider para editar los headers enviados
al servidor en cada petición o transformar la respuesta del mismo antes de ser entregada
por el servicio. Este comportamiento puede ser configurado en el archivo HTTP.js.
En esencia, éste es el contenido de la carpeta App/Config de igual forma se puede
continuar creando archivos de configuración según las necesidades de cada aplicación y a
medida que se vayan usando los servicios. Las configuraciones de los módulos de terceros
deben estar situados en App/Config/Packages para lograr una adecuada estructura.
Los directorios restantes dentro de la carpeta App tienen un significado muy simple:
Controllers, Directives y Filters serán utilizados para guardar los controladores, directivas y filtros respectivamente. La carpeta de Services será utilizada para organizar toda
la lógica de nuestra aplicación que pueda ser extraída de los controladores, logrando de
esta forma tener controladores con responsabilidades únicas. Y por último en la carpeta
Models se maneja todo lo relacionado con datos en la aplicación.
Si logramos hacer uso de esta estructura obtendremos como resultado una aplicación
organizada y fácil de mantener.
Capítulo 3: Módulos
Hasta ahora hemos estado declarando el controlador como una función de Javascript
en el entorno global, para los ejemplos estaría bien, pero no para una aplicación real. Ya
sabemos que el uso del entorno global puede traer efectos no deseados para la aplicación.
AngularJS nos brinda una forma muy inteligente de resolver este problema y se llama
Módulos.
Creando módulos
Los módulos son una forma de definir un espacio para nuestra aplicación o parte de
la aplicación ya que una aplicación puede constar de varios módulos que se comunican
entre sí. La directiva ng-app que hemos estado usando en los ejemplos anteriores es el
atributo que define cual es el módulo que usaremos para ese ámbito de la aplicación.
Aunque si no se define ningún módulo se puede usar AngularJS para aplicaciones
pequeñas, no es recomendable.
En el siguiente ejemplo definiremos el primer módulo y lo llamaremos miApp, a
continuación, haremos uso de él.
1
2
3
4
5
6
7
8
9
10
11
12
<body ng-app="miApp">
<div class="container" ng-controller="miCtrl">
{{ mensaje }}
</div>
<script src="lib/angular.js"></script>
<script>
angular.module('miApp', [])
.controller('miCtrl', function ($scope) {
$scope.mensaje = 'AngularJS Paso a Paso';
});
</script>
</body>
En el ejemplo anterior tenemos varios conceptos nuevos. Comencemos por mencionar
que al incluir el archivo angular.js en la aplicación, éste hace que esté disponible el objeto
angular en el entorno global o sea como propiedad del objeto window, lo podemos
comprobar abriendo la consola del navegador en el ejemplo anterior y ejecutando
console.dir(angular) o console.dir(window.angular)
30
Capítulo 3: Módulos
31
A través de este objeto crearemos todo lo relacionado con la aplicación.
Para definir un nuevo módulo para la aplicación haremos uso del método module del
objeto angular como se puede observar en la línea 7. Este método tiene dos funcionalidades: crear nuevos módulos o devolver un módulo existente. Para crear un nuevo
módulo es necesario pasar dos parámetros al método. El primer parámetro es el nombre
del módulo que queremos crear y el segundo una lista de módulos necesarios para el
funcionamiento del módulo que estamos creando. La segunda funcionalidad es obtener
un módulo existente, en este caso sólo pasaremos un primer parámetro al método, que
será el nombre del módulo que queremos obtener y este será devuelto por el método.
Minificación y Compresión
En el ejemplo anterior donde creábamos el módulo comenzamos a crear los controladores fuera del espacio global, de esta forma no causará problemas con otras librerías
o funciones que hayamos definido en la aplicación. En esta ocasión el controlador es
creado por un método del módulo que recibe dos parámetros. El primero es una cadena
de texto definiendo el nombre del controlador, o un objeto de llaves y valores donde
la llave sería el nombre del controlador y el valor el constructor del controlador. El
segundo parámetro será una función que servirá como constructor del controlador, este
segundo parámetro lo usaremos si hemos pasado una cadena de texto como primer
parámetro.
Hasta este punto todo marcha bien, pero en caso de que la aplicación fuera a ser
minificada¹⁴ tendríamos un problema ya que la dependencia $scope seria reducida y
quedaría algo así como:
1
.controller('miCtrl',function(a){
AngularJS no podría inyectar la dependencia del controlador ya que a no es un servicio
de AngularJS. Este problema tiene una solución muy fácil porque AngularJS nos permite
pasar un arreglo como segundo parámetro del método controller. Este arreglo contendrá una lista de dependencias que son necesarias para el controlador y como último
elemento del arreglo la función de constructor. De esta forma al ser minificado nuestro
script no se afectarán los elementos del arreglo por ser solo cadenas de texto y quedaría
de la siguiente forma:
1
.controller('miCtrl',['$scope',function(a){
¹⁴Minificar es el proceso por el que se someten los scripts para reducir tamaño y así aumentar la velocidad de carga del mismo.
Capítulo 3: Módulos
32
AngularJS al ver este comportamiento inyectará cada uno de los elementos del arreglo
a cada uno de las dependencias del controlador. En este caso el servicio $scope será
inyectado como a en el constructor y la aplicación funcionará correctamente.
Es importante mencionar que el orden de los elementos del arreglo será el mismo
utilizado por AngularJS para inyectarlos en los parámetros del constructor. En caso de
equivocarnos a la hora de ordenar las dependencias podría resultar en comportamientos
no deseados. En lo adelante para evitar problemas de minificación el código será escrito
como en el siguiente ejemplo.
1
2
3
4
5
6
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.mensaje = 'AngularJS Paso a Paso';
}]);
</script>
Inyectar dependencias mediante $inject
Hasta el momento hemos visto como inyectar dependencias mediante la notación del
arreglo como se explicó en el apartado de la minificación. Existe otra vía la cual nos
permitirá escribir código más fácil de leer e interpretar. Haciendo uso de la propiedad
$inject de las funciones que utilizaremos, podremos especificar que necesitamos inyectar en estas. Para ver su funcionamiento vamos a ver el siguiente ejemplo.
1
2
3
4
5
6
7
angular.module('app', [])
.controller('AppCtrl', AppCtrl);
AppCtrl.$inject = ['$scope', '$interval', '$http', '$log'];
function AppCtrl($scope, $interval, $http, $log){
// Contenido del controlador
}
Como habrás podido comprobar es mucho más fácil de entender el código si lo creamos
especificando funciones separadas. Hay varias ventajas que nos permite separar el
controlador a su propia función nombrada y no en una función anónima. La primera
es que es mucho más descriptivo el código a la hora de interpretarlo. La más importante
ventaja es la de poder especificar una propiedad $inject con todas las dependencias que
necesita el controlador.
Esta versión de la inyección de dependencia es la más utilizada por los desarrolladores.
Por este motivo en lo adelante esteremos intercambiando entre esta vía para inyectar
las dependencias y la que ya sabias anteriormente. De esta forma te será más fácil
recordarlas.
Capítulo 3: Módulos
33
Inyección de dependencia en modo estricto
En ocasiones puede ocurrir que olvidemos poner la anotación de alguna de las dependencias que necesita la aplicación. En este caso cuando vallamos a producción y el
código seaminificado podríamos tener graves problemas. Para solucionar este problema
en Angular 1.3 incluye una nueva directiva ng-strict-di que impedirá que la aplicación
funcione hasta que todas las dependencias sean anotadas correctamente. Esta directiva
debe ser utilizada en el mismo elemento HTML donde definimos la aplicación con ngapp.
1
<html lang="en" ng-app="miApp" ng-strict-di>
Este no es uno de los cambios más importantes de esta versión, pero para los desarrolladores que utilizan las dependencias anotadas les es de gran utilidad.
Configurando la aplicación
En el ciclo de vida de la aplicación AngularJS nos permite configurar ciertos elementos
antes de que los módulos y servicios sean cargados. Esta configuración la podemos
hacer mediante el módulo que vamos a utilizar para la aplicación. El módulo posee
un método config() que aceptará como parámetro una función donde inyectaremos las
dependencias y configuraremos. Este método es ejecutado antes de que el propio módulo
sea cargado.
A lo largo del libro estaremos haciendo uso de este método para configurar varios
servicios. Es importante mencionar que un módulo puede tener varias configuraciones,
estas serán ejecutadas por orden de declaración. En lo adelante también mencionamos
varios servicios que pueden ser configurados en el proceso de configuración del módulo
y será refiriendo a ser configurado mediante este método.
La inyección de dependencia en esta función de configuración solo inyectará dos tipos
de elementos. El primero serán los servicios que sean definidos con el método provider.
El segundo son las constantes definidas en la aplicación. Si tratáramos de inyectar algún
otro tipo de servicio o value obtendríamos un error. La sintaxis de la configuración es
la siguiente.
1
2
3
4
angular.module('miApp')
.config(['$httpProvider', function ($httpProvider) {
// Configuraciones al servicio $http.
}]);
Capítulo 3: Módulos
34
Método run
En algunas ocasiones necesitaremos configurar otros servicios que no hayan sido declarados con el método provider del módulo. Para esto el método config del módulo no nos
funcionará ya que los servicios aún no han sido cargados, incluso ni siquiera el módulo.
AngularJS nos permite configurar los demás elementos necesarios de la aplicación justo
después de que todos los módulos, servicios han sido cargados completamente y están
listos para usarse.
El método run() del módulo se ejecutará justo después de terminar con la carga de todos
los elementos necesarios de la aplicación. Este método también acepta una función como
parámetro y en esta puedes hacer inyección de dependencia. Como todos los elementos
han sido cargados puedes inyectar lo que sea necesario. Este método es un lugar ideal
para configurar los eventos ya que tendremos acceso al $rootScope donde podremos
configurar eventos para la aplicación de forma global.
Otro de los usos más comunes es hacer un chequeo de autenticación con el servidor,
escuchar para si el servidor cierra la sesión del usuario por tiempo de inactividad cerrarla
también en la aplicación cliente. Escuchar los eventos de cambios de la ruta y del servicio
$location. La Sintaxis es esencialmente igual a la del método config.
1
2
3
4
5
6
angular.module('miApp')
.run(['$rootScope', function ($rootScope) {
$rootScope.$on('$routeChangeStart', function(e, next,current){
console.log('Se comenzará a cambiar la ruta hacia' + next.originalPath);
})
}]);
Después de haber visto como obtener AngularJS, la manera de insertarlo dentro de la
aplicación, la forma en que este framework extiende los elementos HTML con nuevos
atributos, la definición, la aplicación con módulos y controladores considero que has
dado tus primeros pasos. Pero no termina aquí, queda mucho por recorrer. Esto tan solo
es el comienzo.
Capítulo 4: Servicios
En la estructura MVC debemos seguir unos patrones que nos indican como debe ser
la organización interna de la aplicación. Las Vistas son las encargadas de mostrar la
información al usuario. Los modelos se encargan de almacenar la información y hacerla
disponible cuando sea necesaria. Y por último los controladores son los encargados
de obtener las Peticiones del usuario y transformarlas en Respuestas. De esta forma
pareciera que no tenemos lugar donde escribir la lógica de la aplicación. Es donde viene
a tomar lugar los Servicios. Estos son los encargados de llevar toda la lógica de la
aplicación que no debe ser de interés para el controlador.
Un ejemplo clásico de lo mencionado anteriormente es cuando un usuario entra a
la aplicación y se le requiere que se identifique. El usuario escribe el usuario y la
contraseña en la vista y envía el formulario pidiendo ser comprobado sus credenciales.
El controlador recibe la petición y aquí es donde comienza el proceso de identificación.
Podríamos simplemente comprobar pidiendo información al modelo para saber si sus
datos son los correctos y permitir al usuario entrar en la aplicación. Pero desde el
momento en que realicemos esta operación, el controlador estará realizando tareas que
no le corresponden ya que su única responsabilidad es recibir peticiones y entregar
respuestas.
En este lugar es donde los Servicios deberían hacer su trabajo. Continuando con la
hipótesis anterior, el controlador al recibir la petición del usuario entrega los datos al
servicio de identificación. Éste comprueba con el modelo si los datos de identificación
son correctos e indica al controlador que el usuario puede entrar o no en la aplicación.
El controlador devuelve una respuesta al usuario con un mensaje de error o redireccionandolo hacia donde debe ir después de identificarse.
Logrando este nivel de extracción, los controladores siempre deberán tener una responsabilidad única y delegar en los servicios todo tipo de lógica de la aplicación. Obteniendo
como resultado una aplicación bien organizada y fácil de pasar por pruebas (Test’s).
Los servicios en AngularJS son singleton lo que quiere decir que son objetos instanciados
una vez y las demás ocasiones que se trate de instanciarlos se obtendrá el mismo objeto.
El uso de los servicios nos permitirá intercambiar información entre diferentes partes
de la aplicación ya que al ser creados y modificados todos los que accedan al obtendrán
el mismo resultado.
AngularJS trae en su núcleo muchos servicios que nos permiten ahorrarnos gran
cantidad de código ya que nos proveen de funcionalidades básicas de cualquier aplicación
web. Además de los que nos proporciona el framework, este nos permite crear servicios
para satisfacer las necesidades específicas de la aplicación que estas creando.
35
Capítulo 4: Servicios
36
Existen tres formas de definir los servicios en AngujarJS pero todas forman parte
del módulo. En algunas aplicaciones podrás observar que se crean módulos solo para
almacenar servicios con funcionalidades específicas de la aplicación. Ya que con el uso de
los servicios podemos crear bloques de códigos que sean reutilizables en varios lugares de
la aplicación e incluso a través de diferentes aplicaciones si estos son menos específicos.
Factory
Las tres formas de definir servicios en el módulo son con los métodos service(), factory()
y provider(), en este orden de complejidad. Comenzaré por los factory() con el siguiente
ejemplo. Teniendo en cuenta la estructura de directorios descrita en el Capítulo 2 el
archivo index.html posee el siguiente contenido.
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<div class="container" ng-controller="PlaylistCtrl">
<ul> <li ng-repeat="titulo in playlist"> {{ titulo }} </li> </ul>
</div>
<div ng-controller="PlaylistMetodosCtrl">
<ul>
<li ng-repeat="titulo in playlist">
{{ titulo }}
<a class="button" href="#" ng-click="borrar($index)">×</a>
</li>
</ul>
</div>
<script src="lib/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Services/Playlist.js"></script>
<script src="js/Controllers/PlaylistCtrl.js"></script>
<script src="js/Controllers/PlaylistMetodosCtrl.js"></script>
</body>
El archivo App/js/app.js posee la declaración del módulo y sus dependencias. Aunque
por ahora no tiene ninguna dependencia.
Capítulo 4: Servicios
37
Archivo: App/js/app.js
1
2
'use strict';
angular.module('miApp', []);
Dentro de la carpeta Servicios crearemos un nuevo archivo Playlist.js que será el primer
servicio de tipo factory.
Archivo: App/js/Services/Playlist.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
angular.module('miApp')
.factory('Playlist', [function () {
var playlist = [
'The Miracle (Of Joey Ramone)',
'Raised By Wolves',
'Every Breaking Wave',
'Cedarwood Road',
'California (There Is No End to Love)',
'Sleep Like a Baby Tonight',
'Song for Someone',
'This Is Where You Can Reach Me Now',
'Iris (Hold Me Close)',
'The Troubles',
'Volcano'
];
var listar = function(){return playlist;};
var borrar = function(id){playlist.splice(id,1);};
return {
listar: listar,
borrar: borrar
};
}])
La definición de un servicio de tipo factory es muy sencilla además es la más usada. Un
factory se declara con el método factory() del modelo y este recibe como parámetro
el nombre del servicio y un arreglo con las dependencias y el constructor del servicio
que será utilizado para crear la instancia del servicio. Algo muy importante a tener en
cuenta es que los servicios de tipo factory siempre tienen que devolver una respuesta
con la palabra return. Este comportamiento nos permite crear cualquier tipo de objetos
complejos privados y solo devolver un objeto con los métodos visibles para el usuario, de
esta manera toda la lógica quedaría privada y accesible al usuario solo un API para llevar
a cabo las acciones que permite el servicio.
Capítulo 4: Servicios
38
El servicio que ha sido creado es muy sencillo, posee una lista de canciones y dos métodos
para interactuar con la misma. La lista está directamente dentro del constructor de
manera que si tratamos de acceder a ella fuera del servicio el resultado será undefined
ya que el servicio solo devuelve un objeto con los métodos públicos.
Ahora que ya tenemos el servicio creado vamos con el controlador PlaylistCtrl.js en la
carpeta Controllers
Archivo: App/js/Controllers/PlaylistCtrl.js
1
2
3
4
5
angular.module('miApp')
.controller('PlaylistCtrl', ['$scope', 'Playlist',
function ($scope, Playlist) {
$scope.playlist = Playlist.listar();
}]);
Como han podido observar hemos inyectado el servicio Playlist como dependencia de
nuestro controlador y hemos asignado la lista de canciones al $scope mediante la función
listar() definida por el servicio para hacerlo disponible en la vista. Ahora iteraremos
sobre el con el uso de la directiva ng-repeat.
Archivo: App/index.html
1
2
3
<div class="container" ng-controller="PlaylistServiceCtrl">
<ul> <li ng-repeat="titulo in playlist"> {{ titulo }} </li> </ul>
</div>
Después de haber incluido el controlador en el index.html. Con estas líneas de código
el contenido del servicio se mostrará al usuario con la ayuda de la directiva ng-repeat la
cual es detallada a fondo en el Capítulo 5. De esta forma el controlador PlaylistCtrl solo
ha tenido la responsabilidad de gestionar la información que será mostrada al usuario.
El servicio Playlist fue el encargado de obtener esa información y hacerla disponible.
Esto ha sido un ejemplo muy sencillo donde el servicio solo ha devuelto un objeto con
la funcionalidad necesaria para manejar la información, pero la lógica donde obtenemos
esos datos queda fuera de alcance.
Ahora haremos uso de ese servicio en otro controlador para ejecutar el otro método del
servicio para borrar canciones de la lista. De esta forma también podrás observar como
los servicios son singleton y cuando su contenido es modificado en un lugar de nuestra
aplicación, a su vez este cambio es reflejado a lo largo de la aplicación.
Capítulo 4: Servicios
39
Archivo: App/js/Controllers/PlaylistMetodosCtrl.js
1
2
3
4
5
6
angular.module('miApp')
.controller('PlaylistMetodosCtrl', ['$scope', 'Playlist', function ($scope, Play\
list) {
$scope.playlist = Playlist.listar();
$scope.borrar = function(id){Playlist.borrar(id);};
}]);
En el controlador hemos expuesto a la vista el método borrar, este método recibe como
parámetro el índice que queremos borrar del arreglo. Este índice es obtenido de la
variable $index que hace disponible ng-repeat dentro del bucle. Veamos la vista como
quedaría.
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div class="container" ng-controller="PlaylistCtrl">
<ul> <li ng-repeat="titulo in playlist"> {{ titulo }} </li> </ul>
</div>
<div ng-controller="PlaylistMetodosCtrl">
<ul>
<li ng-repeat="titulo in playlist">
{{ titulo }}
<a class="button" href="#" ng-click="borrar($index)">×</a>
</li>
</ul>
</div>
<script src="lib/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Services/Playlist.js"></script>
<script src="js/Controllers/PlaylistMetodosCtrl.js"></script>
</body>
Como puedes observar en la segunda lista hay un vínculo al final de cada canción el cual
eliminará ese índice de la lista. Al eliminar un elemento del arreglo podemos comprobar
que efectivamente este es eliminado pero que también es actualizada la lista mostrada
por el primer controlador. De esta forma podríamos intercambiar información a través
de los controladores ya que los servicios pueden ser inyectados tantas veces como sean
necesarios y siempre existirá una sola instancia de los mismos.
Capítulo 4: Servicios
40
Service
Ahora hablaremos de otra de las formas de declarar servicios en AngularJS es específicamente con el método service() de los módulos. Esencialmente se declaran de la misma
forma que los factory() y podemos obtener los mismos resultados de los mismos. Pero lo
que lo hace diferente es que los services() van a declarar una nueva instancia de una clase
cuando son utilizados. Vamos a hacer el ejemplo del factory() pero esta vez con service()
para ver la diferencia.
Archivo: App/js/Services/PlaylistService.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
angular.module('miApp')
.service('PlaylistService', [function () {
var playlist = [
'The Miracle (Of Joey Ramone)',
'Raised By Wolves',
'Every Breaking Wave',
'Cedarwood Road',
'California (There Is No End to Love)',
'Sleep Like a Baby Tonight',
'Song for Someone',
'This Is Where You Can Reach Me Now',
'Iris (Hold Me Close)',
'The Troubles',
'Volcano'
];
this.listar = function(){return playlist;};
this.borrar = function(id){playlist.splice(id,1);};
}])
Como puedes observar ahora el servicio no devuelve ningún objeto con la palabra return
esta vez la misma función es el objeto que ha sido instanciada con new y los métodos son
expuestos a través de this como haríamos con una clase Javascript. Veamos su uso en el
controlador.
Capítulo 4: Servicios
41
Archivo: App/js/Controllers/PlaylistServiceCtrl.js
1
2
3
4
5
6
angular.module('miApp')
.controller('PlaylistServiceCtrl', ['$scope', 'PlaylistService',
function ($scope, PlaylistService) {
$scope.playlist = PlaylistService.listar();
console.log(PlaylistService.playlist);
}]);
En la línea 4 he tratado de acceder a la variable privada playlist y escribir su contenido
a la consola para comprobar que no es accesible. El contenido restante del controlador
es esencialmente el mismo. Veamos la vista.
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
<body>
<div class="container" ng-controller="PlaylistServiceCtrl">
<ul> <li ng-repeat="titulo in playlist"> {{ titulo }} </li> </ul>
</div>
<script src="lib/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Services/PlaylistService.js"></script>
<script src="js/Controllers/PlaylistServiceCtrl.js"></script>
</body>
Si abrimos la consola del navegador esta mostrará el mensaje undefined ya que la
propiedad playlist es privada dentro del servicio.
Aun así, los service no son muy diferentes de los factory, he aquí la mejor parte para
los que están acostumbrados a crear clases Javascript. Veamos el ejemplo anterior de una
forma diferente y utilizaremos otra forma de declarar servicios con service().
Archivo: App/js/Services/PlaylistServiceClass.js
1
2
3
4
5
6
7
8
9
var PlaylistServiceClass = function(){
var playlist = [
'The Miracle (Of Joey Ramone)',
'Raised By Wolves',
'Every Breaking Wave',
'Cedarwood Road',
'California (There Is No End to Love)',
'Sleep Like a Baby Tonight',
'Song for Someone',
Capítulo 4: Servicios
10
11
12
13
14
15
16
17
18
19
20
42
'This Is Where You Can Reach Me Now',
'Iris (Hold Me Close)',
'The Troubles',
'Volcano'
];
this.listar = function(){return playlist;};
this.borrar = function(id){playlist.splice(id,1);};
}
angular.module('miApp')
.service('PlaylistService', PlaylistServiceClass);
De esta forma podemos crear los servicios como clases comunes de Javascript y luego
usarlas como servicios en AngularJS.
Provider
La tercera vía y la más compleja de declarar servicios en AngularJS es mediante el
método provider() de los módulos. Primero veamos el ejemplo y después lo describiré.
Archivo: App/js/Services/PlaylistProvider.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
angular.module('miApp')
.provider('Playlist', [function () {
var playlist = [
'The Miracle (Of Joey Ramone)',
'Raised By Wolves',
'Every Breaking Wave'
];
var listar = function(){return playlist;};
var borrar = function(id){playlist.splice(id,1);};
return {
agregar: function(data){
playlist = playlist.concat(data);
},
$get: function(){
return {
listar: listar,
borrar: borrar
};
}
Capítulo 4: Servicios
20
21
43
};
}]);
A primera vista parece un poco raro, con ese $get que no de donde habrá salido. Una vez
más este servicio tiene la misma funcionalidad que los que hemos creado hasta ahora
con factory y service. Lo que hace diferente este a los dos anteriores es que los provider
permiten ser configurados en el momento en que se está configurando la aplicación.
Mediante el método config del módulo podremos pre configurar el servicio antes de
que este sea inyectado.
Comencemos por mencionar que se puede declarar exponiendo los métodos con return
o creando una clase completamente aislada como con los servicios, exponiendo los
métodos con this y pasándola como segundo parámetro en la declaración del provider.
Algo muy importante y que es requerido por los provider es que se exponga el método
$get que será una función que devolverá los métodos públicos del servicio. Realmente
lo que sea devuelto por la función de $get es lo que obtendremos cuando inyectemos el
servicio en los controladores u otros servicios. Los demás métodos que se expongan en
el provider serán solo accesibles desde el método config() del módulo. Para configurar
he creado un archivo en la carpeta Config con el nombre de PlaylistProvider.js para
mantener la organización de la aplicación.
Archivo: App/js/Config/Playlist.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('miApp').
config(['PlaylistProvider', function (PlaylistProvider) {
var canciones = [
'Cedarwood Road',
'California (There Is No End to Love)',
'Sleep Like a Baby Tonight',
'Song for Someone',
'This Is Where You Can Reach Me Now',
'Iris (Hold Me Close)',
'The Troubles',
'Volcano'
];
PlaylistProvider.agregar(canciones);
}])
En el método config del módulo inyectamos como dependencia el PlaylistProvider. Ya
sé que no existe, cuando creamos el servicio con provider lo llamamos solo Playlist. El
motivo por lo que solo lo llamamos Playlist fue porque AngularJS cuando ve la palabra
Capítulo 4: Servicios
44
Provider detrás de un servicio automáticamente busca el servicio con el nombre que
precede la palabra provider e inyecta el provider de ese servicio en vez de inyectar el
$get. De esta forma tendremos acceso a los métodos expuestos por el servicio para su
configuración. El servicio después de ser configurado será inyectado donde quiera que
se necesite, pero ya con los cambios realizados.
En este archivo de configuración solo agregamos las canciones restantes a la lista de
canciones. Ya que en el servicio inicialmente tiene solo 3. Esto lo hacemos mediante el
método agregar que expusimos en el provider cuando lo creamos.
Constant y Value
Existen otras dos formas de declarar servicios, pero aún más sencillas ya que estos
responden a funcionalidades muy simples. En la programación las constantes siempre
han sido una clase de variable con un contenido definido que no puede ser alterado
después de su definición. En AngularJS ese concepto de constante no es del todo
valido. El framework nos permite declarar constantes con el método constant(). Este
acepta como primer parámetro el nombre y como segundo parámetro una cadena de
texto, numero, arreglo, objeto o función que será devuelta cuando se utilice. Estas
constantes pueden ser inyectadas desde la configuración del módulo, en otros servicios o
controladores donde pueden ser modificadas asignándole un nuevo valor a la constante.
Archivo: App/js/Config/Constant.js
1
2
3
4
5
6
angular.module('miApp')
.constant('CSRF_TOKEN', '94a08da1fecbb6e8b46990538c7b50b2')
.constant('API_TOKEN', {
_public: 'a2d10a3211b415832791a6bc6031f9ab',
_secret: '5ebe2294ecd0e0f08eab7690d2a6ee69'
});
Los value son una forma sencilla de registrar un servicio como con provide donde su
propiedad $get no recibe parámetros y es devuelta. Estos no pueden ser inyectados en la
configuración del módulo, pero pueden ser modificados por un decorator de Angular.
Llevado a la práctica realmente no tienen mucha diferencia de las constantes. Se declaran
de la misma forma con el método value() del módulo.
Archivo: App/js/Config/Values.js
1
2
angular.module('miApp')
.values('API_URL', 'api.example.com/v1/');
Capítulo 4: Servicios
45
¿Cuándo debemos usar una constant o un value?. Deberíamos usar los values cuando
necesitemos registrar un servicio. Las constant cuando necesitamos incluirlas en la
configuración del módulo ya que los values producirían un error si son inyectados en la
configuración.
Decorators
Cuando necesitamos agregar cierta funcionalidad a un servicio sin modificar su código,
ya sea uno propio o de una librería de terceros. Angular nos provee el método decorator()
del servicio $provide. Este método intercepta la creación del servicio que queremos
decorar permitiéndonos modificar el comportamiento del servicio antes de que sea
creado. El objeto que devuelva el decorator debe ser el mismo servicio o un nuevo
servicio que remplace el original.
Este método recibe dos parámetros. El primero es el nombre del servicio que queremos
decorar. El segundo es una función que será ejecutada cuando el servicio necesite ser
instanciado y necesita devolver una instancia del servicio ya decorado. Esta función
se le inyectara la instancia original del servicio para ser decorada. Veamos un ejemplo
decorando uno de los servicios anteriores para obtener una cadena de texto separada
por comas de lista de canciones del servicio.
Archivo: App/js/Decorators/Playlist.js
1
2
3
4
5
6
7
8
9
angular.module('miApp')
.config(['$provide', function ($provide) {
$provide.decorator('Playlist', ['$delegate', function($delegate) {
$delegate.texto = function(){
return $delegate.listar().join(', ');
};
return $delegate;
}]);
}]);
De esta forma ahora tenemos disponible un nuevo método en el servicio llamado texto
que devuelve una cadena con todas las canciones separadas por comas.
$provide
Hasta el momento hemos estado usando los métodos provider(), constant(), value(),
factory() y service() del módulo para declarar los servicios en angular. Todos estos no
son más que accesos directos a los métodos del servicio $provide de AngularJS. Este
Capítulo 4: Servicios
46
servicio es el encargado de registrar los componentes con el $injector que a su vez es el
encargado de devolver las instancias de los servicios definidos por $provide.
AngularJS nos provee varios servicios para resolver tareas específicas dentro de la
aplicación, a medida que vayamos haciendo uso de estos iré explicándolos al detalle.
Ahora solo detallaré el servicio $q ya que lo utilizaremos en el próximo capítulo.
Promesas
En el desarrollo de una aplicación en ocasiones necesitamos mostrar información al
usuario y puede que esta no esté disponible. En la mayoría de los casos la aplicación
para su curso de ejecución hasta que esos datos estén disponibles para ser mostrados
y continuar con la ejecución. Estos comportamientos no deseados podrían afectar
grandemente a la aplicación. Situaciones como estas se agudizan más aun cuando se trata
de realizar peticiones a un servidor remoto donde tenemos que esperar una respuesta
que puede tardar períodos de tiempo diferentes en cada petición. También al recibir la
respuesta puede ser de un error o de un resultado satisfactorio para la aplicación.
Para resolver este tipo de situaciones necesitaríamos lograr que cuando la aplicación
llegue a la ejecución de una de estas peticiones, las hiciera de forma paralela para que la
aplicación siga su curso de carga. Para cuando la petición termine de realizarse también
necesitaríamos que nuestra aplicación sea notificada y hacer los trabajos necesarios con
la respuesta del servidor.
Para resolver problemas como este AngularJS nos provee un servicio llamado $q que
es una implementación de las promesas en Javascript. Este servicio está basado en la
librería Q de Kris Kowal’s. Y AngularJS hace un uso extensivo de las promesas para
entregar información cuando esté disponible.
$q puede ser inyectado como los demás servicios de Angular en controladores y servicios
para ejecutar tareas asíncronas y permitir tomar decisiones dependiendo de si la promesa
es resuelta o no. Comencemos por explicar cómo funciona. Para comenzar a usar
las promesas necesitamos crear un objeto para aplazar alguna tarea. Esto es logrado
mediante $q.defer(), de esta forma obtenemos una nueva instancia del objeto defer listo
para ser utilizado. Este objeto tiene tres métodos, resolve, reject y notify. Veamos un
ejemplo.
Capítulo 4: Servicios
Archivo: App/js/Controllers/PromiseCtrl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
angular.module('miApp')
.controller('PromiseCtrl', ['$scope', '$q', function ($scope, $q) {
var checkServer = function(){
var def = $q.defer();
setTimeout(function(){
def.resolve('Online');
}, 2000);
return def.promise;
};
var checkHTTP = function(){
var def = $q.defer();
setTimeout(function(){
if ( Math.floor(Math.random()*100) > 50 ) {
def.resolve('Online');
} else {
def.reject('El servicio no está disponible');
};
}, 5000)
return def.promise;
}
var checkDb = function(){
var def = $q.defer();
setTimeout(function(){
if ( Math.floor(Math.random()*100) > 50 ) {
def.resolve('Online');
} else {
def.reject('El servicio no está disponible');
};
}, 3000)
return def.promise;
}
var checkSsl = function(){
var def = $q.defer();
setTimeout(function(){
def.notify('Comprobación de conexión segura iniciada.');
if ( Math.floor(Math.random()*100) > 50 ) {
def.notify('Las conexiones seguras están habilitadas');
def.resolve('SSL');
47
Capítulo 4: Servicios
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
48
} else {
def.notify('Las conexiones seguras están desactivadas');
def.reject('Desactivadas');
};
}, 4000)
return def.promise;
}
checkServer().then(function(result){
$scope.status = result;
});
checkHTTP().then(function(result){
$scope.http = result;
}, function(err){
$scope.http = err;
});
checkDb().then(function(result){
$scope.db = result;
}, function(err){
$scope.db = err;
});
checkSsl().then(function(result){
$scope.ssl = result;
}, function(err){
$scope.ssl = err;
}, function(notif){
console.log(notif);
});
}])
En el ejemplo anterior he simulado una comprobación de estados de un servidor
utilizando setTimeout para demorar las respuestas y observar el comportamiento de
las promesas. Hay que tener en cuenta que toda esta lógica la he escrito en el controlador
para propósitos del ejemplo, en una aplicación real estos métodos de chequeo deben ser
extraídos a su propio servicio. Ya que el controlador no necesita saber cómo es que se
comprueba el estado del servidor sino cual es el estado de los servicios para mostrarlos
al usuario. Observemos los resultados en el navegador.
Capítulo 4: Servicios
49
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
10
11
<body>
<div class="container" ng-controller="PromiseCtrl">
<p>Estado del servidor: {{ status }}</p>
<p>Estado del servicio HTTP: {{ http }}</p>
<p>Estado del servicio de Base de Datos: {{ db }}</p>
<p>Estado de las conexiones seguras: {{ ssl }}</p>
</div>
<script src="lib/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Controllers/PromiseCtrl.js"></script>
</body>
Como se ha podido observar todos los procesos se comienzan a ejecutar al mismo tiempo
y los resultados se van mostrando a medida que se van resolviendo por $q. De esta forma
la aplicación no ha parado para esperar a que el primer resultado sea obtenido para
continuar su ejecución. Ahora describiré el código del ejemplo anterior
En el controlador se ha inyectado como dependencia el servicio $q para hacer uso
de las promesas. También se han creado una función para comprobar cada uno de
los servicios del servidor comencemos por la primera. Esta función se encargará de
comprobar la disponibilidad del servidor. Para esta función he creado el objeto def
mediante $q.defer() que devuelve una nueva instancia del objeto defer y representa
una tarea que se finalizará en el futuro. Ahora el objeto def tiene varios métodos. El
primero que usamos es el método resolve() que recibe como parámetro el valor que será
entregado por la promesa cuando sea utilizada en el futuro. En este ejemplo es la cadena
Online porque hemos obligado a que siempre muestre que el servidor está online. Luego
devolvemos la promesa con return def.promise para ser usada posteriormente.
Como el resultado ha sido obligado, esta promesa siempre devolverá Online como
resultado de su ejecución. Ya tenemos la función lista para ser ejecutada asíncrona.
Ahora necesitamos ejecutar acciones cuando esta haya terminado si ejecución y tenga
los resultados listos. El objeto promise que se ha devuelto de la función tiene un método
then para ejecutar acciones dependiendo del resultado de la promesa. El método then
recibe tres funciones como parámetros, el primero se ejecutará si la promesa se ha
resuelto, el segundo si ha sido rechazada y la tercera es una función que se ejecutará
tantas veces como se haya usado el método notify del objeto defer. Cada una de estos
métodos recibe como parámetro una función que a su vez recibe como parámetro el
resultado de la promesa.
Para la comprobación del estado del servidor solo se ha pasado la primera función de
parámetro al método then por qué dispuesto en el código de la promesa que siempre
será resuelta. En esta función asignamos el resultado de la promesa a la propiedad status
Capítulo 4: Servicios
50
del $scope para hacerlo disponible en la vista desde el momento en que el resultado esté
listo.
Para la comprobación del servicio HTTP he creado otra función donde esta vez la
promesa será resuelta o rechazada dependiendo de un valor aleatorio obtenido con la
clase Math de Javascript. El resultado es mostrado en la vista dependiendo si se resuelve
o no la promesa, porque esta vez hemos pasado el segundo parámetro a al método then
que se ejecutará si la promesa es rechazada.
En la comprobación de conexiones seguras he utilizado el método notify para indicar
el estado de la comprobación y el resultado de la misma. De esta forma podremos ver
reflejado en la consola del navegador, el proceso de comprobación cada vez que se
notifique.
Varias promesas a la vez
Existen ocasiones donde tenemos varias promesas que se resolverán en diferente tiempo,
pero necesitamos esperar a que todas se resuelvan para tomar acciones cuando todas
hayan finalizado. Para solucionar este tipo de necesidad, el servicio $q tiene un método
que acepta un arreglo de promesas en el cual podremos tomar acciones cundo todas
hayan finalizado.
Para ver este comportamiento en acción vamos a crear un ejemplo con tres promesas
utilizando la función setTimeout de Javascrip, cuando cada una de ellas se resuelva
imprimiremos en la consola un mensaje.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.controller('AppCtrl', function ($q) {
var promesa1 = $q.defer();
var promesa2 = $q.defer();
var promesa3 = $q.defer();
promesa1.promise.then(completado);
promesa2.promise.then(completado);
promesa3.promise.then(completado);
function completado(data) {
console.log(data);
}
setTimeout(function () {
promesa1.resolve('Promesa #1 resuelta');
}, Math.random() * 1000);
setTimeout(function () {
Capítulo 4: Servicios
18
19
20
21
22
23
51
promesa2.resolve('Promesa #2 resuelta');
}, Math.random() * 1000);
setTimeout(function () {
promesa3.resolve('Promesa #3 resuelta');
}, Math.random() * 1000);
});
Como puedes observar, cuando se ejecuta este controlador, en la consola aparecen
los mensajes de respuesta de cada promesa. Estas son resueltas en orden aleatorio
dependiendo del tiempo que demore el setTimeout. Pero ahora necesitamos una vía
para tomar acciones cuando todas se hayan resuelto. Esta funcionalidad la podemos
obtener mediante el método all del servicio $q. Para ejecutar una acción cuando todas
las promesas han sido resueltas, pasaremos como parámetro un arreglo con todas las
promesas a la función all y luego ejecutaremos la acción necesaria.
1
2
3
4
var todas = $q.all([promesa1.promise, promesa2.promise, promesa3.promise]);
todas.then(function (data) {
console.log(data);
})
En esta ocasión cuando ejecutamos el método then y recibimos los datos, estos serán un
arreglo con el resultado de cada una de las promesas. Es importante mencionar que el
orden en que vienen los resultados de las promesas es el mismo en que le pasamos las
promesas a la función all, sin importar el orden en que estas hayan sido resueltas.
El constructor de las promesas
En la versión 1.3 Angular se introdujo una nueva forma de crear promesas. Esta vez
más acorde a lo que nos entregara la nueva versión de Javascript ECMAScript 6. Ahora
tendremos la posibilidad de utilizar las promesas mediante el constructor del servicio $q.
Para ver la nueva vía vamos a crear un ejemplo simple con la versión antigua y luego la
transformaremos a la nueva forma de utilizar promesas en Angular 1.3.
Para comenzar crearemos una vista con dos botones, uno para que resuelva la promesa
y otro para que la rechace. Estos botones ejecutaran una acción en el controlador
pasándole un valor verdadero o falso como parámetro para resolver o no la promesa.
Además, mostraremos el resultado de la promesa o el error de la misma.
Capítulo 4: Servicios
1
2
3
4
5
6
7
8
52
<body ng-controller="AppCtrl">
<button ng-click="accion(false)">Resolver</button>
<button ng-click="accion(true)">Rechazar</button>
<div>{{resuelta}}</div>
<div>{{rechazada}}</div>
<script src="bower_components/angular/angular.js"></script>
<script src="app.js"></script>
</body>
Ahora en el controlador crearemos una función tarea que será la encargada de crear
la promesa y ejecutarla. Esta se ejecutará de forma asíncrona utilizando la función
setTimeout de Javascript. Dependiendo del valor verdadero o falso que se le pasa como
parámetro a esta función, se resolverá o rechazará la promesa.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('app', [])
.controller('AppCtrl', function ($scope, $q) {
function tarea(comprobar){
var dfd = $q.defer();
setTimeout(function() {
if (!comprobar) {
dfd.resolve('Promesa resuelta');
} else {
dfd.reject('Promesa rechazada');
}
}, 1000);
return dfd.promise;
}
});
Para terminar solo nos queda crear la función que ejecutara la promesa. Crearemos la
función ejecutar y la asignamos al $scope para que los botones de la vista puedan acceder
a ella. Esta función asignará al scope los valores de la promesa en dos variables, resuelta y
rechazada.
Capítulo 4: Servicios
1
2
3
4
5
6
7
8
9
10
11
53
...
$scope.accion = ejecutar;
function ejecutar(comprobar){
tarea(comprobar).then(function (data) {
$scope.resuelta = data;
}, function (error) {
$scope.rechazada = error;
})
}
...
Para convertir la promesa anterior a la nueva vía para crear las promesas, solo necesitaremos hacer algunos cambios en la función tarea que creamos anteriormente. Para
comenzar ya no tendremos que crear un objeto defer sino devolver el resultado del
constructor del servicio $q. Al constructor le pasamos como parámetro una función
anónima que recibirá dos parámetros, estos son dos funciones, resolve que la utilizaremos
para resolver las promesas y reject que utilizaremos para rechazarlas. De esta forma el
nuevo código quedaría como aparece a continuación.
1
2
3
4
5
6
7
8
9
10
11
function tarea(comprobar) {
return $q(function (reject, resolve) {
setTimeout(function () {
if (!comprobar) {
resolve('Promesa resuelta');
} else {
reject('Promesa rechazada');
}
}, 1000);
});
}
Como habrás podido comprobar, el resultado es el mismo, pero esta nueva versión está
más acorde a la nueva interfaz de promesas que trae la nueva versión de Javascript
Ahora solo nos queda ejecutar la aplicación en el navegador y ver su funcionamiento.
Esencialmente este es el comportamiento de las promesas en AngularJS. Poner una tarea
asíncrona a la ejecución de la aplicación y tomar acciones cuando esté lista.
Desplazamiento con $anchorScroll
Cuando creamos aplicaciones, en ocasiones queremos dirigir al usuario a cierto lugar
dentro de la página. Usualmente esto es logrado mediante la asignación de id a ciertos
54
Capítulo 4: Servicios
elementos en el código y haciendo uso de vínculos con la propiedad href apuntando a la
id a la que queremos ir. Esta acción hará que en la dirección del navegador aparezca la id
y el navegador se dirija a esa nueva posición. Pero después de haber dirigido el usuario
a cierto lugar, si este se mueve hacia otro lugar de la página la dirección del navegador
no se cambiará de forma automática. Si el usuario vuelve a dar click en el vínculo que lo
envía a la posición del id, en esta ocasión el navegador no tomara ninguna acción porque
ya está en esa posición la dirección.
Con el nuevo servicio $anchorScroll introducido en la versión 1.3 de Angular, podremos
resolver este problema. Este servicio nos permitirá desplazarnos hacia cualquier id de la
página incluso cuando la dirección del navegador ya este apuntando a esa id. Para ver un
ejemplo vamos a crear una vista con varios vínculos y varios id para desplazarnos hacia
ellos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
.contenido {height: 800px;}
#contenido1 {background-color:
#contenido2 {background-color:
#contenido3 {background-color:
#contenido4 {background-color:
#1ABC9C}
#3498DB}
#9B59B6}
#E74C3C}
</style>
</head>
<body ng-controller="AppCtrl">
<a href="" ng-click="irA(1)">Contenido 1</a>
<a href="" ng-click="irA(2)">Contenido 2</a>
<a href="" ng-click="irA(3)">Contenido 3</a>
<a href="" ng-click="irA(4)">Contenido 4</a>
<div class="contenido" id="contenido1">Contenido
<div class="contenido" id="contenido2">Contenido
<div class="contenido" id="contenido3">Contenido
<div class="contenido" id="contenido4">Contenido
1</div>
2</div>
3</div>
4</div>
<script src="bower_components/angular/angular.js"></script>
<script src="app.js"></script>
</body>
En la vista se han agregado unos estilos para diferenciar cada uno de los contenidos.
Todos los vínculos tienen una directiva ng-click apuntando a una función irA que
definiremos en el controlador. Esta función será la encargada de movernos dentro de
la página hacia el contenido que especifiquemos como parámetro.
Capítulo 4: Servicios
1
2
3
4
5
6
7
55
angular.module('app', [])
.controller('AppCtrl', function ($scope, $anchorScroll) {
$scope.irA = function (id) {
var nuevaId = 'contenido' + id;
$anchorScroll(nuevaId);
}
})
En el controlador inyectamos como dependencia el nuevo servicio $anchorScroll.
Dentro de la función irA que definimos en el $scope, hacemos una llamada al servicio
$anchorScroll pasándole como parámetro el id al que queremos desplazarnos.
Como podrás observar en el ejemplo, después de haber sido desplazado hacia un
contenido específico, puedes ir hacia el inicio de la página y volver a desplazarte hacia el
mismo contenido sin problemas. Con el nuevo servicio se desplazará, aunque no se haya
cambiado la dirección en el navegador.
Hay que tener en cuenta que el servicio $anchorScroll no reflejara la id en la dirección.
Si necesitamos reflejar el id en la dirección para que esta posición pueda ser guardada
como marcador, podemos inyectar el servicio $location y utilizar su método hash para
que este refleje la posición en la dirección del navegador. Una vez utilizado el servicio
location, no es necesario pasar la id al servicio anchorScroll ya que este navegara hacia la
nueva posición gracias a la dirección.
1
2
3
4
5
6
7
8
angular.module('app', [])
.controller('AppCtrl', function ($scope, $anchorScroll, $location) {
$scope.irA = function (id) {
var nuevaId = 'contenido' + id;
$location.hash(nuevaId);
$anchorScroll();
}
});
Este servicio tiene una propiedad que podemos utilizar para dejar un margen superior
cuando hacemos el desplazamiento. Esta propiedad la podremos configurar en el bloque
run de la aplicación para de esta forma este definida para toda la aplicación. Veamos un
ejemplo.
1
2
3
4
angular.module('app', [])
.run(function ($anchorScroll) {
$anchorScroll.yOffset = 50;
});
Capítulo 4: Servicios
56
Ahora cuando utilizamos cualquiera de los enlaces para movernos dentro de la aplicación
y nos desplazamos, veremos que tendremos un margen superior de 50px como definimos
en el bloque run anteriormente. Este comportamiento es muy útil para cuando tenemos
barras de navegación con una posición fija en la parte superior.
El valor de esta propiedad puede ser un número como el ejemplo anterior, este será la
cantidad de pixeles que se dejará como margen en la parte superior. Además, este valor
puede ser una función que será llamada en cada ocasión que se utilice el servicio. Esta
función siempre debe devolver un número, así podremos realizar cálculos para saber la
cantidad de margen que necesitamos en la parte superior. Y por último ese valor también
puede ser un elemento jqLite o jQuery. Se tomará como margen la distancia desde la parte
superior de la página hasta la parte inferior del objeto jqLite/jQuery. Es importante que
este elemento tenga una posición fija, de lo contrario no se tomara en cuenta.
Cache
Si queremos obtener un buen rendimiento en la aplicación que estemos desarrollando,
es importante tener en cuenta no repetir operaciones innecesarias. Hay muchos casos
en los que cuando vamos de una página a otra necesitamos recalcular datos de la página
anterior, esto conlleva a repetir las mismas operaciones de cálculo una y otra vez. Para
resolver este problema, Angular nos provee con un servicio de cache para guardar datos
y reutilizarlos en cualquier momento.
Con este servicio tendremos la posibilidad de almacenar todo tipo de datos para su
posterior uso. El servicio $cacheFactory es muy fácil de utilizar y acelerará el procesamiento de la aplicación. Este servicio tiene un API muy sencilla e intuitiva que describiré
a continuación.
Para hacer uso de este servicio lo podemos inyectar en cualquier lugar como los demás
servicios de Angular, y utilizarlo para crear objetos de cache. Los datos que deseas
almacenar en la cache, son asociados a un objeto de cache que hayas creado previamente.
Estos objetos de cache tienen métodos para agregar y eliminar datos de la cache. Cuando
un objeto de cache ha sido creado con el servicio $cacheFactory, este puede ser utilizado
desde cualquier otra parte de la aplicación para obtener sus datos.
Para crear nuevos objetos de cache con el servicio $cacheFactory, solo necesitamos pasar
el nombre del nuevo objeto de cache que queremos crear como parámetro al constructor
del servicio.
1
var info = $cacheFactory('infoCache');
Después de creado el objeto de cache, tendremos una serie de métodos para ejecutar las
acciones con la cache. Ahora vamos a describir cada una de ellas y como utilizarlas.
Capítulo 4: Servicios
57
• put: Es el método que utilizaremos para depositar nuevos elementos dentro de la
cache. Acepta dos parámetros, el primero es una cadena de texto que será la llave
por la cual llamaremos a este elemento desde la cache. El segundo parámetro es
lo que necesitamos guardar en la cache, esto puede ser un arreglo, objeto, texto,
número o booleano.
• get: Este método será el encargado de extraer un elemento de la cache y devolverlo como resultado. Acepta como único parámetro el nombre del objeto que se
encuentra en la cache. Si el elemento que se está solicitando no existe, se devolverá
undefined.
• remove: Utilizaremos este método para eliminar elementos del objeto de cache.
Solo acepta un parámetro y es el nombre del elemento que queremos eliminar.
• removeAll: Cuando necesitamos eliminar todos los elementos del objeto de cache,
ejecutaremos este método sin pasar ningún parámetro.
• info: Este método devolverá información básica del objeto de cache como son el
nombre o la cantidad de elementos que posee.
• destroy: Si necesitamos eliminar el objeto de cache del servicio $cacheFactory
podremos hacerlo mediante esta acción.
Con los métodos que exponen los objetos creados por el servicio $cacheFactory podremos
realizar todas las acciones necesarias para manejar la cache de nuestra aplicación. Este
servicio además tiene otro método info, este nos devolverá un objeto con la información
de cada uno de los objetos de cache que existen en el servicio de cache. Para ver los objetos
de la cache podemos ejecutar lo siguiente.
1
console.log($cacheFactory.info());
Si revisas en la consola del navegador, podrás observar los objetos de cache, así como la
cantidad de elementos que posee cada uno de estos. Como te habrás podido dar cuenta
existen dos objetos de cache que crea angular, uno es $http y el otro es templates. Más
adelante hablare sobre estos objetos de cache, pero por ahora solo mencionar que en ellos
se guardan las peticiones que hagamos con el servicio $http y las plantillas que se cargan
en la aplicación.
Para ver el servicio en funcionamiento crearemos un ejemplo sencillo donde tendremos
dos controladores. En uno pondremos un elemento input de tipo texto donde podremos
escribir un valor para guardarlo en la cache. En el otro controlador tendremos un botón
que cargará el contenido que hayamos escrito en el controlador anterior y lo imprimirá
en la consola. Para comenzar crearemos la vista con los elementos necesarios.
Capítulo 4: Servicios
1
2
3
4
5
6
7
8
9
10
11
12
58
<body>
<div ng-controller="PrimerCtrl as uno">
<input type="text" ng-model="uno.texto">
<button ng-click="uno.guardar()">Guardar en la Cache</button>
</div>
<div ng-controller="SegundoCtrl as dos">
<button ng-click="dos.imprimir()">Imprimir desde la Cache</button>
</div>
<script src="bower_components/angular/angular.js"></script>
<script src="app.js"></script>
</body>
En la vista anterior los controladores están en el mismo nivel, de esta forma uno no puede
acceder a los elementos del otro ya que no están anidados. Ahora crearemos el primer
controlador e inyectaremos el servicio $cacheFactory para crear un objeto de cache con
el nombre de cachePrincipal.
1
2
3
4
5
6
7
8
angular.module('app', [])
.controller('PrimerCtrl', PrimerCtrl);
PrimerCtrl.$inject = ['$cacheFactory'];
function PrimerCtrl($cacheFactory){
var vm = this;
var cachePrincipal = $cacheFactory('cachePrincipal');
}
Si después de crear el objeto de cache pedimos imprimimos la información del servicio
$cacheFactory en la consola, podremos observar que hay un nuevo elemento con el
nombre que acabamos de crear.
1
2
3
4
...
var cachePrincipal = $cacheFactory('cachePrincipal');
console.log($cacheFactory.info());
...
Ahora que tenemos listo el objeto de cache, necesitamos escribir una función para
guardar el contenido del input dentro de la cache.
Capítulo 4: Servicios
1
2
3
4
5
59
...
vm.guardar = function () {
cachePrincipal.put('mensaje', vm.texto);
}
...
Con esta función, al hacer clic en el botón del primer controlador, se guardará el
contenido del input dentro de la cache en una llave con el nombre mensaje. Ahora
necesitamos crear en el segundo controlador la función que imprimirá el mensaje en
la consola.
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app')
.controller('SegundoCtrl', SegundoCtrl);
SegundoCtrl.$inject = ['$cacheFactory'];
function SegundoCtrl($cacheFactory){
var vm = this;
var cachePrincipal = $cacheFactory.get('cachePrincipal');
vm.imprimir = function () {
console.log(cachePrincipal.get('mensaje'));
}
}
Ahora que el ejemplo está completo, puedes ejecutarlo en el navegador y ver el resultado.
Si escribes un mensaje en el elemento input y lo guardas, lo podrás imprimir desde el otro
controlador. También puedes implementar funcionalidades como la de borrar, limpiar la
cache e incluso eliminar el objeto de cache ahora que ya tienes el conocimiento de cómo
hacerlo.
Aunque el ejemplo que se utilizó para demostrar su funcionamiento no tiene mucha utilidad, el servicio en si es muy útil para cuando se realizan series de cálculos repetidos en
diferentes lugares. Estos pueden ser guardados en la cache una vez y después utilizados
dentro de la aplicación donde sean necesarios. Realmente el mayor uso que le darás a
este servicio es cuando comiences a utilizarlo en conjunto con el servicio $http, ya que
ahorrara mucho tiempo de espera en peticiones a servidores remotos.
Log
Durante el proceso de desarrollo de una aplicación, con frecuencia hacemos uso de la
consola para imprimir información mediante el método log. Angular posee un servicio
Capítulo 4: Servicios
60
pensado para utilizado en reemplazo del clásico console. El servicio $log no es más que
un acceso a las principales funcionalidades de console pero con algunos cambios lo hacen
más útil en algunos casos.
Este servicio incluye cinco métodos las cuales podemos utilizar para manejar diferentes
situaciones que necesiten ser imprimidas en la consola. Los métodos son los que se
relacionan a continuación.
1.
2.
3.
4.
5.
log(): Escribe un mensaje.
info(): Escribe un mensaje de información (Icono Info)
warn(): Escribe un mensaje de cuidado (Icono Warning)
error(): Escribe un mensaje de error (Icono Error)
debug(): Escribe un mensaje se el servicio está en modo debug
A simple vista el servicio $log no tiene gran utilidad sobre el uso del objeto console pero,
este servicio nos permite configurar si deseamos o no mostrar los mensajes de debug.
Esta utilidad puede ser utilizada como remplazo del simple log del objeto console. A lo
largo de la aplicación podremos especificar mensajes de debug para que facilite el proceso
de desarrollo. Al terminar la aplicación podremos configurar el servicio para que no
muestre los mensajes de debug, de esta forma no tendremos que borrar todas las líneas
de código que imprimen mensajes en la consola.
Para configurar el servicio necesitamos inyectar el $logProvider en la configuración de
la aplicación y pasar un valor falso al método ** debugEnabled** del servicio. De esta
forma el servicio no imprimirá ninguno de los mensajes de tipo debug. Esto deberemos
hacerlo para todas las aplicaciones que vallan a pasar a producción.
1
2
3
4
5
6
7
angular.module('app', [])
.config(Log);
Log.$inject = ['$logProvider'];
function Log($logProvider) {
$logProvider.debugEnabled(false);
}
61
Capítulo 4: Servicios
Muestra del servicio $log en la consola de Google Chrome
Manejando Excepciones
Angular posee una forma muy sencilla de manejar los errores que devuelve la aplicación.
Estos errores son imprimidos a la consola por un servicio llamado $exceptionHandler,
que a su vez utiliza el servicio $log. En cada ocasión que se produzca una excepción,
angular lo procesara automáticamente a través de este servicio. Para poder llevar un
procesamiento de errores más profundo, podríamos reemplazar el servicio por uno
nuestro que cumpla los requisitos de la aplicación.
Para cambiar el funcionamiento por defecto de este servicio tendremos que redefinirlo
mediante la creación de un Factory con el nombre $exceptionHandler. Este Factory
debe devolver una función que acepte dos parámetros, el primero es la excepción y el
segundo la causa. Para ver su funcionamiento vamos a crear el servicio, pero utilizaremos
el servicio $log con su método debug. De esta forma cuando la aplicación entre en
producción, los errores no sean imprimidos en la consola.
Lo primero que necesitamos hacer es definir un Factory con el nombre exceptionHandler
que devuelva una función y acepte los parámetros del error. Inyectamos el servicio
$log y hacemos debug en la consola de cualquier excepción que no haya sido manejada
previamente. Después en el controlador lanzamos un error para simular el uso del nuevo
servicio.
1
2
3
4
5
6
7
8
9
10
angular.module('app', [])
.factory('$exceptionHandler', ExceptionHandler)
.controller('AppCtrl', AppCtrl);
ExceptionHandler.$inject = ['$log'];
function ExceptionHandler($log){
return function (exception, cause) {
$log.debug.apply($log, arguments);
}
}
Capítulo 4: Servicios
11
12
13
14
15
62
AppCtrl.$inject = ['$scope'];
function AppCtrl($scope) {
throw new Error('Error grave.');
}
Como habrás podido observar los mensajes de la consola ahora salen a través del
servicio que creamos anteriormente. Haciendo uso del método debug del servicio $log,
podremos deshabilitar que muestre los errores en la consola cuando estamos en modo
de producción. El ejemplo anterior no tiene gran utilidad, pero haciendo un correcto uso
del servicio podremos enviar los errores a una base de datos para analizarlos.
Retrasando funcionalidades
En muchas ocasiones necesitamos retrasar la ejecución de alguna funcionalidad en la
aplicación. Usualmente en Javascript este comportamiento es realizado mediante la
función setTimeout. Para este tipo de necesidades, Angular dispone de un servicio llamado
$timeout. En esencia este servicio realizará lo que estamos acostumbrados a hacer con
setTimeout pero desde el punto de vista del framework.
El servicio $timeout tiene algunas diferencias con respecto al nativo de javascript y es lo
que lo hará más útil al utilizarlo en el framework sobre el método nativo. Para comenzar
la función que se retrasará será rodeada por un bloque try/catch, cualquier excepción que
sea lanzada será delegada al servicio $exceptionHandler que explicamos anteriormente.
Lo que realmente hace más útil este servicio sobre el nativo de Javascript es que está
basado en promesas. Siendo así tendremos una serie de funcionalidades que serían muy
útil a la hora de implementarlo en la aplicación. Otra de las capacidades es que este está
relacionado directamente con el ciclo digest de la aplicación. Esto facilita que las acciones
que realicemos con este servicio serán interpretadas por Angular correctamente.
El primer parámetro que pasaremos al constructor será la función que necesitamos
retrasar en su ejecución. El segundo parámetro es el tiempo que será retrasado, este
tiempo es especificado en milisegundos. El tercer parámetro es un valor booleano que
de ser false obviará el chequeo de los modelos, de lo contrario invocara $applay. A
partir del cuarto parámetro en adelante serán pasados como parámetros a la función
que especificamos como primer parámetro.
Cuando creamos un nuevo objeto con el constructor de $timeout y lo guardamos en una
variable, este puede ser cancelado antes de que se ejecute la función. Para cancelarlo
el servicio posee un método cancel donde pasaremos como parámetro la promesa que
necesitamos cancelar.
Ahora veremos un ejemplo completo del uso del servicio $timeout. Primero crearemos
una vista con un botón que nos permita cancelar el $timeout desde la vista. En el
Capítulo 4: Servicios
63
controlador crearemos un objeto timeout con el nombre retraso el cual ejecutara la
función Accion después de tres segundos. A esta función le pasaremos dos parámetros
adicionales que imprimiremos por la consola. Luego de que la promesa se ha ejecutado
imprimiremos en la consola el mensaje de devuelto por la ejecución de la función. Si
cancelamos el temporizador, se disparará la acción catch de la promesa y por ultimo
definimos el método cancelar para poder ejecutarlo en la vista.
Vista
1
2
3
<body ng-controller="AppCtrl as vm">
<button ng-click="vm.cancelar()">Cancelar</button>
</body>
Controlador
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
angular.module('app', [])
.controller('AppCtrl', AppCtrl);
AppCtrl.$inject = ['$timeout'];
function AppCtrl($timeout) {
var vm = this;
var retraso = $timeout(Accion, 3000, true, 'Uno', 'Dos');
function Accion(param1, param2) {
console.log('Ejecutado después de dos segundos.');
console.log('Parámetros: ', param1, param2);
return 'Mensaje devuelto por el temporizador.';
}
retraso.then(function (msg) {
console.log(msg);
console.log('Retraso finalizado');
});
retraso.catch(function () {
console.log('Retraso cancelado.');
})
vm.cancelar = function () {
$timeout.cancel(retraso);
}
}
Capítulo 4: Servicios
64
Con este ejemplo hemos podido observar todas las características de este servicio y lo
que lo hace más útil sobre el clásico setTimeout de Javascript.
Creando repeticiones con intervalos
Cuando queremos que una tarea específica se ejecute cada cierto tiempo, usualmente
utilizamos la función serInterval de Javascript. Al igual que para setTimeout AngularJS
dispone de un servicio que te ayudará a ejecutar tareas repetidamente cada un tiempo
específico. El servicio $interval es muy similar al servicio $timeout.
El servicio $timeout se diferencia en $interval solo en que este realizará la tarea una
cantidad especificada de ocasiones. Los dos servicios utilizan exactamente el mismo
API para trabajar con ellos, el único cambio es a la hora de especificar la cantidad de
veces que se ejecutara. La cantidad de veces que se repetirá la tarea se especificará como
tercer parámetro en la invocación del servicio. Para ver un ejemplo crearemos un conteo
regresivo de 5 segundos e imprimiremos en la consola cada uno de los segundos. Al
terminar el conteo regresivo enviaremos una alerta al usuario.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
angular.module('app', [])
.controller('AppCtrl', AppCtrl);
AppCtrl.$inject = ['$scope', '$interval'];
function AppCtrl($scope, $interval){
var conteo = $interval(imprimirConteo, 1000, 5);
var i = 4;
function imprimirConteo() {
if ( i > 0 ) {
console.log('Quedan ' + i + ' segundos.');
i--;
} else {
console.log('Conteo finalizado.');
}
}
conteo.then(function () {
alert('Ya han pasado 5 segundos.');
});
}
Es importante mencionar que los intervalos creados por este servicio no son cancelados
automáticamente después de finalizados. Estos deben ser cancelados de forma manual
una vez que hayan terminado o antes de que el *$scope** sea destruido. Para lograrlo lo
primero que necesitamos es cancelarlo después que haya terminado.
65
Capítulo 4: Servicios
1
2
3
4
conteo.then(function () {
alert('Ya han pasado 5 segundos.');
$interval.cancel(conteo);
});
La otra precaución que necesitamos tomar es para cuando el intervalo aún no ha
finalizado. Para ello podremos escuchar el evento $destroy del $scope y entonces cancelar
el intervalo cuando este ocurra.
1
2
3
$scope.$on('$destroy', function () {
$interval.cancel(conteo);
});
De esta forma evitaremos problemas de rendimiento al cambiar desde un $scope hacia
otro.
Anotaciones en el DOM
Como habrás podido notar a lo largo del desarrollo con el framework, en el DOM
de la aplicación, Angular escribe una serie de anotaciones que son innecesarias para
el funcionamiento de esta. Las anotaciones se muestran en la imagen que aparece a
continuación.
Anotaciones en el DOM
En la nueva versión del framework existe una vía para eliminar esas anotaciones.
Para lograrlo necesitamos configurar el servicio $compiler en la configuración de la
aplicación.
66
Capítulo 4: Servicios
1
2
3
4
angular.module('app', [])
.config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);
Una vez configurado podremos observar que Angular ha eliminado las anotaciones como
se muestra en la imagen a continuación.
Anotaciones en el DOM
Esto implica un ligero aumento en el rendimiento general de la aplicación ya que Angular
no tiene que escribir todas esas anotaciones en el DOM para el funcionamiento de la
aplicación.
Capítulo 5: Peticiones al servidor
Hasta ahora podemos comprobar que con AngularJS podemos crear una aplicación
completa del lado del cliente, pero solo con la información que cargamos al inicio.
Claro que con solo el lado del cliente no se puede lograr muchas cosas. Por este motivo
AngularJS trae un servicio que nos ayudará a intercambiar información con el servidor.
Otro de los servicios del núcleo del framework es $http que será el encargado de
interactuar con el servidor remoto mediante el objeto XMLHttpRequest.
Este servicio solo aceptará un argumento que será un objeto de configuración para
dependiendo de este, generar las peticiones al servidor remoto. Como comentamos en
el capítulo anterior este servicio siempre devolverá una promesa. Lo que quiere decir
que podemos usar el método then para manejar la respuesta. Pasándole la primera
función como parámetro para si la promesa ha sido resuelta y una segunda para sí ha
sido rechazada. Estos dos métodos reciben como parámetro un objeto que representa
la respuesta. Además del método then $http nos proporciona dos métodos de acceso
rápido para gestionar la promesa. El primero será el método success() y el segundo será
error().
Si el código de la respuesta es un número entre 200 y 299 la respuesta se se
considerará como resuelta, de lo contrario será tratada como error y el método
error() será ejecutado.
Los parámetros que recibirán los métodos success() y error() serán data, status, headers,
config, statusText. Mientras que el método then solo recibirá un objeto de respuesta que
une el contenido anterior.
data Puede ser de tipo objeto o texto. Son los datos retornados por el servidor después
de haber sido transformados por las funciones de transformación.
status
De tipo número. Será el código de la respuesta que ha enviado el servidor.
headers
Es una función para obtener las cabeceras de la respuesta.
config
De tipo objeto. Es el objeto de configuración que fue usado para generar la petición.
statusText
Cadena de texto con el mensaje de estado HTTP de la respuesta.
67
Capítulo 5: Peticiones al servidor
68
Los parámetros antes mencionados son los que recibirán los métodos success y error.
No es necesario usarlos todos, por lo general solo se usan los dos primeros. Los datos
de la respuesta y el código para tomar acciones en la aplicación correspondiente a lo
recibido.
Objeto de configuración del servicio $http
Como he dicho antes el servicio $http obtiene un objeto de configuración ahora describiré cuales son las propiedades que puede tener este objeto.
method
Cadena de texto que describe el método HTTP que se usará para la petición (‘GET’,
‘POST’, ‘PUT’, ‘PATCH’, ‘DELETE’, etc.).
url
Dirección absoluta o relativa a la que se hará la petición.
params
Objeto de llaves: valor que será enviado después de la url (?llave=valor&llave2=valor2).
Si el valor no es una cadena de texto será convertido a JSON.
data Cadena de texto u objeto que será enviado como datos de la petición.
headers
Objeto de cadenas de texto, o funciones que devuelven cadenas de texto que representen cabeceras HTTP para ser enviadas al servidor. Si alguna de las funciones
devuelve null esa cabecera no será enviada.
xsrfHeaderName
Cadena de texto con el nombre de la cabecera HTTP que será utilizada para el token
XSRF.
xsrfCookieName
Cadena de texto con el nombre de la cookie que contiene el token XSRF.
transformRequest
Función de transformación o arreglo de funciones de transformación. Estas funciones reciben el cuerpo de la petición y las cabeceras como parámetro y las devuelven
transformadas.
transformResponse
El mismo funcionamiento que transformRequest pero para transformar las respuestas.
Capítulo 5: Peticiones al servidor
69
cache
Si recibe un valor verdadero se hará cache de la petición, si es una instancia del
servicio $cacheFactory esta será usada para hacer cache de la petición.
timeout
Tiempo de espera en mili segundos o una promesa que aborte la petición cuando
se resuelva.
withCredentials
Valor verdadero o falso para ser indicado en el objeto XHR
responseType
Cadena de texto con el tipo de respuesta solicitada.
Veamos un ejemplo de cómo se utiliza el servicio $http. Para este ejemplo he creado un
archivo JSON que simulará una respuesta del servidor.
Archivo: App/usuarios.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[{
"nombre": "Maikel",
"apellidos": "Rivero Dorta",
"email": "[email protected]",
"lenguajes": ["en", "es"]
},
{
"nombre": "john",
"apellidos": "Doe",
"email": "[email protected]",
"lenguajes": ["en"]
},
{
"nombre": "Jane",
"apellidos": "Doe",
"email": "[email protected]",
"lenguajes": ["en","es"]
}]
Ahora utilizaremos un controlador para hacer una petición a este archivo y mostrar el
resultado en la vista.
Capítulo 5: Peticiones al servidor
70
Archivo: App/js/Controllers/UsersCtrl.js
1
2
3
4
5
6
7
8
9
10
11
angular.module('miApp')
.controller('UsersCtrl', ['$scope', '$http', function ($scope, $http) {
var usuarios = $http({
method: 'GET',
url: 'usuarios.json'
}).success(function(data, status){
$scope.usuarios = data;
}).error(function(data, status){
console.log(data, status);
});
}])
El ejemplo anterior si lo ejecutas fuera de un servidor HTTP no te va a funcionar, si estás
trabajando local te recomiendo que uses los AMP osea para Mac MAMP para Windows
WAMP y para Linux LAMP. Estas aplicaciones son servidores muy fáciles de usar para
desarrollo local y vienen pre-cargados con servicio HTTP y base de datos MySQL. Si no
tienes experiencia con servidores, los anteriores son muy fáciles de hacerlos funcionar,
en su web explican paso a paso como utilizarlos. Otras de las opciones que puedes utilizar
es crear tu propio servidor con node.js o instalar de forma dedicada Apache o Nginx en
tu pc.
En el ejemplo anterior configuramos el servicio $http para hacer una petición de tipo
GET al archivo usuarios.json. Cuando su promesa ha sido resuelta ejecutará el método
success, donde asignamos la respuesta al $scope para hacerlos disponible en la vista. Si
la promesa no se resuelve se ejecutará el método error donde enviamos la respuesta a la
consola. Veamos el código para la vista.
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div class="container" ng-controller="UsersCtrl">
<hr>
<div ng-repeat="usuario in usuarios">
<p><strong>Nombre:</strong> {{ usuario.nombre }}</p>
<p><strong>Apellidos:</strong> {{ usuario.apellidos }}</p>
<p><strong>Email:</strong> {{ usuario.email }}</p>
<p><strong>Lenguajes:</strong> |
<span ng-repeat="lenguaje in usuario.lenguajes">
{{ lenguaje }} |
</span></p>
<hr>
Capítulo 5: Peticiones al servidor
13
14
15
16
17
18
71
</div>
</div>
<script src="lib/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Controllers/UsersCtrl.js"></script>
</body>
En el código anterior no hay nada nuevo, simplemente usamos la directiva ng-repeat
para mostrar los usuarios y sus datos.
De esta forma comenzamos a hacer peticiones al servidor. En esta ocasión lo hemos
hecho a un archivo en nuestro propio servidor, pero esta es la vía más rápida de hacer
las peticiones. En la propiedad url del objeto de configuración que le pasamos al servicio
http es donde decidiremos a donde haremos la petición. Existen varias API públicas con
las que podrías usar este servicio. Ejemplo de estas son Twitter, Github, IMDB, todas
estas tienen su ayuda donde explican su funcionamiento.
Métodos de acceso rápido
Este servicio nos brinda varios métodos de acceso rápido para ejecutar acciones con los
métodos HTTP.
$http.get(url, config)
Este método realiza una petición get a la url que recibirá como primer parámetro
y en caso de que necesitemos especificar alguna otra configuración lo recibirá
como segundo parámetro, pero no es necesario. Este método reduciría el código
del ejemplo anterior a
1
2
3
4
5
6
$http.get('usuarios.json')
.success(function(data, status){
$scope.usuarios = data;
}).error(function(data, status){
console.log(data, status);
});
$http.head(url, config)
Este método nos permite hacer una petición head a la url especificada. Como
segundo parámetro recibe un objeto de configuración.
Capítulo 5: Peticiones al servidor
72
$http.post(url, data, config)
Este método es el que por lo general usamos para enviar peticiones al servidor
con un cuerpo, ya sea para enviar datos de identificación, o para la creación de
nuevos recursos en el servidor. Se creará una petición de tipo post a la url que se
pasará como primer parámetro, en el segundo parámetro pasaremos el cuerpo de
la petición, y opcional como tercer parámetro el objeto de configuración.
$http.put(url, data, config)
El método put es usado para hacer las peticiones de actualización, los parámetros
que recibe son los mismos que las peticiones de tipo post.
$http.delete(url, config)
Con este método podremos realizar una petición de tipo delete para eliminar
recursos en el servidor. El primero parámetro es la url a la que se realizará la
petición, por lo general sería algo así www.api.com/contactos/52 donde se eliminará
el contacto 52 si el servidor tiene implementado este tipo de peticiones. El segundo
parámetro es opcional, un objeto de configuración.
$http.patch(url, data, config)
Realiza una petición de tipo patch esencialmente es como el método put.
$http.jsonp(url, config)
Realiza una petición tipo jsonp al servidor donde el nombre del callback debe ser
la cadena de texto JSON_CALLBACK. El primer parámetro es la url que especifica
la dirección a donde se hará la petición, el segundo parámetro es un objeto de
configuración.
Provider del servicio $http
El servicio $http está registrado como provider lo que quiere decir que puede ser
configurado en el proceso de creación del módulo. En esta configuración podemos
definir varios parámetros para que nuestra aplicación siempre que use el servicio $http
los tenga disponibles. En esta configuración podemos cambiar las cabeceras para cada
tipo de petición y poner otras cabeceras que necesite enviar la aplicación a la hora de
hacer la petición al servidor. Supongamos que necesitamos enviar el Token CSRF en
todas las peticiones, quedaría de esta forma.
Capítulo 5: Peticiones al servidor
73
Archivo: App/Config/http.js
1
2
3
4
angular.module('miApp').
config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common.CSRF_TOKEN = "a2d10a3211b415832791a6bc6";
}]);
De esta forma el token CSRF será enviado en todas las peticiones que hagamos al
servidor. En el ejemplo anterior he añadido el token al objeto common pero si solo
quisiéramos enviar el token en las peticiones post, put, path o delete podríamos hacerlo
escribiéndolo en cada método por individual de la siguiente forma
Archivo: App/Config/http.js
1
2
3
4
angular.module('miApp').
config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.post.CSRF_TOKEN = "a2d10a3211b415832791a6bc6";
}]);
El servicio $http permite transformar las peticiones y las respuestas antes de ser entregadas. Automáticamente $http siempre las transforma, las peticiones que se hacen al
servidor y tengan una propiedad data en el objeto de configuración y esta sea un objeto,
el servicio serializa automáticamente el objeto data en formato JSON para ser entregado
al servidor. En cuanto a las respuestas, si es detectado que el contenido es en formato
JSON es deserializado a un objeto o arreglo Javascript.
La configuración del servicio permite incluir propias funciones de transformación para
si necesitas hacer cambios específicos a tus peticiones o respuestas. Veamos un ejemplo
de cómo podemos realizar estas transformaciones para utilizarlas en nuestro favor.
Archivo: App/Config/respTransformer
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('miApp').
config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.transformResponse.push(function(data){
data.push({
"nombre": "Junior",
"apellidos": "Doe",
"email": "[email protected]",
"lenguajes": ["es"]
});
return data;
})
}]);
Capítulo 5: Peticiones al servidor
74
Si ese archivo de configuración lo cargamos en el archivo index.html del ejemplo anterior
podremos observar que el cuándo la respuesta es entregada al controlador y este a la vista
ya incluye el nuevo usuario que escribimos en el archivo de configuración. De esta misma
forma se escriben los transformadores de las peticiones.
Además de esta flexibilidad de transformar las peticiones y las respuestas el servicio
$http, nos brinda otra vía de interceptar las respuestas antes de ser entregadas a la aplicación y las peticiones antes de ser enviadas al servidor. Para entender correctamente debes
haber entendido el funcionamiento del servicio $q y las promesas. Los Interceptors
serán servicios factory que serán añadidos al arreglo $httpProvider.interceptors. Estos
serán llamados y se les inyectará sus dependencias en caso de necesitar alguna y devolverá
el interceptor. Veamos otro ejemplo de cómo enviar el token CSRF en todas las peticiones
o agregar un usuario nuevo a la respuesta, pero esta vez con un interceptor.
Archivo: App/Config/reqInterceptor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
angular.module('miApp')
.factory('reqInterceptor', [function () {
var interceptor = {
request: function(config){
config.headers['CSRF_TOKEN'] = 'a2d10a3211b415832791a6bc6';
return config;
},
response: function(response){
response.data.push({
nombre: 'Lorem',
apellidos: 'Ipsum Dolor',
email: '[email protected]',
lenguajes: ['en', 'es']
})
return response;
}
}
return interceptor;
}])
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('reqInterceptor');
}])
En el ejemplo anterior he declarado el factory reqInterceptor que devolverá un objeto
con dos propiedades, una es request y la otra es response. La primera recibirá como
parámetro el objeto de configuración del servicio $http antes de enviar la petición.
Este lo usamos para agregarle la cabecera CSRF_TOKEN y siempre tendremos que
Capítulo 5: Peticiones al servidor
75
devolver el objeto de configuración o un objeto de configuración nuevo. La segunda
recibirá como parámetro un objeto de respuesta, que es el mismo que recibe el método
success del servicio $http pero antes de ser entregado a este. Teniendo la posibilidad
de modificarlo he añadido un nuevo usuario a la propiedad data de la respuesta para
ser entregado en el controlador a la vista. En este caso también tendremos que devolver
ese objeto de respuesta después de haberlo modificado. Por último, se agrega el servicio
reqInterceptor al arreglo de interceptors de la configuración de $httpProvider.
Para comprobar que funcionen correctamente podemos ir a las herramientas de desarrollo del navegador y en la pestaña Red buscamos la petición que se hace a usuarios.json
y en los headers de la petición podemos observar que se a añadido el CSRF_TOKEN:a2d10a3211b415832791a6bc6 y que al retornar la respuesta tenemos el nuevo
usuario Lorem en la lista que muestra la vista si has incluido este archivo en el index.html del ejemplo anterior.
Como habrás podido observar el servicio $http es muy útil y muy flexible si necesitamos
hacer peticiones al servidor en la aplicación.
Capítulo 6: Directivas
Como hemos podido observar hasta ahora, las directivas son una parte importante de
AngularJS. Con ellas podemos manipular el DOM de una forma muy fácil y lograr
bloques de código que de otra forma sería un poco complicado. Otra de las ventajas de
las directivas es que nos permite reutilizar partes de la aplicación sin tener que volver a
escribir el mismo código en diferentes partes. Las directivas no se rigen solo a atributos
de los elementos HTML, estas pueden ser elementos e incluso clases CSS.
Cuidado
Las directivas declaradas como elementos puede que no funcionen en Internet
Explorar, solo funcionarán en navegadores como Google Chrome, Safari, Firefox, Opera y otros. Por este motivo debes restringir tus directivas a clases o
atributos.
Angular trae en su núcleo definido una gran cantidad de directivas que te ayudarán a
desarrollar tu aplicación con un código más limpio y efectivo. Pero también te permite
declarar tus propias directivas que sea más específicas para tu aplicación. Hasta ahora he
explicado el funcionamiento de algunas a lo largo de los ejemplos. Antes de comenzar
a crear las directivas específicas de la aplicación veamos otras de las que vienen en el
núcleo de Angular.
ng-class
AngularJS nos permite cambiar o añadir clases a los elementos HTML. Definiendo
una expresión que represente las clases que serían añadidas o removidas del elemento.
Este comportamiento lo realiza mediante la directiva ng-class. Esta directiva funciona
de tres formas diferentes dependiendo del resultado de la evaluación de la expresión
proporcionada como valor.
La primera es si la expresión es evaluada a una cadena de texto, el texto debe ser un
nombre de clase o varios separados por espacios.
76
77
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<head>
<meta charset="UTF-8">
<title>ng-class</title>
<style>
.flotar {float: right; padding: 0 10px;}
.fondoRojo {background-color: red; }
.bordesRedondeados {border: 2px solid black; border-radius: 10px;
}
</style>
<script src="../lib/angular.js"></script>
</head>
<body ng-app="miApp" ng-controller="miCtrl">
<div ng-class="generarClases()">
<h1>Ejemplo de los usos de ng-class</h1>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var clases = ['flotar', 'fondoRojo', 'bordesRedondeados'];
$scope.generarClases = function(){
return clases.join(' ');
}
}])
</script>
</body>
La segunda es si la expresión es evaluada a un arreglo donde cada uno de sus elementos
sea una cadena de texto de uno o varias clases separadas por espacios.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<head>
<meta charset="UTF-8">
<title>ng-class</title>
<style>
.flotar {float: right; padding: 0 10px;}
.fondoRojo {background-color: red; }
.bordesRedondeados {border: 2px solid black; border-radius: 10px;
</style>
<script src="../lib/angular.js"></script>
</head>
<body ng-app="miApp" ng-controller="miCtrl">
<div ng-class="generarClases()">
<h1>Ejemplo de los usos de ng-class</h1>
</div>
}
78
Capítulo 6: Directivas
15
16
17
18
19
20
21
22
23
24
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var clases = ['flotar', 'fondoRojo bordesRedondeados'];
$scope.generarClases = function(){
return clases;
}
}])
</script>
</body>
La tercera es la más compleja ya que podemos tenemos la opción de poner condiciones.
Esta forma es si la expresión se evalúa a un objeto, donde por cada par llave-valor con
un valor verdadero, la llave será usada como clase.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<head>
<meta charset="UTF-8">
<title>Test</title>
<style>
.flotar {float: right; padding: 0 10px;}
.fondoRojo {background-color: red; }
.bordesRedondeados {border: 2px solid black; border-radius: 10px;
}
</style>
<script src="../lib/angular.js"></script>
</head>
<body ng-app="miApp">
<input type="checkbox" ng-model="flotar">Flotar
<input type="checkbox" ng-model="fondoRojo">Fondo Rojo
<input type="checkbox" ng-model="bordesRedondeados">Bordes Redondeados
<div ng-class="{
'flotar':flotar,
'fondoRojo': fondoRojo,
'bordesRedondeados': bordesRedondeados}">
<h1>Ejemplo de los usos de ng-class</h1>
</div>
<script>
angular.module('miApp', []);
</script>
</body>
En el ejemplo anterior hemos hecho uso de ng-model para obtener un valor verdadero o
falso proporcionado por el input. Como han podido observar esta última forma de usar
la directiva nos da muchas posibilidades para obtener resultados de una forma muy fácil.
Capítulo 6: Directivas
79
Existen otras tres directivas para alterar las clases de los elementos, dos de ellas son ngclass-even y ng-class-odd, estas funcionan en conjunto con la directiva ng-repeat que
trataremos más adelante. Las dos directivas funcionan de la misma forma que ng-class
pero solo tienen efecto en las filas pares e impares de ng-repeat. Ahora hablaremos de la
tercera.
ng-style
Esta directiva no será muy usada ya que con ng-class podemos lograr lo que con esta.
ng-style permite que cambies el estilo del elemento condicionalmente. Digo que no será
muy usada por que por lo general no usamos el atributo style de los elementos HTML
regularmente, en su lugar usamos clases definidas en nuestros archivos de css.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body ng-app="miApp" ng-controller="miCtrl">
<div ng-style="clases">
<h1>Ejemplo de los usos de ng-class</h1>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var clases = {
'float': 'right',
'padding': '0 10px',
'background-color': 'red',
'border': '2px solid black',
'border-radius': '10px'
};
$scope.clases = clases;
}]);
</script>
</body>
ng-list
En ocasiones en las aplicaciones necesitamos obtener una lista indicada por el usuario,
un ejemplo de esto es la lista de etiquetas o categorías de un post en un blog. Para estos
propósitos AngularJS posee la directiva ng-list.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
80
<body ng-app="miApp" ng-controller="miCtrl">
Etiquetas: <input ng-model="etiquetas" ng-list><br>
Debug: {{ etiquetas }}
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function($scope) {
$scope.etiquetas = ['Actualidad', 'Finanzas', 'Tecnología'];
}]);
</script>
</body>
ng-non-bindable
En caso de que en la aplicación quisiéramos que AngularJS no ejecute ninguna de sus
acciones o no evalué ninguna expresión podemos usar la directiva ng-non-bindable.
Cuando AngularJS encuentre esta directiva en el código de la aplicación, pasará por alto
ese bloque y continuara con la ejecución de la aplicación.
1
2
3
4
5
6
7
8
9
10
<body ng-app="miApp" ng-controller="miCtrl">
<div>{{ mensaje }}</div>
<div ng-non-bindable>{{ mensaje }}</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.mensaje = 'Hola desde el controlador.';
}])
</script>
</body>
ng-repeat
Una de las directivas más importantes de AngularJS viene a ser ng-repeat que con su ventaja de repetir una plantilla por cada elemento de una colección. Este comportamiento
nos da una gran ventaja a la hora de hacer listas o tablas. Hay varias utilidades que nos
brinda el ng-repeat, las iré describiendo a continuación.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
81
<body ng-app="miApp" ng-controller="miCtrl">
<div class="container">
Listado de compra.
<ul>
<li ng-repeat="compra in musica">
<strong>Artista:</strong> {{ compra.artista }}
<strong>CD</strong> {{ compra.cd }}
</li>
</ul>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var musica = [
{artista: 'U2', cd: 'Songs of Innocence'},
{artista: 'Afrojack', cd: 'Forget the World'},
{artista: 'Alexandra Stan', cd: 'Unlocked'},
{artista: 'Avicii', cd: 'True'},
{artista: 'Dash Berlin', cd: 'The New Daylight'},
{artista: 'David Guetta', cd: 'Lovers on the Sun'},
{artista: 'Echosmith', cd: 'Talking Dreams'},
{artista: 'La Roux', cd: ' Trouble in paradise'}
];
$scope.musica = musica;
}]);
</script>
</body>
En el controlador se ha creado un arreglo de elementos para ser posteriormente asignado
al $scope y hacerlos disponibles en la vista. Por otra parte, en la vista se ha utilizado una
lista desordenada para mostrar los elementos. La directiva ng-repeat está situada en el
elemento <li> ya que será el que queremos que se repita por cada elemento de la lista de
compra.
Esta directiva evalúa su valor de dos formas. La primera es la que hemos usado en el
ejemplo anterior. Se evalúa la expresión de la siguiente forma variable in colección
donde la variable es la que tomará un valor de la colección en cada vez que se repita
y valdrá solo hasta el final de ese ciclo donde comenzará nuevamente con el siguiente
valor de la colección.
La segunda forma es en esencia igual solo que esta vez podremos obtener también la llave
y no solo el valor (llave, valor) in colección como veremos en el siguiente ejemplo.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
82
<body ng-app="miApp" ng-controller="miCtrl">
<div class="container">
<h3>U2 - Songs of Innocence</h3>
<table>
<thead> <tr> <th>Titulo</th><th>Duración</th> </tr> </thead>
<tbody>
<tr ng-repeat="(titulo, tiempo) in playlist">
<td>{{ titulo }}</td><td>{{ tiempo }}</td>
</tr>
</tbody>
</table>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var playlist = {
'The Miracle (Of Joey Ramone)': '4:15',
'Raised By Wolves': '4:12',
'Every Breaking Wave': '3:59',
'Cedarwood Road': '3:46',
'California (There Is No End to Love)': '5:19',
'Sleep Like a Baby Tonight': '3:14',
'Song for Someone': '4:05',
'This Is Where You Can Reach Me Now': '4:25',
'Iris (Hold Me Close)': '5:01',
'The Troubles': '5:05',
'Volcano': '4:45'
};
$scope.playlist = playlist;
}]);
</script>
</body>
Existen otros parámetros que puede ser incluido en la expresión que le pasamos a la
directiva. Este es muy útil para buscar dentro de listas, filter nos permite filtrar el
contenido de la colección mediante una variable.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
83
<body ng-app="miApp" ng-controller="miCtrl">
<div class="container">
<h1>Biblioteca de CD's.</h1>
<input type="search" ng-model="buscar">
<ul>
<li ng-repeat="cd in cds | filter:buscar">{{ cd }}</li>
</ul>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var cds = [
'Songs of Innocence',
'Forget the World',
'Unlocked', 'True',
'The New Daylight',
'Lovers on the Sun',
'Talking Dreams',
'Trouble in paradise'
];
$scope.cds = cds;
}]);
</script>
</body>
Para especificar el filtro debemos separarlo con el caracter | y a continuación la palabra
filter seguido de : y el nombre de la variable que servirá de filtro. En el ejemplo anterior
usamos un elemento input para hacer la búsqueda dinámica en el navegador.
La directiva ng-repeat además nos provee de una serie de variables útiles que nos pueden
servir muy bien para realizar varias operaciones con la lista. Estas variables solo estarán
disponibles dentro del ciclo que recorre ng-repeat
$index
Nos devuelve el número de la iteración por la que vamos en ese momento, comienza
en 0.
$first
Tiene valor verdadero si es el primer elemento del ciclo.
$middle
Tiene valor verdadero si el elemento no es ni el primero ni el último del ciclo.
Capítulo 6: Directivas
84
$last Tiene valor verdadero si el elemento es el último del ciclo.
$even
Tiene valor verdadero si la variable $index tiene un valor par.
$odd
Tiene valor verdadero si la variable $index tiene un valor impar.
A continuación, un ejemplo combinando todas las particularidades de ng-repeat unido
a otras directivas ya estudiadas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<head>
<meta charset="UTF-8">
<title>ng-repeat</title>
<script src="../lib/angular.js"></script>
<style>
.container {width: 600px; margin: auto;}
ul {list-style: none; width: 100%; padding: 0; margin: 0;}
input {width: 100%; padding: 5px;}
.primera {background-color: #FF7676; }
.medio {background-color: #4AB300;}
.ultima {background-color: #43539C;}
.par { text-decoration: underline;}
.impar {font-weight: bold;}
</style>
</head>
<body ng-app="miApp" ng-controller="miCtrl">
<div class="container">
<h1>Biblioteca de CD's.</h1>
<input type="search" ng-model="buscar" placeholder="Buscar...">
<ul>
<li ng-repeat="cd in cds | filter:buscar"
ng-class="{
'primera':$first,
'medio':$middle,
'ultima':$last,
'par':$even,
'impar':$odd}">
{{ $index+1 }} - {{ cd }}
</li>
</ul>
</div>
Capítulo 6: Directivas
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
85
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
var cds = [
'Songs of Innocence',
'Forget the World',
'Unlocked', 'True',
'The New Daylight',
'Lovers on the Sun',
'Talking Dreams',
'Trouble in paradise'
];
$scope.cds = cds;
}]);
</script>
</body>
Como has podido observar en el ejemplo anterior se combinan todas las bondades de la
directiva ng-repeat con las de ng-class. Pero eso no es todo, hay un sin fin de utilidades
para esta directiva, ya lo verás en próximos capítulos.
ng-if
Esta directiva basada en la evaluación de una expresión que si resulta un valor falso
elimina por completo los elementos del DOM, de lo contrario inserta un clon de los
elementos. Su uso es parecido a la de ng-show/ng-hide pero con la diferencia de que los
elementos no son alterados con la propiedad display de css.
1
2
3
4
5
6
7
8
9
10
11
12
<body ng-controller="miCtrl">
<div class="container">
<input type="checkbox" ng-model="mostrar"> Mostrar bienvenida.
<p ng-if="mostrar">Bienvenido al mundo de <strong>AngularJS</strong></p>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.mostrar = true;
}])
</script>
</body>
Capítulo 6: Directivas
86
Algo a tener en cuenta es el comportamiento de que siempre es insertado un clon del
elemento y no el que existía antes de ser eliminado. Si el elemento había sido modificado
con jQuery este perderá las modificaciones
ng-include
A la hora de hacer la maqueta de la aplicación quizás necesites tener parte de la
aplicación que se usarán en varias plantillas. La directiva ng-include puede solucionarte
ese problema permitiendo que extraigas la porción de la plantilla que será reutilizada a
un archivo diferente y luego con el uso de la directiva, incluirla en varios lugares de tu
aplicación. La directiva debe recibir el URL de la plantilla para ser cargada.
Restricciones
Hay que tener en cuenta que las plantillas deben estar en el mismo dominio
y protocolo que nuestra aplicación, para cargar plantillas fuera de nuestro
dominio necesitamos registrarlo en la lista blanca configurando en el servicio
$sce.getTrustedResourceUrl. Además, hay que tener en cuenta las restricciones
del navegador con respecto a los recursos compartidos a través de dominios. En
todos los navegadores no funciona esta directiva cuando se llama a las plantillas
desde *file://*, será mejor ejecutar la aplicación desde un servidor para obtener
los resultados.
Además, esta directiva permite el uso de otras dos propiedades. Una es una expresión
que será evaluada cuando la plantilla sea cargada onload=”“ y la otra es autoScroll=”“
que si no está presente deshabilita el scrolling, si está presente, pero sin valor habilita el
scrolling o habilita el scrolling si la expresión es evaluada a verdadero.
ng-cloak
Esta directiva es usada para prevenir que Angular muestre partes de la plantilla sin ser
compiladas previamente. Puede ser aplicada al elemento body, pero se trata de una mala
práctica ya que es bueno que la aplicación vaya siendo visualizada a medida que se vaya
cargando. Para ello se pueden usar varias veces la directiva para reducir la cantidad de
contenido que no será mostrado hasta que no sea compilado por Angular. Esta directiva
hace su función mediante CSS y la propiedad display de cada elemento donde se aplica.
Esta directiva es solo el atributo, no necesita valor.
Debido a que trabaja con CSS sería necesario que el framework sea incluido al comienzo
de la página o de lo contrario no existirán las clases CSS que este utiliza para ocultar los
contenidos. De otra forma la clase puede ser incluida en un archivo CSS y el framework al
final de la página. La clase que deberíamos escribir en nuestro archivo CSS es la siguiente.
Capítulo 6: Directivas
1
2
3
87
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
Estas directivas que se han expuesto hasta ahora pueden ser usadas en varios elementos.
Ahora trataremos sobre directivas específicas de algunos elementos HTML.
ng-href
Cuando utilizamos la etiqueta <a> tenemos un atributo href que nos permite definir
la referencia del vínculo a donde se quiere llegar. En AngularJS quizás necesites usar
vínculos como http://www.miapp.com/perfiles/{{ usuario.id }}. En esencia funcionarán, pero
solo si el usuario da clic después de que Angular haya tenido la oportunidad de cambiar
la sintaxis de la plantilla por la id del usuario. Este problema lo resuelve el ng-href ya
que este hará que el vínculo solo funcione cuando esté completamente construido y listo
para ser usado.
ng-src y ng-srcset
Al igual que ng-href esta directiva resuelve el problema de que las llamadas a las URL
generadas por el motor de plantillas se hagan en el momento en que la plantilla está lista.
Ahora mencionaré las directivas relacionadas con eventos. Cada una de estas directivas
hacen disponible un objeto $event con el evento.
ng-blur
Esta directiva dispara un evento en el momento que el elemento pierde el foco. Tiene
varios usos como por ejemplo en un formulario de registro para ejecutar una comprobación si el nombre de usuario existe.
ng-copy, ng-cut y ng-paste
Estas directivas se explican solas por el significado de su nombre. Cada una evalúa una
expresión dada cuando se realiza alguna de las acciones de copiar, cortar o pegar sobre
un elemento.
Capítulo 6: Directivas
88
ng-dblclick
Al igual que la directiva ng-click esta evalúa una expresión al hacer doble clic en el
elemento.
ng-keydown, ng-keypress y ng-keyup
Estas directivas evalúan una expresión al ejecutarse la acción de presionar una tecla,
cuando la tecla esta presionada y cuando la tecla es liberada. Además, en el objeto evento
se les puede extraer el código de la tecla.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body ng-controller="miCtrl">
<input type="text"
ng-keydown="keydown($event)"
ng-keypress="keypress($event)"
ng-keyup="keyup($event)">
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.keydown = function(e){
console.log('Key down - Key Code: '+e.keyCode, 'altKey: '+e.altKey);
};
$scope.keypress = function(e){
console.log('Key press - Key Code: '+e.keyCode, 'altKey: '+e.altKey);
};
$scope.keyup = function(e){
console.log('Key up - Key Code: '+e.keyCode, 'altKey: '+e.altKey);
};
}])
</script>
</body>
Eventos del mouse
Angular maneja seis eventos con el mouse, cada uno de ellos evalúa una expresión al
producirse el evento. Lo describiremos con un ejemplo.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
89
<body ng-controller="miCtrl">
<div ng-style="style"
ng-mousedown="down()"
ng-mouseup="up()"
ng-mouseenter="enter()"
ng-mouseleave="leave()"
ng-mousemove="move($event)"
ng-mouseover="over()">
{{ pos }}
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.style = {
'border': '2px solid black',
'width': '200px',
'height': '200px',
'background-color': '#56A5F3'
};
$scope.down = function(e){console.log('Ejecutado el evento Mousedown'); };
$scope.up = function(e){console.log('Ejecutado el evento Mouseup'); };
$scope.enter = function(e){console.log('Ejecutado el evento Mouseenter');};
$scope.leave = function(e){console.log('Ejecutado el evento Mouseleave');};
$scope.move = function(e){$scope.pos = 'x: '+e.x + 'y: '+ e.y;};
$scope.over = function(e){console.log('Ejecutado el evento Mouseover');};
}])
</script>
</body>
Ahora describiré las directivas relacionadas con los formularios.
ng-change
Esta directiva evalúa una expresión cuando se modifica el contenido del control del
formulario por el usuario. Si la modificación viene desde el modelo esta acción no tiene
efecto.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
90
<body ng-controller="miCtrl">
<input type="text"
ng-model="input"
ng-change="cambio()"
placeholder="Escribe aquí">
{{ texto }}
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.cambio = function(){
$scope.texto = $scope.input;
}
}])
</script>
</body>
ng-checked
Nos permite evaluar una expresión y definir el valor checked de un elemento checkbox
dependiendo del resultado.
ng-disabled
La directiva ng-disabled nos permite deshabilitar un elemento de formulario dependiendo de la evaluación de una expresión. Si la expresión es evaluada a verdadero Angular
pone el atributo disabled en el elemento. Hay que tener en cuenta que esta directiva no
funcionará en Internet Explorer y navegadores antiguos.
1
2
3
4
5
6
7
8
9
10
11
<body ng-controller="miCtrl">
Deshabilitar el elemento
<input type="checkbox" ng-model="habilitado"> <br>
<input type="text" ng-disabled="habilitado" placeholder="Escribe aquí">
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.habilitado = false;
}])
</script>
</body>
Capítulo 6: Directivas
91
ng-readonly
En esencia esta directiva funciona de la misma forma que ng-disabled con la diferencia
de que no tiene problemas con los navegadores. Evalúa una expresión y si el resultado es
verdadero pone el atributo readonly en el elemento.
ng-selected
Esta directiva funciona de la misma forma que lo hace la directiva ng-checked. Evalúa
una expresión y si el resultado es verdadero pone el atributo selected en el elemento.
ng-submit
Cuando tratamos de hacer submit en un formulario, por defecto el navegador enviará
los datos del formulario y recargará la página. Pero si estamos haciendo una aplicación
de una sola página, la directiva ng-submit nos ayudara a tomar acciones previniendo
este comportamiento de recargar la página. Hay que tener en cuenta que para que esta
directiva tenga un correcto funcionamiento el formulario no debe tener el atributo action,
data-action o x-action definidos. Esta directiva evalúa una expresión al ser ejecutada y hace
disponible un objeto $event.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body ng-controller="miCtrl">
<div class="container">
<form ng-submit="login()">
Nombre: <input type="text" ng-model="usuario.nombre"><br>
Contraseña: <input type="password" ng-model="usuario.contrasena"> <br>
<input type="submit" value="Aceptar">
</form>
</div>
<script>
angular.module('miApp', [])
.controller('miCtrl', ['$scope', function ($scope) {
$scope.usuario = { nombre: '', contrasena: ''}
$scope.login = function(){
//Ejecutar el proceso de login
console.log($scope.usuario);
}
}])
</script>
</body>
Capítulo 6: Directivas
92
Esta directiva debe ser llamada en el elemento <form> si se desea usar ng-click en su lugar
ng-submit no debe estar presente y ng-click debe estar presente en el primer elemento
de tipo submit del formulario.
ng-focus
Este es uno de los eventos de formulario. Evalúa una expresión cuando el elemento
obtiene el foco.
ng-strict-di
Esta directiva se explica detalladamente en el Capítulo 3: Inyección de dependencia en
modo estricto.
ng-model-options
Esta directiva se explica detalladamente en el Capítulo 11: Otras formas de validación
Creando las directivas
Como has podido observar el framework tiene una gran cantidad que resuelven muchos
de las necesidades básicas de una aplicación. Ahora es tiempo de crear las directivas
más específicas de la aplicación que estamos desarrollando. Para esto necesitamos saber
cuál es el tratamiento que da Angular a las directivas. Cuando Angular es iniciado en
la aplicación este recorre todo el DOM aplicando comportamientos específicos a los
elementos. En este momento es donde las directivas son aplicadas, cuando angular
encuentra la llamada a una directiva en el DOM este aplica la funcionalidad definida
por esa directiva.
Para comenzar veamos una directiva muy simple.
1
2
3
4
5
6
7
angular.module('miApp', [])
.directive('primeraDirectiva', [function () {
return {
restrict: 'E',
template: '<p>Esta es la primera directiva</p>'
};
}])
Podemos hacer uso de esta directiva en el HTML de esta forma.
Capítulo 6: Directivas
1
2
3
93
<body>
<primera-directiva></primera-directiva>
</body>
Lo que veremos es que cuando Angular inicie e incluirá en el elemento <primeradirectiva> el elemento <p> con el texto que definimos en la directiva. Las directivas se
definen con el método directive() del módulo. Este recibe como primer parámetro el
nombre de la directiva, Hay que tener en cuenta que en la declaración de la directiva
el nombre tiene que ser definido en notación de camello (Camel Case). Pero a la hora
de hacer uso de esta en la vista, debe ser usada con el nombre dividido por -.‘Como
segundo parámetro recibe un arreglo con las dependencias y la función que será el
comportamiento de la directiva. Esta función siempre debe devolver un objeto con las
opciones que serán compiladas en la directiva.
Es una buena práctica que al nombrar la directiva se le incluya un prefijo para evitar
problemas con futuras especificaciones de HTML. Por ejemplo, si creas la directiva
breadcrumb y en HTML6 es creado ese elemento ocasionaría un conflicto. Por este
motivo deberíamos poner un prefijo mi-breadcrumb.
En el ejemplo anterior usamos la propiedad restrict. De esta forma indicamos a angular
de que tipo es la directiva. E es para elementos, A para atributos y C para nombre de
clases. Las directivas pueden ser declaradas con más de un tipo, veamos la directiva
anterior como atributo y elemento.
1
2
3
4
5
6
7
angular.module('miApp', [])
.directive('primeraDirectiva', [function () {
return {
restrict: 'EA',
template: '<p>Esta es la primera directiva</p>'
};
}])
Y su uso en la vista.
1
2
3
4
<body>
<primera-directiva></primera-directiva>
<div primera-directiva></div>
</body>
El Ejemplo anterior producirá una vista con el siguiente código.
Capítulo 6: Directivas
1
2
3
4
94
<body>
<primera-directiva><p>Esta es la primera directiva</p></primera-directiva>
<div primera-directiva=""><p>Esta es la primera directiva</p></div>
</body>
Como se ha restringido el uso de la directiva a Elementos y Atributos esta podrá ser
usada de cualquiera de las dos formas al mismo tiempo en la aplicación. Deberíamos
tratar de siempre restringir las directivas a Atributos ya que Internet Explorer tiene
problema con los elementos fuera de las especificaciones. Por otra parte, la otra propiedad que usamos en la declaración de la directiva es template que será un bloque de
código que será insertado en el DOM donde se llame a la directiva. Una buena práctica
es siempre separar el código de la vista o sea del template, para eso podemos usar la
propiedad templateUrl en vez de la anterior. Esta propiedad funciona de la misma forma
que la directica ng-include que he explicado antes. Así que solo tendremos que darle
como valor la url del template que queremos incluir.
Deben haber notado que en los dos usos de la directiva del ejemplo anterior el elemento
<p> es insertado dentro de donde se llama la directiva. Este comportamiento puede ser
cambiado si necesitamos que el template de la directiva remplace la llamada. Haciendo
uso de la propiedad replace y dándole un valor verdadero (true).
1
2
3
4
5
6
7
8
angular.module('miApp', [])
.directive('primeraDirectiva', [function () {
return {
restrict: 'EA',
template: '<p>Esta es la primera directiva</p>',
replace: true
};
}])
De esta forma el elemento <primera-directiva></primera-directiva> desaparecerá dejando en su lugar solo el template de la directiva que es el elemento <p>. Como estamos
creando nuevos elementos HTML los validadores no reconocerán las directivas ya que
no están en las especificaciones. AngularJS tiene una solución para este problema y es
que desde la llegada de HTML5 se permitió hacer uso de atributos personalizados con
el prefijo data- de esta forma pueden ser ejecutadas todas las directivas y angular las
reconocerá perfectamente.
Capítulo 6: Directivas
1
2
3
4
5
6
95
<body>
<div data-primera-directiva></div>
<div x-primera-directiva></div>
<div data-primera:directiva></div>
<div x-primera_directiva></div>
</body>
El ejemplo anterior produce en todos sus casos el mismo resultado que es mostrar
elemento <p>. Funciona de esta forma ya que Angular al encontrar una directiva elimina
el prefijo x- o data- del inicio del nombre y los :, -, _ los convierte en notación de camello
para su coincidencia con el nombre de la directiva que declaramos.
Ahora veamos otro ejemplo de la directiva, pero esta vez haciendo uso del scope.
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
9
10
1
2
3
angular.module('miApp')
.controller('UsuariosCtrl', ['$scope', function ($scope) {
$scope.usuario = {
nombre: 'john',
apellido: 'Doe',
email: '[email protected]'
};
}])
angular.module('miApp')
.directive('info', [function () {
var plantilla = '<a href="mailto:{{usuario.email}}">';
plantilla += '{{usuario.nombre}} {{usuario.apellido}}</a>';
return {
restrict: 'A',
template: plantilla,
remplace: true
};
}])
<body data-ng-controller="UsuariosCtrl">
<div data-info></div>
</body>
De esta forma podemos mostrar toda la información del usuario con un solo elemento
HTML, pero si tenemos varios usuarios en el scope no podríamos usar la directiva ya
que no tenemos forma de decirle que usuario es el que queremos mostrar. Para esto
Angular nos permite aislar el scope de la directiva y decirle solo que usará del scope
del controlador. Mediante la propiedad scope del objeto que se devuelve en la directiva
podremos relacionar ambos scopes de la siguiente forma.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('miApp')
.controller('UsuariosCtrl', ['$scope', function ($scope) {
$scope.usuario1 = {
nombre: 'john',
apellido: 'Doe',
email: '[email protected]'
};
$scope.usuario2 = {
nombre: 'Jane',
apellido: 'Doe',
email: '[email protected]'
};
}])
1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('miApp')
.directive('info', [function () {
var plantilla = '<a href="mailto:{{usuario.email}}">';
plantilla += '{{usuario.nombre}} {{usuario.apellido}}</a>';
return {
restrict: 'A',
scope: {
usuario: '=usuario'
},
template: plantilla,
remplace: true
};
}])
1
2
3
4
96
<body data-ng-controller="UsuariosCtrl">
<div data-info data-usuario="usuario1"></div>
<div data-info data-usuario="usuario2"></div>
</body>
Como puedes observar en el ejemplo anterior en la vista damos un nuevo atributo a la
directiva el cual tendrá el nombre de la variable de scope que queremos vincular al scope
de la directiva. En la directiva la propiedad scope tendrá como valor un objeto con los
elementos del scope del controlador que serán vinculados, la llave será la que tendremos
disponible en el scope de la directiva y el valor será el tipo de vínculo y el nombre del
atributo al que vincularemos.
En la directiva anterior puedes observar que cuando vinculamos en el scope el usuario
añadimos un caracter = lo que será el tipo de vínculo que tendremos de ese atributo
Capítulo 6: Directivas
97
en el scope de la directiva. El = vincula directamente los datos haciéndolos visible en el
scope, en caso de que sean modificados por el controlador o en la misma vista, la directiva
mostrará los cambios de forma instantánea. Otro de los tipos de vínculos es @ que hará
visible el valor de uno de los atributos de la directiva directamente como propiedad del
scope, veamos un ejemplo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('miApp')
.directive('miMensaje', [function () {
var plantilla = '<div class="alert-{{tipo}}">';
plantilla += '<h2>{{titulo}}</h2><p>{{mensaje}}</p></div>';
return {
restrict: 'EA',
scope: {
tipo: '@',
titulo: '@',
mensaje: '@'
},
template: plantilla,
replace: true
};
}])
Tipos
Si la llave de la propiedad del scope tiene el mismo nombre que el valor podremos
indicar solo el tipo de vínculo como en el ejemplo anterior.
1
2
3
4
5
6
7
<body>
<mi-mensaje
titulo="Error"
tipo="warning"
mensaje="Error 404, El contenido que usted busca no ha sido encontrado.">
</mi-mensaje>
</body>
En el ejemplo anterior utilizamos los atributos del nuevo elemento mi-mensaje para
definir las propiedades del scope. Aunque estas estuviesen definidas en el scope por el
controlador, la directiva solo vincularía los atributos del elemento como propiedades
del scope. Si dentro del valor del atributo se hace la llamada al modelo, el modelo tendrá
acceso a modificar el valor dentro de la directiva en caso de que sea alterado, pero si es
alterado dentro de la directiva no se reflejará en el scope del controlador, es un vínculo
de una sola vía, desde el scope del controlador hacia la directiva, veamos otro ejemplo
para que quede claro.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
98
<body>
Mensaje: <input type="text" data-ng-model="mensaje">
<mi-mensaje
titulo="Error"
tipo="warning"
mensaje="{{ mensaje }}">
</mi-mensaje>
</body>
Al cambiar el texto en el input automáticamente se cambiará dentro de la directiva, pero
si dentro de la directiva ponemos otro input como este.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular.module('miApp')
.directive('miMensaje', [function () {
var plantilla = '<div class="alert-{{tipo}}">';
plantilla += '<h2>{{titulo}}</h2><p>{{mensaje}}</p>';
plantilla += '<input type="text" data-ng-model="mensaje"></div>';
return {
restrict: 'EA',
scope: {
tipo: '@',
titulo: '@',
mensaje: '@'
},
template: plantilla,
replace: true
};
}])
Al modificar el texto en el input dentro de la directiva este lo modificará en el scope
aislado de la directiva, pero no lo modificará fuera de la directiva como podemos
observar.
Existe un tercer tipo de vínculo & que se utilizará para delegar funciones. Este es el tipo
más complicado de los tres veamos un ejemplo para explicarlo con detalles.
Capítulo 6: Directivas
1
2
3
4
5
6
99
angular.module('miApp')
.controller('LogCtrl', ['$scope', function ($scope) {
$scope.log = function(elem) {
console.log(elem);
};
}])
El controlador solo tendrá la función que vincularemos a la directiva, nada especial en
él.
1
2
3
<body data-ng-controller="LogCtrl">
<mi-contacto log="log(msg)"></mi-contacto>
</body>
En la vista hacemos uso de la función log que creamos en el controlador pasándole un
msg como parámetro ya que de esta forma es como se ejecutará la función dentro de la
directiva.
1
2
3
4
5
6
7
8
9
10
11
angular.module('miApp')
.directive('miContacto', [function () {
return {
restrict: 'E',
scope: {
log: '&'
},
templateUrl: '_vistas/contacto.html',
replace: true
};
}])
En el objeto scope definimos la propiedad log con el valor & para hacer referencia a la
función declarada en el controlador. En esta ocasión usaremos la propiedad templateUrl
para no incluir la plantilla en la misma directiva. La plantilla de la directiva se encuentra
en un archivo contacto.html.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
100
<form name="contacto">
<label for="titulo">Titulo:</label>
<input type="text" id="titulo" ng-model="titulo"><br>
<label for="mensaje">Mensaje:</label>
<textarea
ng-model="mensaje" id="mensaje"
cols="30" rows="10">
</textarea><br>
<button ng-click="log({msg: 'Titulo: '+titulo+' Mensaje: '+mensaje})">
Enviar
</button>
</form>
En la plantilla de la directiva tenemos un formulario con Titulo y Mensaje y un botón que
hará uso de la función log del controlador a través de la directiva. Hay que destacar que
los parámetros a la función no se le pasarán directamente sino como un objeto donde
cada una de las propiedades del objeto es cada uno de los parámetros que recibirá la
función del controlador, En esta ocasión solo enviamos un parámetro que es msg por
lo enviamos en un objeto con una propiedad msg donde su valor será en entregado a la
función del controlador. Hay que tener en cuenta que los nombres son muy importantes,
si en vez de enviar msg en el objeto se envía alguna propiedad que no coincide con la que
espera en el uso de la directiva, esta no funcionara correctamente.
El uso del scope aislado dentro de la directiva solo puede comunicarse con el scope padre
de las tres formas anteriores. Es importante que se Entienda cada una de ellas ya que si se
declara la propiedad scope dentro de la directiva no se tendrá acceso al scope de padre a
no ser que se empleen estas vías. Por otra parte, cada vez que se necesite usar una directiva
en varios lugares diferentes de la aplicación será necesario aislar el scope porque siempre
el padre no tendrá las mismas características.
Como mismo podemos usar solo el símbolo del tipo de vínculo (@, = y &) si la propiedad
tiene el mismo nombre, también podemos vincular propiedades con nombres diferentes.
1
<mi-mensaje titulo="Error" tipo="warning" mensaje="{{ mensaje }}"></mi-mensaje>
1
2
3
4
5
scope: {
tipo: '@',
titulo: '@',
texto: '@mensaje'
}
De esta forma vinculamos el atributo mensaje al scope de la directiva con el nombre
texto.
101
Capítulo 6: Directivas
Hasta ahora las directivas parecen solo bloques estáticos en la aplicación. En caso de
que quisiéramos modificar el comportamiento de del DOM al estilo jQuery claro que
podemos hacerlo. Anteriormente comente que Angular nos brinda una versión reducida
de jQuery con las utilidades más usadas. En una directiva podremos utilizar la propiedad
link del objeto que devuelve la directiva para tomar acciones que modifiquen el DOM
al estilo jQuery.
Esta propiedad link tomará como valor una función anónima que recibirá tres parámetros. El primero es el scope pero no es el scope de la inyección de dependencias
en esta ocasión, es el scope de la directiva. En segundo lugar tomara el elemento, este
es el objeto jqLite con el que tendremos las funcionalidades jQuery para modificar el
elemento que será el template de la directiva, resumiendo $('<template>') para ejecutar
acciones sobre él. El tercer parámetro es un objeto con los atributos que definimos en la
directiva. Veamos un ejemplo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
angular.module('miApp')
.directive('miTitular', [function () {
return {
restrict: 'E',
template: '<div><h1>{{texto}}</h1></div>',
replace: true,
scope: {
texto: '@'
},
link: function (scope, iElement, iAttrs) {
console.log(scope.texto);
iElement.css('cursor', 'pointer')
.on('mouseenter', function(e){
iElement.css('opacity', 0.6);
})
.on('mouseleave', function(e){
iElement.css('opacity', 1)
}).append('<p>-- '+iAttrs.especial+'
}
};
}])
--</p>')
En el ejemplo anterior declaramos la directiva miTitular de tipo elemento con un
template muy sencillo, indicamos que queremos remplazar el contenido del template por
el elemento de la directiva. Obtenemos el texto como propiedad en el scope para enviarlo
a la consola y mostrarlo en la plantilla. Acto siguiente hacemos uso de la propiedad link
que recibe los tres parámetros antes detallados. Los nombres de los parámetros no tienen
importancia ya que no serán inyectados como dependencias.
Capítulo 6: Directivas
102
Como podemos observar en la función link, primero se envía el texto a la consola a
través del scope, luego se procede a usar el elemento con jqLite, cambiamos el cursor, y
añadimos dos eventos al elemento y por último insertamos el atributo especial después
del elemento. Notar que este último elemento que es accesible a través del objeto iAttrs
no está definido en el scope y aun así es accesible desde la función link. Para usar la
directiva lo haremos de la siguiente forma.
1
2
3
4
5
6
<body>
<mi-titular texto="Lorem ipsum dolor sit amet, consectetur adipisicing elit, s\
ed do eiusmod
tempor incididunt ut labore et dolore magna aliqua." especial="Atributo Especi\
al"></mi-titular>
</body>
El ejemplo anterior no es la mejor forma de realizar esa funcionalidad, pero para el
propósito de demostrar cómo funciona la propiedad link del objeto devuelto por la
directiva está bien. En el inicio de Angular el proceso de compilación de la vista ejecutará
esta función link individualmente en cada una de las directivas que encuentre.
En la directiva miTitular la función link se ejecutará individualmente en cada una
de las directivas aplicando comportamientos. Angular provee otra propiedad que se
ejecutará antes que link pero se ejecutará una vez en todas las instancias de miTitular al
mismo tiempo. Esta propiedad será útil para cuando necesites aplicar comportamientos
a todas las instancias pero que no requiera datos del scope de cada una. La mayoría
de las directivas no necesitarán esta propiedad ya que en la función compile estarás
modificando el DOM de todas las instancias y en la función link estarás modificando
una directiva específica, esperando por modificaciones, agregando eventos y demás.
Esta función recibe tres parámetros, el primero es el elemento, es la plantilla en si para
ser modificada en todas las directivas a la vez. El segundo parámetro es un arreglo con
los atributos de la directiva. El tercero es una función transclude la cual creará un clon
del elemento para modificar el DOM. Como puedes observar no hay parámetro scope
para poder manipular los datos de la directiva de forma individual. En esta ocasión no
hay inyección de dependencia por lo tanto los nombres de los parámetros pueden variar.
Veamos un ejemplo de la sintaxis.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
103
angular.module('miApp')
.directive('miCita', [function () {
return {
restrict: 'E',
compile: function (iElement, iAttrs) {
var plantilla = angular.element('<blockquote></blockquote>');
plantilla.append(iElement.contents());
iElement.replaceWith(plantilla);
}
};
}])
<body>
<mi-cita>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur\
adipisci eaque blanditiis minus explicabo voluptas, quo corrupti velit debitis \
quidem necessitatibus ab rem. Quaerat nisi quod sapiente tenetur, perspiciatis d\
ucimus!</mi-cita>
<mi-cita>Esta es una segunda cita.</mi-cita>
</body>
Como puedes observar en el ejemplo anterior se ejecutará la función compile y convertirá las directivas mi-cita en elementos <blockquote>. Para esto he hecho uso de la función
de angular element que no es más que un alias del jqLite, en este caso le pasamos como
parámetro una cadena HTML y nos devuelve el objeto jqLite para tomar acciones con
él.
Cuando la propiedad compile está presente en el objeto de la directiva, la propiedad
link es ignorada por completo. En casos de que necesitemos definir las dos propiedades
para tomar acciones diferentes dependiendo de las posibilidades de cada una, la función
compile deberá devolver la función link. Veamos el ejemplo siguiente.
1
2
3
4
5
6
7
8
9
10
11
angular.module('miApp')
.directive('miCita', [function () {
return {
restrict: 'E',
compile: function (iElement, iAttrs) {
var plantilla = angular.element('<blockquote></blockquote>');
plantilla.append(iElement.contents());
iElement.replaceWith(plantilla);
return function(scope, iElement, iAttrs){
iElement.css('text-align', 'right');
if (iAttrs.autor) {
Capítulo 6: Directivas
12
13
14
15
16
17
1
2
3
4
5
6
104
iElement.append('<br><span>Por: '+iAttrs.autor+'</span>')
};
};
}
};
}])
<body>
<mi-cita>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur\
adipisci eaque blanditiis minus explicabo voluptas, quo corrupti velit debitis \
quidem necessitatibus ab rem.</mi-cita>
<mi-cita data-autor="Maikel Rivero">Esta es una segunda cita.</mi-cita>
</body>
De esta forma es como usaremos compile y link en la misma directiva para lograr una
funcionalidad específica con cada una de ellas.
Además de las dos funciones antes mencionadas que añaden comportamientos a la
directiva, también disponemos de un controlador para la directiva. Esto quiere decir que
podremos crear todo tipo de funciones para tomar acciones dentro de nuestra directiva
por individual. Esta propiedad puede recibir uno de dos valores, el primero es una cadena
de texto que será el nombre de un controlador definido en la aplicación. El otro valor
posible es una función anónima para hacer de constructor del controlador. Como la
propiedad link provee control aislado dentro de la directiva la propiedad controller
brinda la posibilidad de declarar funcionamientos compartidos entre las directivas. Esto
puede lograrse ya que una directiva puede requerir el controlador de otra, estaremos
hablando sobre este tema más adelante.
El controlador recibe como parámetros el $scope que es el asociado directamente
con la directiva, el $element que es el elemento de la directiva en sí, los $attrs que
son los atributos definidos en la directiva y $transclude que es la función que creará
el clon del elemento para manipular el DOM. Hacer uso de este último dentro del
controlador es una mala práctica ya que los controladores no deben ser utilizados para
modificar el DOM, pero aun así tendremos la posibilidad a través de $transclude. Es una
buena práctica solo usar $transclude dentro de la función compile. Este controlador
funcionará de igual forma que si utilizáramos la directiva ng-controller en la directiva
que estamos creando.
El controlador dentro de la directiva es una buena idea solo cuando necesitamos exponer
funcionalidades para ser utilizadas en otras directivas. De lo contrario deberíamos
utilizar la función link para realizar las tareas individuales de la directiva.
Anteriormente mencionamos que podemos hacer uso de los métodos de un controlador en otra directiva. Para esto debemos entender el funcionamiento de la propiedad
Capítulo 6: Directivas
105
require. Esta recibirá como parámetro una cadena de texto o un arreglo de cadenas de
texto, las cuales serán el nombre de la directiva que queremos utilizar el controlador.
Este nombre de directiva que utilizamos para incluir el controlador puede tener varios
prefijos. Si no especificamos ningún prefijo la se buscará el controlador de una directiva
que exista en el mismo elemento veamos un ejemplo.
1
2
3
restrict: 'A',
require: 'otraDirectiva',
link: function(scope, iElemen, iAttrs, ctrl){//...}
1
<div mi-directiva otra-directiva></div>
En el ejemplo anterior en la directiva miDirectiva buscaremos el controlador de otraDirectiva que deberá estar en el mismo elemento. En caso que otraDirectiva estuviese
en otro elemento tendremos que poner el prefijo � en el nombre.
1
2
3
restrict: 'A',
require: '^otraDirectiva',
link: function(scope, iElemen, iAttrs, ctrl){//...}
1
2
3
<div otra-directiva>
<div mi-directiva></div>
</div>
De esta forma el controlador será buscado en el elemento padre. En caso de que no
se encuentre el controlador ocurrirá un error. Para evitar estos errores podremos
requerir un controlador de forma opcional especificando el prefijo ?. En caso de que
el controlador no sea encontrado se pasará el valor null.
1
2
3
restrict: 'A',
require: '?otraDirectiva',
link: function(scope, iElemen, iAttrs, ctrl){//...}
1
<div mi-directiva></div>
También podremos hacer una combinación de los dos prefijos para requerir un controlador en el elemento padre opcionalmente. Estos controladores serán pasados como
cuarto parámetro a la función link para ser usado.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
106
angular.module('miApp')
.directive('miDirectiva', [function () {
return {
restrict: 'A',
controller: function () {
this.log = function(){
console.log('Método de la directiva: mi-directiva');
};
}
};
}] )
.directive('otraDirectiva', [function () {
return {
restrict: 'A',
require: 'miDirectiva',
link: function (scope, iElement, iAttrs, ctrls) {
ctrls.log();
}
};
}])
<body>
<div mi-directiva otra-directiva></div>
</body>
En el ejemplo anterior se imprimirá en la consola el mensaje definido en el controlador
de la directiva miDirectiva a través de la función link de la directiva otraDirectiva. Esto
funciona de forma correcta por que las dos directivas están en el mismo elemento, pero
si la directiva mi-directiva es movida a el elemento <body> se producirá un error al no
ser encontrado el controlador ya que no especificamos el prefijo �. Como habrás podido
notar en el controlador se ha definido la función en el objeto this ya que si se hiciera en
el $scope este no estaría disponible para las demás directivas por ser el scope privado de
cada directiva. Mediante this podemos exponer los métodos que serán utilizados por la
función link de otras directivas.
Cuando usamos this para exponer contenido en el controlador al no ser declarado en el
$scope este no podrá ser utilizado directamente en la propia directiva a no ser que se use
la propiedad controllerAs. Utilizando esta propiedad damos un alias al controlador y lo
hacemos accesible con ese nombre dentro del scope. Veamos un ejemplo.
Capítulo 6: Directivas
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
107
angular.module('miApp')
.directive('miEjemplo', [function () {
return {
restrict: 'A',
template: '<div>{{ctrl.mensaje}}</div>',
controllerAs: 'ctrl',
controller: function () {
this.mensaje = 'La variable mensaje está expuesta para ser utilizada '
this.mensaje += 'fuera del controlador y a la vez dentro de la misma dir\
ectiva.';
}
};
}])
<body>
<div mi-ejemplo></div>
</body>
De esta forma el controlador se expondrá para ser utilizado en otras directivas y mediante la propiedad controllerAs lo definimos dentro del scope interno de la directiva.
Hasta el momento hemos creado varias directivas que cumplen diferentes funcionalidades, debes haber notado que ninguna tiene contenido dentro de sí misma. En muchas
de las ocasiones el contenido de la directiva es importante, por lo que necesitamos
encargarnos de que cuando la plantilla de la directiva sea intercambiada en su lugar se
tenga en cuenta el contenido que tiene la misma. Esto se posible mediante la propiedad
transclude y la directiva ng-transclude. La propiedad transclude debe tener como valor
true cuando la necesitemos usar ya que en su ausencia angular la declara automáticamente false. Veamos un ejemplo de cómo funciona.
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('miApp')
.directive('comentario', [function () {
var plantilla = '<div><div><img ng-src="{{imagenSrc}}">Por: {{por}}</div>';
plantilla += '<blockquote ng-transclude></blockquote></div>';
return {
restrict: 'E',
scope: {
por: '@',
imagenSrc: '@'
},
replace: true,
transclude: true,
Capítulo 6: Directivas
13
14
15
1
2
3
4
5
6
7
108
template: plantilla
};
}])
<body>
<comentario por="Maikel Rivero" imagen-src="camino/hacia/el/avatar.jpg">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nemo, sed qui sint\
, vitae repellendus sit deleniti fuga voluptate maxime ut eius numquam pariatur \
dolorum, quos nostrum? Sequi voluptatibus tempora labore.
</comentario>
</body>
En el ejemplo anterior damos el valor verdadero a la propiedad transclude y luego en el
template definimos la directiva ng-transclude dentro del elemento <blockquote> que es
donde se pondrá el contenido.
Otro de las propiedades que se puede definir en el objeto de la directiva es la prioridad
con que se ejecuta la directiva en el mismo elemento. Por lo general esta propiedad es
omitida y por defecto obtiene valor 0. Esta propiedad es necesario definirla con un mayor
valor en caso de que quisiéramos que la directiva se ejecute primero que otras en el mismo
elemento. En un elemento que tenga dos directivas se ejecutará primero la que mayor
prioridad posea, en caso de que las dos tengan la misma prioridad se ejecutará por orden
de declaración. Esta propiedad debe ser definida con un valor numérico.
Hasta ahora hemos ya detallado como crear directivas propias que sean específicas
para la aplicación que estés creando. Con todos los temas tratados y las directivas
que proporciona el mismo framework tienes en las manos las herramientas para crear
potentes bloques de código que sean reutilizables incluso para otras aplicaciones que
desarrolles en el futuro.
Capítulo 7: Filtros
Si AngularJS tiene muchas directivas en su núcleo como vimos en el capítulo anterior,
no es así con los filtros. En esta ocasión tiene una cantidad muy reducida, pero de igual
forma puedes crear los tuyos propios, y es lo que trataremos en este capítulo. Primero
comenzaremos detallando los filtros que nos brinda el núcleo del framework y luego
comenzaremos a crear filtros propios.
En AngularJS los filtros pueden ser usados en cualquier parte de la aplicación mediante el
servicio $filter que puede ser inyectado en controladores y servicios. En la vista pueden
ser usados mediante el caracter | como ya pudimos observar en el capítulo anterior donde
se explicaba la directiva ng-repeat. Para ver un ejemplo vamos a ver el primer filtro.
Currency
El filtro currency da formato a los números como moneda poniendo el símbolo de la
moneda que se está usando delante del número y separándolo por comas y punto con
decimales. Veamos el ejemplo.
1
2
3
4
<body>
<div>Costo: {{ 1412.99 | currency }}</div>
<div>Costo: {{ 728.99 | currency: "€" }}</div>
</body>
En la vista podremos especificar el filtro currency solo o podremos pasarle un parámetro
de tipo cadena de texto con el símbolo en que queremos que se convierta el número. En
caso de que no se especifique el símbolo se tomará el definido local para moneda en el
sistema. En los controladores y servicios se utilizan los filtros a través del servicio $filter
veamos el ejemplo.
109
Capítulo 7: Filtros
1
2
3
4
5
6
7
110
angular.module('miApp')
.controller('FiltroCtrl', ['$scope', '$filter', function ($scope, $filter) {
var costo = 1453.50;
$scope.costo = $filter('currency')(costo);
$scope.costoEuro = $filter('currency')(costo, '€');
console.log($scope.costo,$scope.costoEuro);
}] )
Como puedes observar es muy sencillo usarlo en el controlador, el servicio $filter es
una función que recibe como parámetro una cadena de texto con el nombre del filtro
que se quiere utilizar. El filtro es devuelto por el servicio filter así que podremos usarlo
en cadena pasándole como primer valor el número que se quiere pasar por el filtro, y
de forma opcional el segundo parámetro será una cadena de texto con el símbolo que se
quiere mostrar.
En la versión 1.3 de Angular se añadió un nuevo parámetro que puedes especificar a la
hora de mostrar la moneda, este es la cantidad de decimales que deseas mostrar después
del .. Para utilizarlo debes primero especificar el tipo de moneda y luego como segundo
parámetro la cantidad de decimales.
1
<div>Costo: {{ 728.42963 | currency:"€":3 }}</div>
Number
Como el filtro anterior interpreta números a monedas poniendo las comas y los puntos
en los decimales de forma correcta, el filtro number lo hace, pero sin especificar una
moneda. Además, es posible especificar la cantidad de decimales que deseamos que se
muestren con el número, en caso de no ser especificado será obtenido por defecto de la
configuración del sistema, normalmente 3 dígitos.
1
2
3
4
<body>
{{ 2432.44*332.91 | number }}
{{ 2432.44*332.91 | number:5 }}
</body>
Uppercase y Lowercase
Para convertir una cadena de texto en mayúsculas o minúsculas tenemos dos filtros que
resolverán en cualquier tipo de situación.
Capítulo 7: Filtros
1
2
3
4
111
<body>
{{ "Prueba de texto" | uppercase }}
{{ "PRUEBA De TeXtO" | lowercase }}
</body>
limitTo
Otro de los filtros es limitTo que funcionará para arreglos o para cadenas de texto
devolviendo solo la parte a la que se ha limitado. Este filtro recibe un parámetro que
es la cantidad a la que se limitará la cadena o arreglo. Si el número proporcionado como
límite es negativo, se devolverá la cantidad de caracteres o elementos comenzando desde
el final.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
function ctrl ($scope) {
$scope.arreglo = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
$scope.texto = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Do\
lorum maxime eum perspiciatis hic corporis sapiente. Ab, provident modi deleniti\
assumenda nobis ratione, accusantium porro, necessitatibus similique beatae dol\
oremque perferendis tempora!';
}
</script>
<div data-ng-controller="ctrl">
{{ texto | limitTo: 150 }}
{{ texto | limitTo: -135 }}
{{ arreglo | limitTo: 6 }}
{{ arreglo | limitTo: -3 }}
</div>
Date
Uno de los filtros más útiles que nos brinda Angular es date ya que por lo general
las fechas son almacenadas en el servidor con un formato como este 1409323723006.
Difícilmente podremos saber qué fecha de ese número a primera vista, para eso el filtro
date la convertirá a una fecha que podamos interpretar.
1
2
3
<body>
{{ 1409323723006 | date }}
</body>
Capítulo 7: Filtros
112
Hay casos en que el formato de la fecha no es el que deseamos, por ese motivo podremos
pasarle el formato como parámetro como una cadena de texto para que sea formateada
como deseemos.
1
2
3
<body>
{{ 1409323723006 | date: "dd/MM/y hh:mm:ss a" }}
</body>
Esta cadena de formato puede obtener cualquiera de los valores de fecha usados normalmente en programación como los que están demostrados en el ejemplo anterior, además
unas cadenas con formatos predefinidos que facilitan la escritura del formato.
•
•
•
•
•
•
•
•
‘medium’: es equivalente a ‘MMM d, h:mm:ss a’. ej: Aug 29, 2014 10:48:43 AM
‘short’: es equivalente a ‘M/d/yy h:mm a’. ej: 8/29/14 10:48 AM
‘fullDate’: es equivalente a ‘EEEE, MMMM d,y’. ej: Friday, August 29, 2014
‘longDate’: es equivalente a ‘MMMM d, y’. ej: August 29, 2014
‘mediumDate’: es equivalente a ‘MMM d, y’. ej: Aug 29, 2014
‘shortDate’: es equivalente a ‘M/d/yy’. ej: 8/29/14
‘mediumTime’: es equivalente a ‘h:mm:ss a’. ej: 10:48:43 AM
‘shortTime’: es equivalente a ‘h:mm a’. ej: 10:48 AM
En caso de no especificar ninguno de los formatos, Aungular utilizará mediumDate por
defecto. El filtro date se puede utilizar de igual forma a través del servicio $filter en
controladores y servicios pasando como primer parámetro la fecha y como segundo el
formato de forma opcional.
En la versión 1.3 de Angular se añadió un nuevo formato para mostrar en la fecha.
Además de todos los anteriores ahora tienes disponible el número de la semana en el
año. Para mostrar el número de la semana de una fecha puedes utilizar el valor ww si
deseas que este se muestre con dos dígitos (Ej. 04), o una simple w para que se muestre
con un solo digito (Ej. 4).
OrderBy
El filtro orderBy es muy útil a la hora de mostrar arreglos de datos ya que los puede
ordenar de forma alfabética si son cadenas de texto o de forma numérica si son números.
Este filtro funciona muy bien combinado con la directiva ng-repeat ya que permite
ordenar la lista que se muestra iterando sobre un arreglo.
Podemos pasar tres parámetros al filtro, el primero es el arreglo si lo estamos usando
a través del servicio $filter ya que si se usa en la directiva ng-repeat no es necesario.
Capítulo 7: Filtros
113
El segundo parámetro es la expresión por la que se ordenará el arreglo. Este parámetro
puede ser una función que devolverá la expresión por la que se ordenará, también puede
ser una cadena de texto con el nombre de una de las propiedades de un objeto dentro del
arreglo, esta puede tener un prefijo + o - para indicar orden ascendente o descendente.
El tercer valor que puede obtener el segundo parámetro es un arreglo de cadenas de
texto o funciones para ser evaluadas en caso de que el primer elemento del arreglo haga
que coincidan dos elementos de la lista se ejecutará la siguiente expresión del arreglo. El
tercer parámetro que recibe el filtro es uno de tipo booleano que indica si el arreglo será
ordenado de forma reversa.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body data-ng-controller="ctrl">
<table border="1">
<thead> <tr>
<th><a href="#" data-ng-click="campo = 'nombre'; reverso=!reverso">
Nombre
</a></th>
<th><a href="#" data-ng-click="campo = 'apellidos'; reverso=!reverso">
Apellidos
</a></th>
<th><a href="#" data-ng-click="campo = 'email'; reverso=!reverso">
Email
</a></th>
<th>Lenguajes</th>
</tr> </thead>
<tbody>
<tr data-ng-repeat="usuario in usuarios | orderBy:campo:reverso">
<td>{{ usuario.nombre }}</td>
<td>{{ usuario.apellidos }}</td>
<td>{{ usuario.email }}</td>
<td>{{ usuario.lenguajes.join(',') }}</td>
</tr>
</tbody>
</table>
<script src="lib/angular.js"></script>
<script src="js/app.js"></script>
<script>
function ctrl ($scope) {
$scope.campo = 'nombre';
$scope.reverso = true;
$scope.usuarios = [
{ nombre: "Maikel",
apellidos: "Rivero Dorta",
email: "[email protected]",
Capítulo 7: Filtros
34
35
36
37
38
39
40
41
42
43
44
45
46
114
lenguajes: ["en", "es"] },
{ nombre: "john",
apellidos: "Doe",
email: "[email protected]",
lenguajes: ["en"] },
{ nombre: "Jane",
apellidos: "Doe",
email: "[email protected]",
lenguajes: ["en","es"] }
];
}
</script>
</body>
Existen otras extensiones de angular que proveen filtros específicos como también hay
muchos filtros desarrollados por la comunidad que pueden ser utilizados en la aplicación
que estés desarrollando. Ahora que ya tienes conocimiento de los filtros que provee
Angular es hora de que crees tus propios filtros.
Creando filtros
Como Angular nos permite crear directivas también nos permite crear filtros, estos son
relativamente más fáciles de crear que las directivas. Los filtros los definimos con el
método filter del módulo, este acepta un primer parámetro que será el nombre del filtro
y como segundo parámetro un arreglo con las dependencias y la función del filtro. Esta
función siempre debe devolver otra función que será el filtro en sí.
Te estarás preguntando por que dos funciones, es sencillo, la primera es la función
que se usará para inyectar las dependencias del filtro y la segunda es el filtro que se
ejecutará. Esta función del filtro recibirá como primer parámetro el contenido que será
filtrado, haciéndolo disponible de esta forma para poder alterarlo como sea necesario.
En caso de que el filtro necesite recibir algún parámetro de configuración, estos serán
inyectados como parámetro en la función del filtro después del contenido. La función
del filtro siempre debe devolver el contenido modificado o un nuevo contenido. Veamos
un ejemplo.
Capítulo 7: Filtros
1
2
3
4
5
6
7
8
9
10
115
angular.module('miApp')
.filter('titulo', function(){
return function(input){
return input.split(' ')
.map(function(elem){
return elem[0].toUpperCase() + elem.slice(1);
})
.join(' ');
};
});
En la vista lo usaremos como los filtros de Angular, especificando el nombre del filtro
después del caracter |.
1
2
3
<body>
<h1> {{ 'este es el título de una entrada del blog' | titulo }} </h1>
</body>
El filtro anterior convertirá a mayúsculas todas las primeras letras de cada palabra para
que parezca un título. Este filtro también puede ser utilizado en un servicio o en un
controlador mediante el servicio $filter.
1
2
3
4
5
6
angular.module('miApp')
.controller('BlogCtrl', ['$scope', '$filter',
function ($scope, $filter) {
var titulo = 'este es el título de una entrada del blog';
$scope.titulo = $filter('titulo')(titulo);
}]);
De esta forma obtendremos el mismo resultado que utilizándolo en la vista con el
caracter |. Además de esa forma de usar los filtros mediante el servicio filter existe otra
forma de inyectar los filtros y es inyectando como dependencia el nombre del filtro y a
continuación la palabra Filter ejemplo tituloFilter.
1
2
3
4
5
6
angular.module('miApp')
.controller('BlogCtrl', ['$scope', '$filter', 'tituloFilter',
function ($scope, $filter, tituloFilter) {
var titulo = 'este es el título de una entrada del blog';
$scope.titulo = tituloFilter(titulo);
}] )
De esta forma el código queda más limpio que usando el servicio $filter pero el
comportamiento es exactamente el mismo así que deberás escoger cual es la forma que
utilizarás en tus aplicaciones.
Capítulo 8: Rutas
Hasta el momento hemos estado viendo diferentes usos de AngularJS, todos en la misma
página, lo que nos detiene un poco a la hora de crear diferentes funcionalidades. Otro
de los aspectos importantes del framework es su habilidad para crear aplicaciones de
una sola página. Esto es logrado mediante la observación del cambio de las rutas en
el navegador. Para este propósito Angular posee un módulo llamado ngRoute que nos
proporciona la habilidad de configurar rutas en la aplicación y responder con diferentes
comportamientos para cada una de ellas.
El módulo ngRoute
El módulo ngRoute no está incluido en el núcleo de Angular por lo que debemos incluirlo
en nuestra aplicación como mismo hicimos con el framework. Este módulo lo podremos
obtener de varias formas, a través del CDN de Google //ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z
route.js, descargándolo desde la página oficial de Angular o instalándolo con el gestor
de dependencias bower.
1
bower install --save angular-route
Luego de tener el módulo de alguna de las formas anteriores debemos incluirlo en
nuestra aplicación después de la línea donde incluimos el framework, nunca antes u
obtendremos un error.
1
2
<script src="lib/angular.js"></script>
<script src="lib/angular-route.js"></script>
Con incluir el archivo del módulo en la aplicación no es suficiente, tendremos también
que inyectarlo como dependencia donde definimos el módulo para que Angular lo haga
disponible.
1
angular.module('miApp', ['ngRoute']);
De esta forma ya podremos comenzar a hacer uso del módulo dentro de la aplicación.
ngRoute nos proporciona varios componentes como la directiva ng-view la que se
encargará de mostrar el contenido de las plantillas. También hace disponible el $routeProvider que utilizaremos para configurar las rutas de la aplicación. Además, dos
servicios, $route y $routeParams los cuales explicaremos al detalle más adelante.
116
Capítulo 8: Rutas
117
Definiendo las rutas con $routeProvider
Siguiendo las prácticas de la organización de ficheros del Capítulo 2, configuraremos
las rutas en el archivo app/js/Config/Routes.js para mantener la aplicación organizada.
Las rutas de la aplicación se definen mediante el $routeProvider que tiene un API muy
simple con la que relacionaremos plantillas, controladores y resolveremos datos antes de
mostrar a la vista. Veamos un ejemplo sencillo.
1
2
3
4
5
6
7
8
9
10
angular.module('miApp')
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/', {templateUrl: 'home.html', controller: 'HomeCtrl'})
.when('/contacto', {
templateUrl: 'contacto.html',
controller: 'ContactoCtrl'
})
.otherwise({ redirectTo: '/' });
}] )
La definición de las rutas de la aplicación se hace dentro de un bloque config() del
módulo. De esta forma tendremos acceso al Provider del servicio $route que es el que se
encarga de todo el enrutamiento de la aplicación. Este provider tiene dos métodos con
los cuales definiremos las rutas.
El primero es when(), este método es el encargado de añadir nuevas rutas al servicio
$route. Acepta dos parámetros, el primero es una cadena de texto con el patrón de la
ruta que queremos responder. La ruta puede tener parámetros los cuales podremos usar
en la aplicación para cambiar el comportamiento dependiendo de cuales parámetros se
recibe. Estos parámetros son representados en la ruta comenzando por : y terminando
por /. El nombre especificado en el parámetro estará disponible como propiedad en
el servicio $routeParams. Además podremos tener parámetros que sean opcionales
especificando un ? al final del parámetro, de manera que si este no estuviese presente
la ruta se resolvería sin él.
1
2
3
4
5
$routeProvider
.when('/saludo/:mensaje?', {
templateUrl: 'saludo.html',
controller: 'SaludoCtrl'
});
En el ejemplo anterior podríamos utilizar el parámetro mensaje de la ruta para enviarlo
a la vista o como es opcional si no está disponible enviar a la vista un mensaje por defecto.
Capítulo 8: Rutas
118
Si el usuario introduce un caracter / al final de la ruta o le falta alguno para coincidir con la ruta que definimos, el servicio $location se encargará de agregarlo
o eliminarlo para que la ruta coincida. El proceso es realizado por el servicio
location debido a que todas las rutas son utilizadas por $location.path.
El segundo parámetro que recibe el método when del $routeProvider es un objeto de
configuración que puede tener varias propiedades, veamos cada una de ellas.
template: Esta propiedad puede tener una cadena de texto o una función como valor. Si
es una cadena de texto deberá ser la plantilla HTML que se mostrará para esta ruta. Si
es una función esta debe revolver una plantilla HTML para ser usada. Esta función será
llamada con un objeto como parámetro el cual tendrá los parámetros de la ruta como
propiedades. La propiedad template no debería ser usada, siempre debemos usar una
plantilla externa en vez de escribir código HTML directamente dentro de la lógica de
la aplicación, en su lugar se debe usar la propiedad templateUrl. Veamos un ejemplo
utilizando una función y los parámetros.
1
2
3
4
5
6
$routeProvider
.when('/saludo/:mensaje?', {
template: function(params){
return '<p>'+(params.mensaje || 'Hola')+'</p>';
}
})
templateUrl: Esta propiedad puede tener como valor una cadena de texto o una función.
Si es una cadena de texto deberá ser la url de una plantilla que será utilizada para mostrar
al usuario cuando la ruta se resuelva. Si es una función deberá devolver la ruta de la
plantilla, esta función será llamada con un objeto que contendrá los parámetros de la
ruta como propiedades al igual que la propiedad template. Si está presente la propiedad
template no se utilizará templateUrl.
controller: Puede tener una cadena de texto como valor indicando el nombre de un
controlador previamente definido en la aplicación o una función para definir el controlador. Es una mala práctica definir el controlador en la misma ruta. Este controlador
será el utilizado para toda la plantilla que responderá a la ruta. Algunos desarrolladores
prefieren especificar el controlador en la vista con la directiva ng-controller, de manera
que sea fácil de saber cuál controlador es el que utiliza la vista. En mi opinión es
responsabilidad de la ruta especificar la vista y el controlador. También se puede utilizar
la sintaxis controller as en esta propiedad.
controllerAs: Es una cadena de texto con un alias para el controlador. Si esta propiedad
está presente el controlador será expuesto en el $scope con ese alias.
resolve: Esta propiedad debe tener como valor un objeto con dependencias que serán
inyectadas en el controlador. Si alguna de estas dependencias son promesas se esperará
Capítulo 8: Rutas
119
a que todas sean resueltas o que alguna sea rechazada antes de instanciar el controlador.
Si todas las promesas son resueltas los valores de las respuestas son inyectadas en el
controlador y se disparará el evento $routeChangeSuccess si alguna es rechazada se
disparará el evento $routeChangeError. Más adelante estaremos hablando sobre los
eventos así que no te preocupes por esto ahora.
Las propiedades del objeto deberán tener como llave el nombre que será inyectado como
dependencia en el controlador. Como valor podrán tener una cadena de texto la cual debe
ser el nombre de un servicio previamente definido en la aplicación. El otro valor puede
ser una función que será instanciada y el resultado será enviado al controlador como
dependencia. Si la función devuelve una promesa se esperará hasta que sea resuelta antes
de ser inyectada en el controlador. Es importante saber que si dentro de esta función se
utilizará el servicio $routeParams, este todavía tendrá los parámetros de la ruta anterior
no los de la que se está ejecutando. Si se necesita utilizar los parámetros se deberán
acceder a ellos mediante el servicio $route.current.params para acceder a los nuevos
parámetros.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$routeProvider
.when('/resolver', {
template: '<p>Contenido: {{contenido}} </p>'+
'<p>Promesa: {{promesa}} </p>',
resolve: {
contenido: function(){
return 'Este es el contenido resuelto';
},
promesa: function($q){
var def = $q.defer();
setTimeout(function(){
def.resolve('La promesa ha sido resuelta.');
}, 2000);
return def.promise;
}
},
controller: function($scope, contenido, promesa){
$scope.contenido = contenido;
$scope.promesa = promesa;
}
})
En el ejemplo anterior tenemos dos dependencias que se resolverán, la primera es una
función que se resolverá de forma instantánea e inyectará su respuesta al controlador. La
segunda es una promesa que demorará 2 segundos en ser resuelta, así que el controlador
Capítulo 8: Rutas
120
no se instanciará hasta que se haya resuelto. Como uno de los elementos a resolver es
una promesa el controlador no será instanciado hasta que la promesa sea resuelta.
redirectTo: Esta propiedad puede tener como valor una cadena de texto o una función.
Si es una cadena de texto se hará una redirección hacia ese valor mediante el servicio
$location. Si es una función deberá devolver una cadena de texto con la ruta a la que
se hará la redirección. A esta función se le inyectarán tres parámetros, el primero es un
objeto con los parámetros de la ruta, el segundo es el valor path del servicio $location
cuando se hace la llamada a la ruta, el tercero es un objeto con los valores de la búsqueda
que existen en la url proporcionados por $location.search().
1
2
3
4
5
6
7
$routeProvider
.when('/redirect/:param', {
redirectTo: function(param, path, search){
console.log(param, path, search);
return '/resolver';
}
})
reloadOnSearch: Esta propiedad recibe un valor de tipo booleano, por defecto es
verdadero e indica que se recargara la vista cuando se cambie el valor dela propiedad
$location.search() o $location.hash(). Si se declara con un valor false y se cambia la url
en el navegador, se disparará el evento $routeUpdate en el $rootScope para tomar las
acciones necesarias.
caseInsensitiveMatch: Debe tener un valor verdadero o falso. Por defecto esta propiedad tiene un valor false lo que hace que la url que especifiquemos en el navegador tenga
que coincidir exactamente con la declarada incluyendo mayúsculas y minúsculas. Si este
valor se define a verdadero (true) se intentarán resolver las direcciones sin importar las
mayúsculas y las minúsculas.
Las anteriormente descritas son todas las propiedades que acepta el objeto de configuración de la ruta del método when().
El segundo método que proporciona el $routeProvider es otherwise() que acepta como
único parámetro un objeto de configuración como el del método when(). Por lo general
este método se utiliza solo para redireccionar a otra ruta.
1
.otherwise({ redirectTo: '/' });
En la nueva versión 1.3 de Angular al método otherwise() se le agrego la opción de
aceptar una cadena de texto como parámetro. Esto quiere decir que ya no tendremos
que especificar un objeto con la propiedad redirectTo, simplemente pasamos una cadena
con la dirección a la que queremos que nos envíe este método.
Capítulo 8: Rutas
1
121
.otherwise('/');
La única propiedad que tiene $routeProvider es caseInsensitiveMatch. Esta funciona
igual que si lo definiéramos dentro de la configuración de una ruta en específico,
pero se aplicará para todas las rutas a la vez. Es importante que esta propiedad se
utilice antes de la primera llamada del método when, de lo contrario podríamos obtener
comportamientos no deseados.
1
2
3
4
5
6
7
8
9
$routeProvider.caseInsensitiveMatch = true;
$routeProvider
.when('/', {
template: 'Inicio <a href="#/Nosotros">Nosotros</a>'
})
.when('/nosotros', {
template: 'Acerca de nosotros'
})
.otherwise('/');
Uniendo los componentes
Con toda la configuración antes mencionada ya puedes comenzar a crear tu aplicación
con tus propias rutas, crear las plantillas y los controladores para que respondan a
estas rutas y visualizar los resultados en el navegador. Aún quedan más detalles del
módulo ngRoute que hablaremos más adelante. Ahora para continuar con el aprendizaje
crearemos una pequeña aplicación para almacenar contactos, esto lo haremos paso a paso
para ver cómo se integran todos los componentes que hemos estado aprendiendo hasta
ahora.
Crearemos un archivo index.html que responderá como la aplicación. Incluiremos el
framework y el módulo ngRoute. Declararemos la directiva ng-app con valor ContactosApp que será el nombre del módulo que crearemos. Utilizaremos la directiva ng-view
que será el contenedor de todas las vistas. También incluiremos el archivo app.js que
tendrá la definición de la aplicación y el archivo de rutas que veremos más adelante.
Capítulo 8: Rutas
122
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en" data-ng-app="ContactosApp">
<head>
<meta charset="UTF-8">
<title>Contactos</title>
</head>
<body>
<h1>Contactos</h1>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/Config/rutas.js"></script>
</body>
</html>
El archivo de las rutas por ahora solo tendrá definida la página principal que será una
lista de los contactos.
Archivo: App/js/Config/rutas.js
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('ContactosApp')
.config(['$routeProvider', function ($routeProvider) {
var vista = function(vista) {
return '_vistas/' + vista.split('.').join('/') + '.html';
}
$routeProvider
.otherwise({ redirectTo: '/' })
.when('/', {
templateUrl: vista('lista'),
controller: 'ListaCtrl'
})
}])
Además, se ha definido una función vista para que sea más fácil indicar las vistas a la
propiedad templateUrl del objeto de configuración de las rutas. Esta función devolverá
la dirección de una plantilla.html dentro de la carpeta _vistas donde se dividirá por .
la ruta y no se necesitará indicar la extensión html. La ruta / tendrá como plantilla el
archivo /_vistas/lista.html y el controlador ListaCtrl. Veamos el controlador.
Capítulo 8: Rutas
123
Archivo: App/js/Controladores/ListaCtrl
1
2
3
4
5
angular.module('ContactosApp')
.controller('ListaCtrl', ['$scope', 'contactos',
function ($scope, contactos) {
$scope.contactos = contactos.lista();
}])
En el controlador ListaCtrl hacemos uso del servicio contactos y exponemos al $scope
la lista de contactos mediante el método lista() del servicio contactos. Veamos ahora la
vista de esta ruta.
Archivo: App/_vistas/lista.html
1
2
3
4
5
6
7
<div>
<ul>
<li data-ng-repeat="contacto in contactos">
<a ng-href="#/{{$index}}">{{contacto.nombre}}</a>
</li>
</ul>
</div>
En esta vista iteraremos sobre el arreglo de los contactos y mostraremos un vínculo a
una nueva ruta para ver contactos con el $index del contacto. Antes de crear esa nueva
ruta veamos el servicio contacto.
Archivo: App/js/Servicios/contactos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular.module('ContactosApp')
.factory('contactos', [function () {
var contactos = [
{ nombre: 'Maikel Rivero Dorta',
email: '[email protected]',
tel: '123456789', },
{ nombre: 'john Doe',
email: '[email protected]',
tel: '543216789', }
];
return {
lista: function(){
return contactos;
}
};
}])
Capítulo 8: Rutas
124
En este servicio almacenaremos los contactos, no trataremos con un backend en un
servidor remoto por ahora, solo un arreglo con los datos de cada contacto y haremos
un API para interactuar con ese arreglo. Se ha expuesto el método listar que devolverá el
arreglo con todos los contactos, este método es el utilizado en el controlador ListaCtrl
para mostrar los contactos en la vista.
Ahora vamos a crear la nueva ruta para ver cada contacto de forma independiente, en
esta ocasión haremos uso del servicio $routeParams para saber que contacto es el que
se quiere ver.
Archivo: App/js/Config/rutas.js
1
2
3
4
5
$routeProvider
.when('/:id', {
templateUrl: vista('ver'),
controller: 'VerCtrl'
})
Crearemos un nuevo controlador llamado VerCtrl para mostrar el contacto.
Archivo: App/js/Controladores/VerCtrl.js
1
2
3
4
5
6
7
8
9
angular.module('ContactosApp')
.controller('VerCtrl', ['$scope', '$routeParams', 'contactos', '$location',
function ($scope, $routeParams, contactos, $location) {
if (contactos.ver($routeParams.id)) {
$scope.contacto = contactos.ver($routeParams.id);
} else {
$location.path('/');
};
}])
En este controlador haremos uso del servicio contactos para obtener la información del
contacto que se va a mostrar y el servicio $location. Comenzaremos por usar el método
ver del servicio contacto, si esta devuelve un contacto lo asignaremos al scope, en caso de
que el contacto no exista se redireccionará el navegador a la página principal mediante el
método path() del servicio $location. Esta comprobación nos dará la posibilidad de que
si el usuario intenta introducir un id que no existe en la URL es redireccionado hacia
la lista de contactos de forma automática. Antes de crear el método ver en el servicio
veamos la vista para esta ruta.
Capítulo 8: Rutas
125
Archivo: App/_vistas/ver.html
1
2
3
4
5
6
<div>
<span data-ng-repeat="(key, value) in contacto">
<strong>{{key | uppercase}}:</strong> {{value}} <br>
</span>
<a href="#/">Volver</a>
</div>
En esta vista iteraremos sobre cada campo del contacto y mostraremos sus datos, además
tendremos un vínculo para volver a la página principal. Ahora crearemos el método ver
en el servicio contacto.
1
2
3
ver: function(id){
return contactos[id] || false;
}
Exponiendo este método ver en el objeto que devuelve el servicio podremos ejecutar
la aplicación en el navegador y podremos visitar los dos contactos que pusimos en el
servicio. Como puedes comprobar ya tienes una aplicación que maneja rutas y navegas
sobre los contactos viendo la información sin recargar la página. Ahora agregaremos
algunas funcionalidades extra a la aplicación como borrar, editar y crear contactos
además un buscador para encontrar los contactos de forma fácil.
Comencemos por borrar un contacto que es la más fácil de las acciones. Para lograrlo
debemos crear el método borrar en el servicio.
1
2
3
borrar: function(id){
return contactos.splice(id,1).length ? true : false;
}
Borraremos el índice que se pase como valor a la función y devolveremos verdadero o
falso. En el controlador expondremos un método borrar en el $scope.
Capítulo 8: Rutas
1
2
3
4
5
6
7
126
$scope.borrar = function(){
if (contactos.borrar($routeParams.id)) {
$location.path('/');
} else {
console.log('No se ha podido eliminar el contacto.');
};
}
Mediante el método borrar que declaramos en el servicio se borrará el índice y se
redireccionará hacia la página principal, en caso de que el índice no pueda ser borrado
imprimiremos en la consola un mensaje de error.
1
<a href="#/">Volver</a> <a ng-click="borrar()" href="">Borrar</a>
En la vista agregamos el botón borrar que ejecutará la acción de borrar. Con esto ya
tendremos el comportamiento de borrar contactos terminados. Ahora para crear nuevos
contactos necesitamos crear una nueva ruta esta la llamaremos /nuevo. Para que esta
nueva ruta funcione correctamente debemos definirla antes de la ruta de ver contactos
ya que, si la definimos después, tendríamos un conflicto y siempre respondería la ruta de
ver y la palabra nuevo pasaría como parámetro de esa ruta. Si la definimos antes Angular
podrá acertar primero en la ruta /nuevo y en los demás casos lo responderá con la ruta
/:id.
1
2
3
4
5
6
7
8
.when('/nuevo', {
templateUrl: vista('nuevo'),
controller: 'NuevoCtrl'
})
.when('/:id', {
templateUrl: vista('ver'),
controller: 'VerCtrl'
})
Ahora veamos el controlador.
Capítulo 8: Rutas
127
Archivo: App/js/Controladores/NuevoCtrl.js
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('ContactosApp')
.controller('NuevoCtrl', ['$scope', 'contactos', '$location',
function ($scope, contactos, $location) {
var contacto = $scope.contacto = {};
$scope.crear = function(){
if (contactos.crear(contacto)) {
$location.path('/');
} else {
console.log('No se ha podido crear el contacto');
};
}
}]);
Utilizaremos un nuevo método en el servicio contactos para crear nuevos y haremos
una redireccion a / cuando el contacto se haya creado, en caso de error enviaremos un
mensaje a la consola. Veamos la vista.
Archivo: App/_vistas/nuevo.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<div>
<h3>Nuevo contacto</h3>
<form data-ng-submit="crear()">
<label for="nombre">Nombre: </label>
<input type="text" id="nombre" data-ng-model="contacto.nombre"><br>
<label for="email">Email: </label>
<input type="email" id="email" data-ng-model="contacto.email"><br>
<label for="tel">Tel: </label>
<input type="text" id="tel" data-ng-model="contacto.tel"><br>
<input type="submit" value="Crear">
<a href="#/">Volver</a>
</form>
</div>
Tendremos un formulario con la directiva ng-submit apuntando al método crear que
expusimos en el controlador. Esta directiva prevendrá el evento submit por defecto del
navegador y hará un sumbit con el método que le hemos indicado. Hay un campo para
cada dato del contacto con la directiva ng-model haciendo estos datos disponibles en el
controlador para crear el nuevo contacto. Una vez terminado esto ya tendremos lista la
funcionalidad de crear nuevos contactos.
Ahora haremos la de editar un contacto, para lograrlo debemos hacer varias modificaciones. En el controlador VerCtrl crearemos un nuevo método para editar el contacto.
Capítulo 8: Rutas
128
Archivo: App/js/Controladores/VerCtrl.js
1
2
3
$scope.editar = function(){
$location.path('/'+ $routeParams.id + '/editar');
};
En la vista de ver los contactos crearemos un vínculo que nos lleve a editar el contacto.
Archivo: App/_vistas/ver.html
1
<a data-ng-click="editar()" href="">Editar</a>
Ahora crearemos la nueva ruta para editar. Esta vez debajo de la ruta de ver ya que no
tendremos colisiones de ningún tipo al ser un patrón diferente.
1
2
3
4
.when('/:id/editar', {
templateUrl: vista('editar'),
controller: 'EditarCtrl'
})
La vista editar.html será esencialmente lo mismo que la vista de nuevos contactos.
Archivo: App/_vistas/editar.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<div>
<h3>Editando a {{contacto.nombre}}</h3>
<form data-ng-submit="guardar()">
<label for="nombre">Nombre: </label>
<input type="text" id="nombre" data-ng-model="contacto.nombre"><br>
<label for="email">Email: </label>
<input type="email" id="email" data-ng-model="contacto.email"><br>
<label for="tel">Tel: </label>
<input type="text" id="tel" data-ng-model="contacto.tel"><br>
<input type="submit" value="Guardar">
<a data-ng-click="cancelar()" href="">Cancelar</a>
</form>
</div>
En esta vista se utilizarán dos métodos, uno es para guardar el contacto mediante la
directiva ng-submit y la otra es cancelar que regresa a la vista del contacto. Estos dos
métodos los definiremos en el controlador.
Capítulo 8: Rutas
129
Archivo: App/js/Controladores/EditarCtrl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('ContactosApp')
.controller('EditarCtrl', ['$scope', 'contactos', '$location', '$routeParams',
function ($scope, contactos, $location, $routeParams) {
if (!$routeParams.id && !contactos.ver($routeParams.id)) {
$location.path('/')
};
var contacto = $scope.contacto = contactos.ver($routeParams.id);
$scope.cancelar = function(){
$location.path('/'+ $routeParams.id)
};
$scope.guardar = function(){
contactos.editar($routeParams.id, contacto)
$location.path('/'+ $routeParams.id)
}
}])
En el controlador lo primero que haremos es hacer una comprobación si nos han
pasado el id de un contacto existente, de lo contrario redireccionamos para la lista de
contactos. Exponemos el contacto que se desea editar a la vista rellenando los campos. El
método cancelar regresa a la vista del contacto mediante el servicio $location. El método
guardar envía al servicio el id y los nuevos datos del contacto mediante el método editar
y después redirecciona a la vista del contacto. Ahora veremos necesitamos crear ese
método en el servicio.
Archivo: App/js/Servicios/contactos.js
1
2
3
editar: function(id, contacto){
contactos[id] = contacto;
}
Con estas modificaciones tendremos listo la funcionalidad de editar contactos. Ahora
solo queda para terminar esta pequeña aplicación el buscador. Para agregarlo vamos a la
vista lista.html y le agregamos un input para buscar y un filtro en la directiva ng-repeat.
Capítulo 8: Rutas
130
Archivo: App/_vistas/lista.html
1
2
3
4
5
6
7
8
<div>
<input type="search" data-ng-model="buscar.nombre">
<ul>
<li data-ng-repeat="contacto in contactos | filter:buscar">
<a ng-href="#/{{$index}}">{{contacto.nombre}}</a>
</li>
</ul>
</div>
De esta forma ya está lista la aplicación, hemos hecho uso de gran cantidad de los
elementos explicados hasta ahora en el libro, y de otros elementos que aún no se han
detallado como el servicio $location que explicaremos más adelante.
Como habrás podido observar la URL tiene un # antes de todas las rutas de la aplicación.
Esto es debido a que no estamos utilizando el modo HTML5 del navegador. Este modo
puede ser activado mediante el $locationProvider en su propiedad html5Mode que
por defecto esta con un valor false. Por ejemplo, actualmente en la aplicación una ruta
luce de esta forma /#/1/editar y con el html5Mode activado sería de la siguiente forma
/1/editar.
Para cambiar al modo HTML5 hay que hacer varios ajustes en la aplicación. En el archivo
de rutas inyectamos el provider $locationProvider y ponemos la propiedad html5Mode
con un valor true.
Archivo: App/js/Config/rutas.js
1
2
3
4
5
6
angular.module('ContactosApp')
.config(['$routeProvider', '$locationProvider',
function ($routeProvider, $locationProvider) {
// ... demás codigo -$locationProvider.html5Mode(true);
}])
Después de activar el modo HTML5 debemos dar una dirección base al archivo index.html ya que este será requerido por Angular para manejar las rutas. Este elemento
debe tener en su atributo href la dirección base de nuestra aplicación ya que a partir de
esa dirección es que se crearán las rutas. Si tenemos la aplicación en la raíz del servidor
bastara solo con poner un / en el atributo, de no ser así debes especificar la ruta para
acceder a la aplicación.
Capítulo 8: Rutas
131
Archivo: App/index.html
1
2
3
4
<head>
<title>Contactos</title>
<base href="/">
</head>
Si usas Apache como tu servidor puedes crear un host virtual como el que se crea en el
ejemplo detallado en el apartado Entorno de desarrollo en el que se especifica una línea
que hará que todas las peticiones se hagan al archivo index.html.
Si usas NodeJS también puedes usar el servidor descrito en el apartado Entorno de
desarrollo donde se servirá el archivo index.html para todas las peticiones GET.
Lo próximo que debes hacer es eliminar todos los # que hay en los <a href=""> en las
vistas de la aplicación. Después ya podrás visitar la aplicación, en esta ocasión solo se
verán direcciones amigables.
Soporte
No hay que preocuparse por nuestra aplicación cuando sea ejecutada en un
navegador que no soporte HTML5. Angular es lo suficiente inteligente para
cambiar el html5Mode a falso en caso de que los navegadores no tengan soporte
para este modo.
Plantillas
Hasta el momento hemos visto cómo utilizar plantillas utilizando las dos propiedades
de la configuración del método when. De la primera forma escribiendo directamente
la plantilla como valor de la propiedad template, y la segunda especificando una url
para que sea cargada cuando se visite la ruta. Como es una mala práctica escribir las
vistas directamente en la lógica de la aplicación, la primera opción queda totalmente
descartada. La segunda opción donde cargamos la vista cuando esta es requerida, viene
con un costo adicional y es que cuando se visite la ruta se hará una petición HTTP para
obtener la plantilla.
Para solucionar problemas como estos, Angular provee una vía para crear las plantillas
dentro de la misma vista HTML de la aplicación, la cual es cargada la primera vez
que abrimos la página. Esto lo podemos lograr haciendo uso de las etiquetas <script>
de HTML. Angular reconocerá todas las etiquetas <script> que sean de tipo text/ngtemplate y las verá como plantillas. Estas las podremos utilizar en la aplicación mediante
la directiva ng-include o en la propiedad templateUrl de la configuración de las rutas.
Capítulo 8: Rutas
132
Ahora veremos este funcionamiento en un ejemplo. Para comenzar crearemos una
aplicación con solo dos rutas, no es necesario crear controladores, solo la configuración
especificando la plantilla de la ruta.
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app', ['ngRoute'])
.config(Rutas);
Rutas.$inject = ['$routeProvider'];
function Rutas($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'plantilla-inicio.html'
})
.when('/acerca-de', {
templateUrl: 'plantilla-acerca-de.html'
})
}
Ahora que tenemos la configuración para las rutas, solo necesitamos crear las plantillas
para cada una de ellas. Normalmente estas se crearían como archivos separados, pero
ahora haremos uso de las etiquetas <script> de tipo ng-template para hacerlas disponibles
en la aplicación tan rápido como esta cargue.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<ng-view></ng-view>
<script type="text/ng-template" id="plantilla-inicio.html">
<h1>Esta es la vista para la ruta de Inicio</h1>
<p>Esta plantilla está incluida directamente en el contenido de la página pr\
incipal de la aplicación mediante las etiquetas script de tipo <strong>ng-templa\
te</strong></p>
Desde aquí podrás visitar la página de <a href="#acerca-de">Acerca De</a>
</script>
<script type="text/ng-template" id="plantilla-acerca-de.html">
<h1>Vista para hablar acerca de nosotros</h1>
<p>Esta plantilla como la de inicio, es creada mediante las etiquetas <sc\
ript> de tipo <strong>ng-template</strong></p>
</script>
</body>
Como puedes observar las dos plantillas están definidas en el mismo archivo que incluso
es el índice de la aplicación donde reside la etiqueta <ng-view>. La propiedad id de la
etiqueta script indica el nombre de la plantilla por la cual Angular la reconocerá y cargará
en el $templateCache que hablaremos más adelante.
Capítulo 8: Rutas
133
Si observas en la pestaña de red del navegador puedes observar que cambiando de una
ruta hacia la otra no requiere una petición extra al servidor. Ahora las dos plantillas están
cargadas desde que se carga la página principal.
Es importante mencionar que para que estas plantillas sean correctamente reconocidas
es necesario que sea definidas dentro del rango de la aplicación. Con esto quiero decir que
las etiquetas script de tipo ng-template tienen que ser descendientes del elemento donde
se define la aplicación con la directiva ng-app.
Plantillas en cache
Como explique anteriormente haciendo uso de las etiquetas script para crear las plantillas, podría ahorrarnos algunas peticiones al servidor remoto. Existe otra vía por
la que podremos tener disponible todas las plantillas desde el momento en que se
carga la aplicación. Esta vía que explicaré a continuación es haciendo uso del servicio
$templateCache.
Este servicio es utilizado por Angular para el manejo de la cache de todo tipo de plantillas.
Cuando hacemos uso de cualquier tipo de plantilla, la primera ocasión en que se utilice
una nueva plantilla, Angular la incluirá en la cache. Esta cache es manejada por el servicio
$templateCache, Ahora explicare su funcionamiento y su utilización.
La otra vía para cargar plantillas directamente desde que se carga la aplicación es haciendo uso del servicio $templateCache. Este servicio lo podremos inyectar en el método run
de la aplicación e insertarle las plantillas directamente en ese momento. Para cuando el
usuario solicite la aplicación, las plantillas estarán directamente cargadas desde que se
corra el método run.
Para ver un ejemplo de su funcionamiento, utilizaremos el ejemplo de las etiquetas script
con ng-template y lo convertiremos haciendo uso del $templateCache.
1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('app', ['ngRoute'])
.config(Rutas)
.run(Plantillas);
Rutas.$inject = ['$routeProvider'];
function Rutas($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'plantilla-inicio.html'
})
.when('/acerca-de', {
templateUrl: 'plantilla-acerca-de.html'
})
}
Capítulo 8: Rutas
14
15
16
17
18
19
20
21
22
23
24
25
134
Plantillas.$inject = ['$templateCache'];
function Plantillas($templateCache){
$templateCache.put('plantilla-inicio.html', '<h1>Esta es la vista para la ru\
ta de Inicio</h1><p>Esta plantilla está incluida directamente en el contenido de\
la página principal de la aplicación mediante las etiquetas script de tipo <str\
ong>ng-template</strong></p>Desde aquí podrás visitar la página de <a href="#ace\
rca-de">Acerca De</a>');
$templateCache.put('plantilla-acerca-de.html', '<h1>Vista para hablar acerca\
de nosotros</h1><p>Esta plantilla como la de inicio, es creada mediante las eti\
quetas <script> de tipo <strong>ng-template</strong></p>');
}
Como habrás podido observar, con el uso del servicio obtenemos el mismo comportamiento que utilizando las plantillas de ng-template. Inyectando las plantillas en el método
run de la aplicación, estas estarán disponibles desde que la aplicación esté lista.
Para poner las plantillas dentro de la cache hemos hecho uso del método put del
servicio. Este método acepta dos parámetros, el primero es una cadena de texto con el
identificador de la plantilla. El segundo parámetro es Una cadena de HTML que será la
plantilla que será la plantilla para ese identificador.
Este servicio no es más que un acceso directo creado sobre el servicio $cacheFactory.
Esto significa que dispondrá de los mismos métodos que los objetos de cache comunes.
Además, mediante el servicio $cacheFactory puedes acceder a las plantillas cargadas por
$templateCache utilizando el id templates.
Precargando plantillas
A partir de la versión 1.3 de Angular, se incluyó un nuevo servicio para precargar
plantillas en el $templateCache. Cuando estamos en una página de la aplicación y nos
desplazamos hacia otra, la aplicación hace una petición XHR para obtener la plantilla
que necesitamos mostrar. Este proceso sucede en el momento en que el usuario da clic
en la acción para cambiar la página. Si el contenido de la petición es grande, tendremos el
usuario esperando a que se termine de obtener el contenido. Con el nuevo servicio cargar
las plantillas de otras páginas antes de que el usuario de clic para cambiar hacia ellas. De
esta forma las peticiones se realizarán mientras el usuario navega por la aplicación y así
evitaremos tiempo de carga al usuario.
Ya hemos visto otras formas de hacer cache de las plantillas, pero esta nos permitirá hacer
cache de la que necesitamos específicamente. Este proceso de hacer cache solo de las
plantillas que necesitamos utilizar ayudará a mejorar el rendimiento de la aplicación.
Para utilizarlo solo tendremos que inyectar el servicio $templateRequest y solicitar la
plantilla que queremos guardar en cache.
Capítulo 8: Rutas
135
Ahora veremos un ejemplo de cómo funciona el servicio $templateRequest. Para ello
crearemos una simple aplicación de dos páginas, desde que visitemos la primera página
automáticamente se cargara la plantilla de la segunda. Guardando la segunda página en
la cache antes de que el usuario la solicite, evitará que tenga que esperar a que se resuelva
cuando desee ir hacia ella.
Lo primero que vamos a hacer para el ejemplo es crear una página index.html que
contendrá el cuerpo de la aplicación. Después crearemos otros dos archivos HTML que
servirán como plantillas de la página inicio y de la página nosotros.
index.html
1
2
3
4
5
...
<body>
<ng-view></ng-view>
</body>
...
inicio.html
1
2
3
4
5
<h1>Esta es la página de inicio</h1>
<p>Accediendo a esta página se cargara el contenido de la página <strong>/nosotr\
os</strong> de forma automática para evitar tiempo de carga cuando el usuario de\
click para navegar hacia ella.</p>
<a href="#/nosotros">Ir a Nosotros</a>
nosotros.html
1
2
3
<h1>Nosotros</h1>
<p>Esta plantilla ha sido cargada previamente mediante el servicio <strong>$temp\
lateRequest</strong></p>
Ahora necesitamos definir las dos rutas y el controlador para la página principal. El
controlador se encargará de cargar la plantilla de la página de nosotros mediante el
servicio $templateRequest.
Capítulo 8: Rutas
136
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
angular.module('app', ['ngRoute'])
.config(Rutas)
.controller('AppCtrl', AppCtrl);
Rutas.$inject = ['$routeProvider'];
function Rutas($routeProvider){
$routeProvider
.when('/', {
templateUrl: 'inicio.html',
controller: 'AppCtrl'
})
.when('/nosotros', {
templateUrl: 'nosotros.html'
})
}
AppCtrl.$inject = ['$scope', '$templateRequest'];
function AppCtrl($scope, $templateRequest) {
$templateRequest('nosotros.html');
}
Como habrás podido observar, estamos solicitando la página nosotros.html dentro del
controlador de la página de inicio. Con las herramientas de desarrollo del navegador
puedes observar como cuando se abre la página principal se carga la plantilla inicio para
mostrar como ruta por defecto, pero además se carga la página de nosotros para cuando
el usuario necesite visualizar su contenido se muestre de forma instantánea.
Haciendo un buen uso de este servicio evitaremos que el usuario espera por la aplicación.
Esto hará que la estancia en la aplicación sea más agradable para el usuario, además de
que mejorará visualmente el rendimiento de la aplicación.
El servicio $route
El servicio $route esta siempre observando a $location.url() para cuando haga algún
cambio este responder si alguna de las rutas definidas coincide con la nueva URL. Este
servicio solo tiene un método reload() el cual hace que se recargue la ruta actual incluso
aunque no se hayan cambiado los valores en el servicio $location. Debido a la recarga el
controlador es reinstanciado creando un nuevo $scope.
Este servicio además tiene dos propiedades. Una es routes la cual es un objeto que tiene
como propiedades todas las rutas declaradas en la aplicación desde la cual podremos
Capítulo 8: Rutas
137
acceder a las propiedades de cada una. La otra propiedad es current que es un objeto
y tendremos disponible varios datos interesantes que podremos usar en la aplicación.
Veremos ahora las propiedades del objeto current del servicio $route.
$route.current.controller: Devuelve el nombre del controlador para la ruta actual.
$route.current.locals: Es un objeto que tiene la referencia al $scope actual y el $template que se usa en la ruta actual.
$route.current.originalPath: Devuelve el camino de la ruta actual.
$route.current.template: Devuelve la plantilla para la ruta actual.
$route.current.params: Devuelve un objeto con todos los parámetros que le han sido
pasados a la ruta actual.
En caso de que necesites especificar valores en la ruta para luego usarlos dentro de la
vista o el controlador puedes hacerlo dentro del objeto de configuración de la ruta como
una propiedad más del objeto y esta estará disponible en $route.current.tuPropiedad
veamos un ejemplo.
Archivo: App/js/Config/Routes.js
1
2
3
4
5
6
7
8
9
angular.module('miApp')
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/ruta', {
template: '{{propiedad}}',
controller: 'RutaCtrl',
miPropiedad: 'Esta propiedad está definida en la ruta'
})
}])
En el archivo de configuración de la ruta definimos la ruta, dentro del objeto de
configuración que le pasamos como segundo parámetro a when() definimos la propiedad
miPropiedad. Esto lo hacemos como mismo definimos las propiedades controller y
template, en esta última mostraremos el texto de la propiedad que expondremos en el
controlador.
Archivo: App/js/Controllers/RutaCtrl
1
2
3
4
angular.module('miApp')
.controller('RutaCtrl', ['$scope', '$route', function ($scope, $route) {
$scope.propiedad = $route.current.miPropiedad;
}])
Capítulo 8: Rutas
138
Al visitar esta ruta en la aplicación podrás comprobar que el texto que definimos en la
propiedad del objeto de configuración de la ruta ahora se muestra en la vista. Esto puede
ser utilizado para varias configuraciones como por ejemplo definir el título de la página
en la misma configuración de la ruta y después mostrarlo de forma automática en todas
las páginas. Sobre este tema hablaremos después de los eventos.
Cambio de parámetros en la ruta
A partir de la versión 1.3 de Angular, el servicio $route tiene otro método además del
explicado anteriormente reload. Este nuevo método nos permite desde el código de la
aplicación cambiar parámetros de la ruta. El nuevo método es updateParams, este acepta
un objeto de tipo llave:valor donde la llave es el nombre del parámetro a actualizar y el
valor será el nuevo valor que obtendrá el parámetro.
Cuando estamos creando una aplicación, queremos que se pueda regresar a esta a través
de un bookmark para facilitar un acceso directo a lugares específicos. Esta funcionalidad
ya la tenemos actualmente sin hacer algún cambio a la aplicación. Pero si los parámetros
de la ruta son utilizados para organizar el comportamiento de la aplicación no teníamos
la posibilidad de cambiarlos. Ahora con el nuevo método del servicio $route podremos
actualizar los parámetros de forma muy sencilla.
Supongamos una aplicación que es una lista de productos con nombre, valor, votos y ventas.
En esta tendremos un elemento de tipo select para tener la posibilidad de organizar la
lista por los diferentes parámetros. Además, haremos que el parámetro de organización
se pueda especificar en la url. De esta forma cuando se navegue directamente a esta
dirección, con un parámetro que indique la organización, la aplicación organizará de
forma automática dependiendo del parámetro. Haciendo uso del nuevo método del
servicio $route, cambiaremos el parámetro de organización de la ruta para que cuando se
haga un marcador esta se guarde con la nueva organización.
Vamos a ver una imagen de la aplicación terminada y luego iremos describiendo por
partes el proceso completo.
139
Capítulo 8: Rutas
Listado de productos
Para comenzar creamos el modulo y especificamos ngRoute como dependencia. Después
creamos un bloque de configuración donde crearemos la ruta.
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
template: 'Inicio'
})
.when('/productos/:orden?', {
templateUrl: 'views/productos.html',
controller: 'ProductosCtrl',
controllerAs: 'vm'
});
});
Como habrás podido observar, la ruta productos tiene un parámetro orden y además un
símbolo ?, este signo es utilizado para hacer que el parámetro sea opcional. Ahora que
ya tenemos la ruta, necesitamos crear el controlador. Inyectamos los servicios $routeParams y $route para poder utilizarlos posteriormente. Lo primero que necesitamos
hacer es especificar un orden por defecto para la lista. Después necesitamos un arreglo
con los objetos que se mostraran en la lista, usualmente estos datos se obtendrían desde
un servidor remoto.
Capítulo 8: Rutas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
140
.controller('ProductosCtrl', function ($routeParams, $route) {
var vm = this;
vm.orden = $routeParams.orden || '-precio';
vm.productos = [
{ nombre: 'Samsung Galaxy S4', precio: 198.99, puntos: 175, ventas: 4718 },
{ nombre: 'Samsung Galaxy S3', precio: 105.99, puntos: 196, ventas: 1820 },
{ nombre: 'Asus Zenfone 2', precio: 179.99, puntos: 127, ventas: 716 },
{ nombre: 'HTC Desire 620', precio: 199.99, puntos: 166, ventas: 914 },
{ nombre: 'HTC One M7', precio: 175.95, puntos: 1694, ventas: 1589 },
{ nombre: 'LG L Bello', precio: 149.99, puntos: 1211, ventas: 891 },
{ nombre: 'Motorola Moto X 2', precio: 219.99, puntos: 1865, ventas: 6174 }
];
})
Ahora vamos a crear la vista para mostrar la lista de los productos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="row">
<table class="table">
<tr>
<th>Nombre</th>
<th>Precio</th>
<th>Puntos</th>
<th>Ventas</th>
</tr>
<tr ng-repeat="producto in vm.productos | orderBy: vm.orden">
<td>{{producto.nombre}}</td>
<td>{{producto.precio | currency}}</td>
<td>{{producto.puntos}}</td>
<td>{{producto.ventas}}</td>
</tr>
</table>
</div>
Hasta el momento la aplicación ya es funcional. Podremos navegar hacia la ruta productos y se mostrará la lista de los productos, e incluso si queremos organizar por alguno
de las columnas podríamos navegar hacia los productos especificando un orden. Como
ejemplo podremos visitar la ruta /#/productos/ventas la cual mostrará la lista ordenada
por ventas de menor cantidad a mayor. Ahora solo nos queda crear el elemento select
con las opciones para ordenar los elementos. Primero definiremos las ordenes en el
controlador. También crearemos un método para ejecutarlo con la directiva onChange
Capítulo 8: Rutas
141
que pondremos en el elemento select. En este método utilizaremos el servicio route con su
nueva funcionalidad updateParams. Pasaremos como parámetro un objeto con el nuevo
orden para que, al ser cambiado, este actualice la ruta con el nuevo parámetro de orden.
1
2
3
4
5
6
7
8
9
10
11
vm.organizar = [
{ val: '-puntos', texto: 'Mayor Puntuado' },
{ val: 'puntos', texto: 'Menor Puntuado' },
{ val: '-ventas', texto: 'Más Vendido' },
{ val: 'ventas', texto: 'Menos Vendido' },
{ val: 'precio', texto: 'Menor Precio' },
{ val: '-precio', texto: 'Mayor Precio' }
];
vm.cambiarOrden = function () {
$route.updateParams({ orden: vm.orden });
}
Ahora solo nos queda actualizar la vista con el elemento select.
1
2
3
4
5
6
7
8
9
<div class="row">
<div class="col-md-12 master">
<label for="orden">Organizar por:</label>
<select id="orden" name="orden" ng-model="vm.orden"
ng-change="vm.cambiarOrden()"
ng-options="orden.val as orden.texto for orden in vm.organizar">
</select>
</div>
</div>
Gracias a la directiva ng-options se agregarán las opciones que especificamos en el
controlador al elemento select. Como puedes observar al hacer algún cambio en el orden,
este actualiza la lista y a la vez la url con el nuevo parámetro de organización.
Eventos
El modulo ngRoute nos provee de 4 eventos que son disparados en determinados
momentos en que se realiza el proceso de cambio de rutas. Estos eventos son lanzados
sobre el servicio $rootScope que mediante el método $on. No detallaré los eventos y
su propagación en este capítulo, solo hablaré de los eventos del módulo ngRoute. Si no
entiendes algo relacionado con los eventos en general no te preocupes, más adelante lo
entenderás perfectamente cuando se trate el tema de eventos.
Capítulo 8: Rutas
142
El primer evento que trataremos es el $routeChangeStart. Este evento se disparará en el
momento antes de que la ruta se cambie. En este momento el servicio comienza a resolver
las dependencias que se hayan definido en la propiedad resolve de la configuración de
la ruta, así como la plantilla que se mostrará al usuario. Cuando se resuelvan todas las
dependencias se disparará el evento $routeChangeSuccess.
Al escuchar este evento cuando es disparado recibiremos tres parámetros, el primero es
un objeto evento con alguna información relacionada con el evento en sí. El segundo
parámetro es un objeto con la ruta que se comenzará a cargar. En este objeto tendremos
disponible las mismas propiedades que el objeto current del servicio $route pero de la
ruta que se cargara. El tercer parámetro es otro objeto de igual forma al anterior, pero
con la información de la ruta actual antes de comenzar a cambiar.
Conociendo sobre el evento $routeChangeStart vamos a utilizarlo para definir el título
de las páginas en el objeto de definición de cada ruta y mediante el evento actualizarlo
en la etiqueta <title> del <head>. Veamos un ejemplo del archivo de rutas.
Archivo: App/js/Config/rutas.js
1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('miApp')
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/', {
template: '<h1>Página de inicio</h1><br><a href="#/contacto">Contacto</a\
>',
titulo: 'Página de inicio'
})
.when('/contacto', {
template: '<h1>Contacto</h1><br><a href="#/">Volver</a>',
titulo: 'Página de contacto'
})
}])
Es muy simple, solo dos rutas y las propiedades título de cada una para hacerlas
disponibles en la vista. Veamos el archivo index.html.
Capítulo 8: Rutas
143
Archivo: App/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" data-ng-app="miApp">
<head>
<meta charset="UTF-8">
<title>{{titulo}}</title>
</head>
<body>
<div data-ng-view></div>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/Config/Bootstrap.js"></script>
<script src="js/Config/rutas.js"></script>
</body>
</html>
En la etiqueta <title> haremos uso del servicio $rootScope para obtener la propiedad
título ya que en este servicio es donde se escuchará el evento. El archivo Bootstrap.js es
el que contendrá la configuración del evento, vamos a verlo.
Archivo: App/js/Config/Bootstrap.js
1
2
3
4
5
6
angular.module('miApp')
.run(['$rootScope', function ($rootScope) {
$rootScope.$on('$routeChangeStart', function(evento, siguiente, actual){
$rootScope.titulo = siguiente.titulo || 'Titulo por defecto';
});
}])
En este archivo se ha utilizado el método run del módulo. Este método es instanciado
en el momento en que angular ha terminado de cargar toda las dependencias y módulos
de la aplicación. Es un buen lugar para escuchar los eventos y configurar el $rootScope
para que tome acciones en cada uno de ellos.
He inyectado el $rootScope en el método run y mediante el método $on escucharemos
el evento $routeChangeStart que es el primer parámetro que le pasamos a este método
como cadena de texto. El segundo parámetro es una función que será ejecutada en el
momento en que se dispare el evento. Como mencionamos anteriormente el evento
inyecta tres parámetros. Para este ejemplo solo nos interesa el segundo parámetro que
es el que tendrá el objeto de configuración de la ruta que se va a cargar. Asignamos una
Capítulo 8: Rutas
144
propiedad título en el $rootScope con el título que viene en la configuración de la ruta
o en caso de que no venga ningún título pasaremos un título por defecto.
Es suficiente, con el código anterior hemos escuchado al evento y hemos tomado las
acciones correspondientes. Siempre que se dispare el evento $routeChangeStart el
servicio $rootScope estará escuchando y hará el cambio en la propiedad título. De esta
forma siempre tendremos el título de la página actualizado. En caso de que necesites
generar un título dependiendo de datos en el controlador, puedes inyectar el servicio
$rootScope y cambiar la propiedad título. El titulo definido dentro del controlador
topara precedencia ya que el evento es lanzado antes de que sea instanciado el controlador. Cuando el controlador pueda cambiar el titulo remplazará el que habrá puesto por
defecto el evento.
Otro de los eventos es $routeChangeSuccess, este es disparado después de que se hayan
resuelto todo lo dispuesto en la propiedad resolve del objeto de configuración de la ruta.
Este evento recibe los mismos parámetros que el $routeChangeStart.
En caso de que alguno de los elementos de la propiedad resolve del objeto de configuración de la ruta no se resuelva, se disparará el evento $routeChangeError. Este evento
de igual forma recibe los tres parámetros que reciben los demás eventos anteriores.
Además, recibe un cuarto parámetro que es el mensaje de error devuelto por la promesa
rechazada.
El último de los eventos que nos provee el módulo ngRoute es $routeUpdate. Este es
disparado solo si la propiedad reloadOnSearch se ha definido con un valor false y se
cambian los valores de $location.search() o $location.hash() pero aún se usa la misma
instancia del controlador.
Los anteriormente mencionados son los eventos que añade el módulo ngRoute al
framework. Estos no son los únicos, existen otros que veremos más adelante en el
Capítulo de Eventos.
Una de los usos que podemos darles a los eventos de las rutas, por ejemplo, si estamos
en una vista editando mediante un formulario y los cambios no han sido guardados aún.
Mediante el evento $routeChangeStart podríamos cancelar el cambio de ruta y mostrar un
mensaje de alerta al usuario para que no pierda los cambios. Vamos a ver cómo quedaría
el código para este ejemplo.
Primero crearemos dos rutas para navegar desde una hacia la otra. A estas le agregaremos
una propiedad id para poder comprobar en qué ruta estamos cuando escuchemos el
evento.
Capítulo 8: Rutas
1
2
3
4
5
6
7
8
9
10
11
12
145
.config(function ($routeProvider) {
$routeProvider
.when('/editar', {
id: 'editar',
template: 'Editar <a href="#lista">Volver a la lista</a>',
controller: 'EditarCtrl'
})
.when('/lista', {
id: 'lista',
template: 'Lista'
})
})
Ahora en el controlador inyectaremos $rootScope para escuchar el evento * $routeChangeStart. También he definido una variable *editando que servirá como condición para
si esta tiene valor verdadero prevenga que naveguemos hacia otra ruta.
1
2
3
4
5
6
7
8
9
.controller('EditarCtrl', function ($rootScope, $scope) {
$scope.editando = true;
$rootScope.$on('$routeChangeStart', function (evento, siguiente, actual) {
if (!!actual && actual.id === 'editar' && $scope.editando) {
evento.preventDefault();
alert('Debes guardar los cambios antes de salir.')
}
});
});
Mediante el método $on del rootScope escuchamos el evento $routeChangeStart. Primero
comprobamos que exista un estado actual después que su id es la que estamos actualmente que es la de editar y por ultimo si estamos editando. En caso de que la condición se
cumpla evitaremos que se navegue hacia otra ruta mediante el objeto evento ejecutando
el método preventDefault() y luego alertaremos al usuario que debe guardar los cambios
antes de salir.
El servicio $location
Hasta el momento hemos hecho uso del servicio $location aunque aún no se ha detallado
sus usos, métodos y propiedades. El servicio $location es una interface para tratar con
el objeto window.location de javascript. Este tiene algunas diferencias que lo hacen más
útil tratándose de que está completamente relacionado con el ciclo de vida y las fases de la
aplicación. En él se exponen las propiedades con getters y setters al estilo jQuery. Tiene
Capítulo 8: Rutas
146
integración con el API de HTML5 con soporte para navegadores viejos. Esto entre otras
son las ventajas de utilizar el servicio $location en vez de utilizar el nativo de javascript
window.location.
Este servicio tiene una desventaja en cuanto al objeto windows.location y es que no
puede recargar la página por completo cuando la URL del navegador cambia ya que
solo recarga porciones de la aplicación. Para hacer una recarga completa de la página
se deberá utilizar el servicio $window.location.href.
A diferencia del nativo de javascript, este está totalmente integrado con el framework.
Cambios en la URL del navegador son reflejados directamente en el servicio $location
y viceversa. Ahora veremos los métodos y propiedades que tiene el servicio $location.
path(): Este método si es ejecutado sin parámetros devuelve el camino en el que estamos
actualmente. Si se le pasa un parámetro con una cadena de texto este servicio hará que se
navegue hacia esa dirección. Este es el método que hemos estado usando en los ejemplos
anteriores para cambiar de una ruta a otra dentro de la aplicación. Este método no
produce una recarga total de la página, solo se recargan las partes necesarias. Además,
este método interactúa directamente con el API de Historial de HTML5 de forma que,
si el usuario presiona el botón Atrás del navegador, este podrá navegar a la ruta anterior
sin recargar la página.
replace(): En algunas ocasiones no que remos que el comportamiento producido por
la función path() unido al Api historial de HTML5 guarde una referencia a la página
anterior. El método replace hará que se remplaza el historial y no que se cree un nuevo
registro de la página por la que se navega. Esto hace que al presionar el botón Atrás del
navegador, no se navegue a la ruta anterior. Este método es muy útil para casos como
cuando se redirige al usuario después de hacer login y no queremos que regrese a la
redirección. Un ejemplo de su uso seria.
1
2
$location.path('/dashboard');
$location.replace();
Estos métodos se pueden ejecutar en cadena como se realiza en jQuery.
1
$location.path('/dashboard').replace();
absUrl(): Este método devuelve la dirección absoluta con todos los segmentos codificados. Será exactamente lo que podemos observar en la barra de dirección del navegador.
hash(): Devuelve el fragmento de los hash que existan en la URL. En caso de que se le pase
un parámetro cadena de texto se cambiará el hash hacia el nuevo que se ha introducido.
Capítulo 8: Rutas
1
2
147
$location.hash(); // #procesos-sistema
$location.hash('procesos-usuario'); // se cambiará a #procesos-usuario
search(): Devuelve un objeto con los fragmento search que existan en la URL. En caso
de que se le pase un parámetro cadena de texto previamente codificada o un arreglo de
llaves y valores, se cambiará hacia la nueva dirección de búsqueda.
1
2
3
$location.search(); // persistir=true devilverá Object {persistir: "true"}
$location.search('persistir=true'); // se cambiará a ?persistir=true
$location.search({persistir:true}); // se cambiará a ?persistir=true
host(): Devolverá el host donde se está ejecutando la aplicación sin el método por la que
se accede ni los demás segmentos de la ruta.
port(): Devolverá el puerto por el cual se accede a la aplicación.
protocol(): Devolverá el protocolo por el cual se accede a la aplicación ya sea http o https.
url(): Devolverá la url del navegador sin prefijo, host o método. Esta incluye los segmentos de búsqueda y el hash. Este método puede recibir un parámetro de tipo cadena de
texto. Si le pasamos ese parámetro se cambiará la url con los segmentos de búsqueda y
hash a la nueva url.
1
2
$location.url(); // devuelve /12/editar?persistir=true#credito
$location.url('/12#credito'); // cambiará a la nueva URL
Este servicio proporciona dos eventos que pueden ser utilizados para tomar acciones de
acuerdo a los cambios en la URL. El primero es $locationChangeStart que es disparado
exactamente antes de que se produzca el cambio en la URL. Este evento puede ser
detenido en caso de que sea necesario llamando al método preventDefault en el evento.
Este evento recibe tres parámetros, el primero es el objeto evento con el cual podremos
prevenir que se produzca el cambio de la url, el segundo es la url absoluta hacia donde se
va y el tercero es la url absoluta actual.
El otro evento que proporciona este servicio es $locationChangeSuccess. Este es disparado cuando la ruta se ha terminado de cambiar. Este evento también recibe los mismos
tres parámetros que el evento $locationChangeStart.
Lo anteriormente mencionado es lo relacionado con el servicio $location. Este es el que
estaremos utilizando para movernos de un lugar a otro dentro de la aplicación.
Capítulo 9: Eventos
Hasta ahora hemos visto varios eventos y su funcionamiento, por ejemplo, los eventos del
servicio $route. Angular no nos limita a solo esos eventos, permite que crees tus propios
eventos y que tomes acciones en dependencia de lo que suceda en tu aplicación. Esta
será una de las vías que tendrás para intercambiar información dentro de la aplicación
en tiempo real de acuerdo a las interacciones del usuario.
Como se ha explicado anteriormente los scopes de la aplicación pertenecen a un árbol
jerárquico donde el padre de todos los scopes es $rootScope y tendrá los scopes de
la aplicación como hijos o nietos sucesivamente. Existen dos formas de propagar los
eventos en Angular, estos se propagan en dos direcciones, uno hacia arriba o sea los
padres del scope actual y la segunda es hacia abajo a los scopes hijos. Hay que tener
en cuenta que propagar eventos en la dirección equivocada o de manera global puede
ocasionar mal funcionamiento en la aplicación.
Propagando eventos hacia los scopes padres
Una de las dos formas de propagar eventos es haciéndolo hacia los scopes padres,
lo podemos realizar mediante el método $emit del scope. Este método recibe dos
parámetros, el primero es el nombre del evento de tipo cadena de texto por el cual
será escuchado y el segundo es un objeto con los parámetros que recibirá el disparador.
Cuando el método $emit es llamado se alertará a los scopes padres para que tomen
acciones al escuchar el evento. Veamos un ejemplo.
Archivo: index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div data-ng-controller="PadreCtrl">
<h3>Scope Padre</h3>
<div data-ng-controller="HijoCtrl">
<h4>Scope Hijo</h4>
<button data-ng-click="click()">Click</button>
</div>
</div>
<script src="lib/angular/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Controllers/PadreCtrl.js"></script>
<script src="js/Controllers/HijoCtrl.js"></script>
</body>
148
Capítulo 9: Eventos
149
Archivo: App/js/Controllers/PadreCtrl.js
1
2
3
4
5
6
angular.module('miApp')
.controller('PadreCtrl', ['$scope', function ($scope) {
$scope.$on('eventoHijo', function(evt,arg){
console.log(arg.msg);
})
}])
Archivo: App/js/Controllers/HijoCtrl.js
1
2
3
4
5
6
angular.module('miApp')
.controller('HijoCtrl', ['$scope', function ($scope) {
$scope.click = function(){
$scope.$emit('eventoHijo', {msg:'Se ha hecho clic en el scope Hijo.'});
};
}])
En el ejemplo anterior propagaremos un evento eventoHijo al hacer clic en el botón del
scope hijo que enviará un objeto con un mensaje. En el controlador padre escucharemos
el evento eventoHijo y enviaremos a la consola el mensaje que recibimos con el evento.
Propagando eventos hacia los scopes hijos
La segunda forma de propagar eventos es haciéndolo hacia los hijos del scope actual y
esto los realizaremos mediante el método $broadcast del scope. Este método recibe los
mismos parámetros que el método $emit. Veamos un ejemplo.
Archivo: index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div data-ng-controller="PadreCtrl">
<h3>Scope Padre</h3>
<button data-ng-click="click()">Click</button>
<div data-ng-controller="HijoCtrl">
<h4>Scope Hijo</h4>
</div>
</div>
<script src="lib/angular/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/Controllers/PadreCtrl.js"></script>
<script src="js/Controllers/HijoCtrl.js"></script>
</body>
Capítulo 9: Eventos
150
Archivo: App/js/Controllers/PadreCtrl.js
1
2
3
4
5
6
7
8
angular.module('miApp')
.controller('PadreCtrl', ['$scope', function ($scope) {
$scope.click = function(){
$scope.$broadcast('eventoPadre', {
msg:'Se ha hecho clic en el scope Padre.'
});
};
}])
Archivo: App/js/Controllers/HijoCtrl.js
1
2
3
4
5
6
angular.module('miApp')
.controller('HijoCtrl', ['$scope', function ($scope) {
$scope.$on('eventoPadre', function(evt,arg){
console.log(arg.msg);
})
}])
En el ejemplo anterior se envía el evento eventoPadre desde el scope padre hacia el scope
hijo y se escribe el mensaje en la consola cuantas veces sea disparado.
Escuchando eventos
Ya hemos visto como disparar los eventos en ambas direcciones, desde los padres hacia
los hijos y desde los hijos a los padres. Ahora solo queda escuchar estos eventos y
realizar acciones cuando cada uno ocurra. En los ejemplos anteriores puedes observar
que escuchar el evento lo hacemos mediante el método $on() del scope donde tomaremos
las acciones. Este recibe dos parámetros, el primero es el nombre del evento que se está
escuchando y el segundo es una función que será llamada en el momento en que el evento
sea disparado.
Esta función siempre recibirá como primer parámetro el objeto evento de angular, ya
sea un evento creado por nosotros o uno nativo de angular como $viewContentLoaded.
Además, esta función recibirá todos los parámetros que se le envíen desde el disparador
o sea $emit o $broadcast.
Capítulo 9: Eventos
151
Objeto Evento de Angular
El objeto evento que es entregado en cada ocasión que escuchamos un disparador nos
brinda información sobre el evento en sí.
targetScope: (objeto) Es el scope de donde se ha emitido el evento ya sea con $emit
o con $broadcast. currentScope: (objeto) Es el scope que se encargará de manejar el
evento. name: (string) Es el nombre del evento que fue emitido y estamos manejando
en estos momentos. stopPropagation: (función) Esta función cancela la propagación del
evento. preventDefault: (función) Esta función cambia la propiedad defaultPrevented
a true. Aunque no cancela la propagación del evento informa a los scopes hijos que no
se deberá tomar ninguna acción con respecto a este evento. defaultPrevented: (boolean)
Esta propiedad es cambiada a true por la función preventDefault.
Los eventos en una aplicación nos brindan una gran funcionalidad en la aplicación ya
que nos permite tomar acciones específicas en los momentos necesarios mediante las
interacciones del usuario.
Capítulo 10: Recursos
En el Capítulo 5 tratamos el tema sobre las peticiones al servidor utilizando el servicio
$http. Aunque solo hicimos peticiones de tipo get el servicio puede realizar peticiones
a todos los métodos estándar de HTTP. En Este capítulo comenzaremos a utilizar un
servicio llamado ngResource que está basado completamente en el servicio $http pero
está más enfocado al trabajo con APIs RESTful. Aunque el servicio $http por si solo
puede hacer uso de un Api Rest por sí solo, tendríamos que escribir mucho código para
completar las tareas.
Obteniendo ngResource
El servicio ngResource no forma parte del núcleo de angular se debe incluir en la
aplicación después del framework. Para obtenerlo lo podremos descargar desde la página
oficial de angular, usando el CDN o instalándolo como dependencia de la aplicación con
bower. Veamos un ejemplo utilizando bower.
1
bower install angular-resource --save
Especificando –save añadiremos angular-resource como una dependencia en el archivo
bower.json de la aplicación. Después de haber obtenido el servicio lo incluimos en la
aplicación.
1
2
3
4
5
6
7
8
9
10
<body>
<div class="container">
<h1>Hello</h1>
</div>
<script
<script
<script
<script
</body>
src="lib/bootstrap/dist/js/bootstrap.min.js"></script>
src="lib/angular/angular.js"></script>
src="lib/angular-resource/angular-resource.js"></script>
src="js/app.js"></script>
Solo queda un paso para que podamos comenzar a utilizar este servicio, y es que necesitamos incluirlo como una de las dependencias del módulo que estamos desarrollando.
152
Capítulo 10: Recursos
1
153
angular.module('miApp', ['ngResource']);
De esta forma ya podremos comenzar a Hacer uso del servicio $resource. Este servicio
lo utilizaremos mayormente para crear nuestros propios servicios que se encarguen de
tratar con el servidor de una forma RESTful. A medida que vallamos viendo los ejemplos
describiré el servicio y las facilidades que nos brinda con respecto a $http.
Primera petición al servidor REST
En el siguiente ejemplo hare una petición a un recurso REST del servidor donde obtendré
una lista de mensajes y la mostraré mediante la directiva ng-repeat al usuario. Lo
primero que necesitamos es un factory que devuelva el servicio REST para comenzar
a ejecutar las peticiones.
1
2
3
4
var app = angular.module('miApp', ['ngResource'])
.factory('Mensajes', ['$resource', function ($resource) {
return $resource('/api/mensajes/:id');
}]);
El servicio $resource recibe varios parámetros, pero por ahora solo utilizaremos el
primer parámetro que será una cadena de texto con la url a la que haremos la petición.
Como has podido observar en la url de la petición tenemos un parámetro :id, si leíste el
Capítulo 8 te parecerá familiar ya que es de la misma forma que se pasan los parámetros
en las rutas. Internamente el servicio $resource hará uso de ese parámetro para hacer
peticiones a recursos individuales mediante el id.
Ahora creare un controlador para hacer uso del servicio Mensajes que acabo de crear. En
el controlador ejecutaremos la petición y en caso de ser satisfactoria haremos disponibles
los mensajes en la vista asignándolos al $scope.mensajes. En caso de que la respuesta sea
un error imprimiremos en la consola el código del error y el mensaje proporcionado por
el servidor.
1
2
3
4
5
6
7
8
app.controller('MensajesCtrl',['$scope', 'Mensajes',
function ($scope, Mensajes) {
Mensajes.query(function (datos) {
$scope.mensajes = datos;
}, function (err) {
console.error('Error ' + err.status + ': ' + err.data.mensaje);
});
}]);
Capítulo 10: Recursos
154
El servicio devuelto por $resource nos brinda cinco métodos para interactuar con
recursos en el servidor que facilita mucho las tareas con respecto a hacer peticiones
con el servicio $http. Como has podido observar se ha utilizado el método query el
cual explicare al detalle más adelante. Por ahora solo mencionar que lo utilizamos para
hacer una petición de tipo get al servidor donde obtendremos un arreglo de recursos en
forma de colección. Esta petición espera una respuesta de tipo json. Ahora que ya se han
asignado los datos del servidor al $scope podremos mostrarlo en la vista con ng-repeat.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="container" ng-controller="MensajesCtrl">
<div class="row">
<div class="col-md-12">
<h2>Mensajes</h2>
<table class="table table-hover">
<thead>
<tr>
<th>Usuario</th>
<th>Mensaje</th>
</tr>
</thead>
<tbody >
<tr ng-repeat="mensaje in mensajes">
<td>{{mensaje.usuario}}</td>
<td>{{mensaje.mensaje}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
En la vista mostraremos cada mensaje en una fila de la tabla mediante ng-repeat, El
resultado podemos observarlo en la imagen a continuación.
155
Capítulo 10: Recursos
Lista de los mensajes usando Bootstrap3
Parámetros del servicio $resource
En los ejemplos anteriores he creado un servicio mediante $resource utilizando un solo
parámetro. Este primer parámetro es la ruta a la cual se realizarán las peticiones. Como se
comentó anteriormente en esta se pueden especificar parámetros utilizando la notación
de rutas de angular, :nombre donde nombre será el nombre del parámetro por el que
posteriormente se le hará referencia.
El Segundo parámetro es opcional, un objeto de configuración con valores por defecto
de la configuración de la ruta. Estos pueden ser remplazados por los métodos del servicio
cuando son ejecutados. Cada uno de las llaves: valor del objeto de configuración serán
remplazados en los parámetros de la ruta. En caso que el objeto posea más parámetros
que la ruta los restantes serán añadidos como parte de la cadena query de la url. En caso
de que la url sea ‘/categoria/:slug’ y el objeto de parámetros sea {slug:’internet’, pagina:2}
el resultado de la url sería el siguiente /categoria/internet?pagina=2.
Otra manera de especificar los parámetros de la ruta es utilizando un @ como prefijo del
valor. De esta forma el valor será extraído del cuerpo de la petición cuando es llamado.
Por ejemplo, si la ruta es ‘/usuario/:id’ el objeto de configuración es {id: ‘@uid’} el id será
extraído del cuerpo de la petición data.uid.
El tercer parámetro que recibe el servicio $resource es opcional y es un objeto con
la declaración de métodos personalizadas para extender las acciones por defecto del
servicio. Este objeto se definirá de la forma {nombreMetodo:{objetoConfiguracion}}
Capítulo 10: Recursos
156
donde cada key será el nombre de cada método que añadiremos al servicio y el valor un
objeto de configuración del método. El objeto de configuración de cada método puede
tener varios elementos que describiré a continuación.
• action: Cadena de texto con el nombre del nuevo método.
• method: Cadena de texto con el tipo de petición que deberá hacer este método.
(GET, POST, PUT, DELETE, JSONP, etc.)
• params: Objeto de parámetros para ser remplazados en la url, como el segundo
parámetro que recibe el servicio $resource.
• url: Cadena de texto especificando una url a la que hacer la petición, si es especificada se utilizará esta y no la especificada en el primer parámetro de $resource.
• isArray: Boolean, de ser verdadero se esperará una respuesta de tipo arreglo de
json, de lo contrario se esperará un objeto json. Es utilizado cuando se espera una
lista de objetos.
• transformRequest: Función o arreglo de funciones que devolverán la respuesta
transformada. Esta función recibe como parámetros el cuerpo de la petición y los
headers. Por lo general es utilizado para serializar el cuerpo de la petición.
• transformResponse: Función o arreglo de funciones como transformRequest
pero para transformar la respuesta. Usualmente utilizado para de serializar el
cuerpo de la respuesta.
• cache: Boolean o instancia de $cacheFactory, si es verdadero se utilizará el comportamiento del servicio $http para hacer cache cuando la petición es de tipo GET.
Si se utiliza una instancia de $cacheFactory será utilizada para hacer la cache. En
caso que sea falso no se hará cache de la petición.
• timeout: Número u objeto de tipo $promise. Si se utiliza un número será la
cantidad de milisegundos, si se utiliza una promesa esta deberá abortar la petición
cuando esta sea resuelta.
• withCredentials: Boolean que definirá si se utiliza withCredentials en el objeto
XHR.
• responseType: Cadena de texto que se utiliza para definir el XMLHttpRequestResponseType en la petición con los diferentes tipos de respuesta que esperaremos.
• interceptor: Objeto como los interceptores del servicio $http detallado en el
Capitulo 5.
Como has podido observar el servicio $resource brinda una gran flexibilidad y extensibilidad para interactuar con los recursos del servidor. Ahora vamos a ver un ejemplo de
cómo quedaría un objeto configurado.
Capítulo 10: Recursos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
157
.factory('Mensajes', ['$resource', function ($resource) {
return $resource('/api/mensajes/:id', {id: '@mid'}, {
actualizar: {
method: 'PUT',
isArray: false,
transformRequest: function (datos, headerFn) {
return JSON.stringify(datos);
},
transformResponse: function (datos, headerFn) {
return JSON.parse(datos);
},
cache: false,
timeout: 2000,
withCredentials: true,
responseType: 'json'
}
})
}])
El objeto de respuesta
El factory del ejemplo anterior devuelve una clase de tipo resource con varios métodos
que permiten interactuar con el servidor de una manera muy sencilla. Los métodos que
nos brinda esta clase son los siguientes, get, save, query, remove y delete. De estos cinco
parámetros dos son de tipo GET, los demás de tipo POST y DELETE.
Métodos de tipo GET
Los dos métodos GET son get() y query(), estos esperan tres parámetros.
1. El primer parámetro es un objeto con los parámetros que se enviarán en la petición,
estos pueden ser parámetros de la url o parámetros de query que serán codificados
en la url.
2. El segundo parámetro es una función que será llamada cuando la respuesta sea
satisfactoria. Recuerden que se considera respuestas satisfactorias las que su código
de respuesta este entre los valores de 200 y 299.
3. El tercer parámetro es otra función, la cual será llamada en caso de que la respuesta
no sea satisfactoria.
La función que se ejecuta cuando la respuesta es satisfactoria recibe dos parámetros,
el primero son los datos que solicitamos al servidor, y el segundo es una función para
Capítulo 10: Recursos
158
obtener los headers. La función para cuando no se obtiene una respuesta satisfactoria
recibe un único parámetro que es un objeto de error. Veamos un ejemplo usando el
método get().
1
2
3
4
5
6
7
8
app.controller('MensajesCtrl',['$scope', 'Mensajes',
function ($scope, Mensajes) {
$scope.seleccion = function (mid) {
Mensajes.get({id: mid}, function (data, headersFn) {
$scope.seleccionado = data;
})
}
}]);
Con este método que hemos añadido al $scope podemos llamarlo en la vista pasándole
el id del mensaje que queremos obtener y mostrarlo de forma independiente en otra
sección.
1
2
3
4
5
6
7
8
9
10
11
12
13
<tbody >
<tr ng-repeat="mensaje in mensajes" ng-click="seleccion(mensaje.mid)">
<td>{{mensaje.usuario}}</td><td>{{mensaje.mensaje}}</td>
</tr>
</tbody>
//... fin de la tabla
<div class="row" ng-show="seleccionado">
<div class="col-md-12">
<h2>Seleccionado:</h2>
<p>Usuario: {{seleccionado.usuario}}</p>
<p>Mensaje: {{seleccionado.mensaje}}</p>
</div>
</div>
El método get() espera como respuesta un objeto json. En cambio, si necesitamos obtener
una lista de objeto deberemos usar el método query que espera como respuesta un
arreglo de objetos. Este comportamiento esta pre definido en angular utilizando la
propiedad isArray con valor verdadero en la configuración del método.
Otros métodos
Los tres restantes métodos que proporciona el servicio $resource son save(), remove()
y delete(). Estos esperan cuatro parámetros.
Capítulo 10: Recursos
159
1. El primer parámetro es un objeto con los parámetros que se enviarán en la petición,
estos pueden ser parámetros de la url o parámetros de query que serán codificados
en la url.
2. El segundo parámetro es un objeto que será enviado como cuerpo de la petición.
3. El tercer parámetro es una función que será llamada cuando la respuesta sea
satisfactoria. Recuerden que se considera respuestas satisfactorias las que su código
de respuesta este entre los valores de 200 y 299.
4. El cuarto parámetro es otra función, la cual será llamada en caso de que la respuesta
no sea satisfactoria.
El método save() es una petición de tipo POST. Es utilizado para crear nuevos recursos
en el servidor y utiliza el segundo parámetro como cuerpo de la petición. Veamos un
ejemplo continuando con el servicio de Mensajes anterior. Creare un formulario en la
vista para nuevos mensajes, un nuevo método en el $scope para hacer una petición POST
con el método save(). Si obtenemos una respuesta satisfactoria añadiremos la respuesta
al arreglo de mensajes $scope.mensajes y limpiaremos el formulario.
1
2
3
4
5
6
$scope.nuevo = function () {
Mensajes.save({}, $scope.msg, function (res) {
$scope.mensajes.push(res);
$scope.msg = {};
});
}
Como primer parámetro enviaremos un objeto vacío ya que no necesitamos enviar
ningún parámetro en la url para realizar la petición. También podríamos obviar el primer
parámetro y funcionaría de igual manera. Como segundo parámetro el cuerpo de la
petición que será el formulario con los datos. El tercer parámetro es la función que se
ejecutará si la respuesta es satisfactoria. De ser así agregaremos la respuesta como un
nuevo mensaje en la lista de mensajes y limpiaremos el formulario dejándolo en un objeto
vacío.
1
2
3
4
5
6
7
8
9
<div class="col-md-4">
<h2>Nuevo mensaje</h2>
<form class="form-horizontal" name="nuevoMensaje"
role="form" ng-submit="nuevo()">
<div class="form-group">
<input type="text" ng-model="msg.usuario"
class="form-control" placeholder="Usuario">
</div>
<div class="form-group">
Capítulo 10: Recursos
10
11
12
13
14
15
16
17
160
<textarea ng-model="msg.mensaje" class="form-control"
rows="3" placeholder="Mensaje"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Enviar</button>
</div>
</form>
</div>
Con lo anterior será suficiente para crear nuevos recursos en el servidor haciendo
peticiones POST con el servicio Mensajes mediante el método save()
Nos quedan dos métodos por detallar, delete() y remove(). Estos dos hacen la misma
función y es enviar una petición al servidor de tipo DELETE. La única diferencia entre
ellos es que en el lenguaje JavaScript la palabra delete es una palabra reservada y en
Internet Explorer puede ocasionar problemas de incompatibilidad. Para ver un ejemplo
del uso de remove() vamos a agregar un enlace a cada mensaje para hacer una petición y
eliminar el mensaje. Si la respuesta es satisfactoria utilizamos el índice del mensaje para
eliminarlo del arreglo y no tener que volver a pedir los mensajes al servidor.
1
2
3
4
5
$scope.eliminar = function (index) {
Mensajes.remove({id: $scope.mensajes[index].mid}, function () {
$scope.mensajes.splice(index,1);
});
}
De esta forma hacemos la petición de tipo DELETE en la cual especificamos en el primer
parámetro un objeto de configuración especificando el id del mensaje que queremos
eliminar.
1
2
3
4
5
6
7
8
9
10
<tbody>
<tr ng-repeat="mensaje in mensajes">
<td>{{mensaje.usuario}}</td><td>{{mensaje.mensaje}}</td>
<td>
<a ng-click="eliminar($index)">
<i class="glyphicon glyphicon-minus"></i>
</a>
</td>
</tr>
</tbody>
En la vista ejecutamos el método eliminar() pasando como parámetro el $index proporcionado por la directiva ng-repeat.
Capítulo 10: Recursos
161
Ahora solo nos queda poder editar mensajes para cumplir con las funcionalidades básicas
de un CRUD (Create-Retrieve-Update-Delete)(Crear-Obtener-Actualizar-Eliminar)
Creando el método update
Como estamos tratando con APIs RESTful necesitamos hacer peticiones de tipo PUT
para poder actualizar un elemento ya que las peticiones de tipo POST se utilizan para
crear nuevos recursos. El servidor estará esperando una petición con método PUT para
realizar la actualización de un elemento. Para lograrlo necesitaremos crear una nueva
acción en la declaración del servicio.
1
2
3
4
5
6
.factory('Mensajes', ['$resource', function ($resource) {
return $resource('/api/mensajes/:id',
{id: '@mid'},
{update: { method: 'PUT'}}
);
}])
Ahora disponemos del método update que realizará peticiones PUT. Actualizaremos el
método $scope.seleccion para obtener una referencia directa desde la lista de mensajes y
poder editarlo en un formulario. Ya que el mensaje está disponible en la lista evitaremos
hacer una petición innecesaria al servidor.
1
2
3
4
$scope.seleccion = function (index) {
$scope.actualizando = true;
$scope.act = $scope.mensajes[index];
}
He creado una nueva variable $scope.actualizando que definirá si se muestra o no el
formulario de actualización, y otra variable $scope.act que contendrá el mensaje que
queremos actualizar. Este método lo ejecutamos en la vista con la directiva ng-click
pasándole como parámetro el $index del ng-repeat.
1
2
3
4
<td>{{mensaje.usuario}}</td>
<td style="cursor: pointer" ng-click="seleccion($index)">
{{mensaje.mensaje}}
</td>
De esta forma ya tenemos disponible el mensaje listo para editar, creare un formulario
que use como modelo el objeto $scope.act de la selección.
Capítulo 10: Recursos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
162
<h2>Actualizar mensaje</h2>
<form class="form-horizontal" role="form" ng-submit="actualizar()">
<div class="form-group">
<input type="text" ng-model="act.usuario"
class="form-control" placeholder="Usuario">
</div>
<div class="form-group">
<textarea class="form-control" ng-model="act.mensaje"
rows="5" placeholder="Mensaje"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Actualizar</button>
</div>
</form>
Definimos la directiva ng-submit del formulario a un método actualizar() que crearé a
continuación.
1
2
3
4
5
$scope.actualizar = function () {
Mensajes.update($scope.act, function (res) {
$scope.actualizando = false;
})
}
Al hacer submit en el formulario ejecutará el método actualizar y mediante el servicio
Mensajes se ejecutará el método update() que creamos anteriormente en la configuración del servicio para poder hacer peticiones PUT. Si obtenemos una respuesta
satisfactoria ocultamos el formulario de actualización estableciendo un valor falso en
$scope.actualizando. Como detalle podemos agregar que cuando se ejecute el método
eliminar y el formulario de actualizar este visible editando el mensaje que se quiere
eliminar, desaparezca el formulario ya que se eliminará el mensaje y aun estaría en el
formulario para editarlo.
1
2
3
4
5
6
7
$scope.eliminar = function (index) {
if ( $scope.actualizando && $scope.mensajes[index].mid == $scope.act.mid )
$scope.actualizando = false;
Mensajes.remove({id: $scope.mensajes[index].mid}, function () {
$scope.mensajes.splice(index,1);
});
}
Capítulo 10: Recursos
163
Con los ejemplos anteriores completamos una pequeña aplicación capaz de realizar las
tareas básicas que son Leer, crear, actualizar y eliminar datos en un servidor remoto.
Como te habrás percatado, todas las operaciones anteriores se logran sin hacer recargas
de la página. Todas las acciones del servicio ngResource al ser basadas en $http son
completamente mediante AJAX. Aunque la aplicación es funcional y cumple el objetivo
para lo que fue creada se pueden hacer algunas optimizaciones. Hasta el momento
hemos utilizado los métodos que nos brinda los servicios creados con $resource. Ahora
mejoraremos la aplicación utilizando los métodos que poseen las instancias del servicio.
Instancia de un recurso
Cuando ejecutamos una petición con un servicio de $resource, si la petición es satisfactoria este nos devuelve un objeto o una colección dependiendo de la petición. Cada objeto
devuelto es una instancia de la clase resource por lo que tendremos acceso a los métodos
$save, $remove y $delete así como los demás que hayamos creado en la definición del
recurso.
Para optimizar la aplicación lo primero que podemos hacer es utilizar la instancia de la
clase resource que tenemos de cada uno de los mensajes, pare realizar las operaciones.
Comenzaremos por el método eliminar, dejando de utilizar el servicio Mensajes y
utilizando la instancia ejecutando el método $remove de la misma.
1
2
3
4
5
6
7
$scope.eliminar = function (index) {
if ( $scope.actualizando && $scope.mensajes[index].mid == $scope.act.mid )
$scope.actualizando = false;
$scope.mensajes[index].$remove(function () {
$scope.mensajes.splice(index,1);
})
}
A continuación dejaremos de utilizar el método $scope.actualizar ya que podemos
ejecutar directamente update dentro del objeto. Cuando ejecutamos las acciones como
métodos de la instancia de resource tendremos que utilizar un el símbolo $ como
prefijo. Así que en la vista en la directiva ng-submit del formulario de actualización lo
cambiamos por act.$update()
1
2
<form class="form-horizontal" name="nuevoMensaje"
role="form" ng-submit="act.$update()">
De esta forma la aplicación está terminada. En resumen, la vista de quedaría como la
imagen que se muestra a continuación.
164
Capítulo 10: Recursos
Aplicación de mensajes terminada
Ahora que ya dominamos el uso del servicio $resource es importante mencionar un último concepto. Las peticiones que se hacen a través del servicio son totalmente Asíncronas,
lo que quiere decir que en el momento en que se ejecuta la acción el servicio devolverá
una referencia vacía al recurso. Si tratamos de ejecutar acciones inmediatamente que se
ejecuta una petición los resultados no serán los esperados. Cuando el servicio obtiene
los datos desde el servidor Angular rellenara la respuesta automáticamente. Teniendo
en cuenta esto, en la aplicación que estábamos desarrollando anteriormente podríamos
obtener los mensajes de la siguiente forma.
1
$scope.mensajes = Mensajes.query();
Ejecutándolo de esta forma no tendríamos problemas porque por el momento los
mensajes solo se muestran en la vista y Angular refrescará la vista cuando la respuesta esté
lista. Pero si tratamos de acceder al primer mensaje inmediatamente después de hacer la
petición, obtendríamos un error.
1
2
$scope.mensajes = Mensajes.query();
console.log($scope.mensajes[0].mensaje);
Este código detendrá la carga de la aplicación con un TypeError: Cannot read property
‘mensaje’ of undefined ya que en ese momento en que estamos accediendo a la propiedad
mensaje aún no tenemos la respuesta lista.
Capítulo 10: Recursos
165
Aún nos quedan dos propiedades más que tratar con respecto a la instancia de un
$resource. La primera es la propiedad $resolved, está siempre tendrá el valor false
mientras se ejecuta la petición. Cuando la petición es resuelta esta toma el valor true.
Siempre que se resuelva la petición esta obtendrá valor verdadero, independientemente
de que la respuesta sea satisfactoria o no.
1
2
$scope.mensajes = Mensajes.query();
console.log($scope.mensajes.$resolved);
El código anterior imprimirá en la consola un valor falso ya que aún la petición se está
ejecutando en el momento que se ha consultado el valor de $resolved.
1
2
3
Mensajes.query(function (datos) {
console.log(datos.$resolved);
});
El ejemplo anterior imprimirá en la consola un valor verdadero ya que la función se
ejecuta cuando se obtiene una respuesta satisfactoria.
La última propiedad que queda por describir es $promise. Esta es la promesa que se
ha utilizado para crear el $resource. Si la petición que se ejecuta tiene una respuesta
satisfactoria la promesa es resuelta con la colección o la instancia del recurso. De no ser
satisfactoria, la promesa es resuelta con un objeto de respuesta HTTP sin la propiedad
resource. Volviendo a los ejemplos anteriores donde accedíamos al primer mensaje antes
de estar listo, veámoslo utilizando la promesa.
1
2
3
$scope.mensajes.$promise.then(function (data) {
console.log(data[0].mensaje);
});
Esta propiedad $promise es especialmente utilizada en la propiedad resolve del método
when() del servicio $resourceProvider cuando estamos definiendo las rutas.
Trailing Slash
Por defecto en el servicio $resource cuando se hace una petición a un servidor remoto,
angular elimina los slash al final de la dirección. Vamos a ver un ejemplo.
Capítulo 10: Recursos
1
2
3
4
5
6
7
8
9
166
angular.module('app', ['ngResource'])
.factory('res', function ($resource) {
return $resource('/resource/:id/');
})
.controller('AppCtrl', function ($scope, res) {
res.get({ id: 3 }, function (data) {
console.log('done');
})
});
En el ejemplo anterior se ha definido un nuevo factory con un resource apuntando a
la dirección ‘/resource/:id/’ incluyendo el slash del final. Después en el controlador
hacemos una petición al recurso con una id con valor 3. Si vemos este ejemplo en el
navegador, abrimos la consola del mismo y podemos observar en el error 404 que la
petición se ha hecho a la dirección ‘/resource/3’ sin el slash del final.
En la mayoría de los casos este comportamiento no nos será un problema, pero hay
ocasiones que los servicios RESTFul que estamos consumiendo, requieren que se haga
la petición especificando este slash del final de la dirección.
A partir de la versión 1.3 de Angular tendremos la posibilidad de configurar este
comportamiento mediante el provider del servicio resource en el bloque de configuración
de la aplicación. Para ello debemos especificar la propiedad stripTrailingSlashes del
objeto default a un valor falso.
1
2
3
4
angular.module('app', ['ngResource'])
.config(function ($resourceProvider) {
$resourceProvider.defaults.stripTrailingSlashes = false;
});
Ahora cuando hagamos alguna petición con el servicio $resource, este incluirá el slash del
final de la dirección para que no existan problemas con los servidores que lo requieren.
Como has podido observar es muy fácil de utilizar el servicio ngResource de Angular y
nos brinda una forma sencilla, pero a la vez muy potente para realizar aplicaciones que
dependan de un backend remoto.
Capítulo 11: Formularios y Validación
Con la llegada de HTML5 los formularios en la web se vieron mejorados grandemente con respecto al estándar anterior. En la actualidad los formularios son una
parte imprescindible de cualquier aplicación y la vía principal por la que el usuario
intercambia información con el servidor. Dada la necesidad de la veracidad de los datos
intercambiados con el servidor, la validación es la parte más importante si queremos
obtener una información correcta y útil. Entre otras ventajas, una correcta validación
de los datos antes de ser procesados ayuda al usuario a rectificar la información y evita
hacer peticiones innecesarias al servidor.
AngularJS nos provee de una infraestructura completa para la validación de formularios.
En este caso los enriquece permitiéndole declarar estado valido, invalido o modificado
para cada elemento. De esta forma podremos comprobar si los datos que ha introducido
el usuario son válidos antes de procesar la información o enviarlos al servidor. Él
framework se basa en las reglas de validación de los elementos HTML5, así como
directivas para validaciones que no existen aún en el estándar HTML, pero son necesarias
en mucho de los casos que trabajamos con formularios.
Además de la validación HTML5 AngularJS nos brinda directivas para validar elementos
del formulario incluso sin el uso de código extra. A lo largo de este capituló detallaré el
uso de la validación con las reglas de HTML5 así como las directivas proporcionadas
por AngularJS y también la creación validación personalizada. Para comenzar veamos
algunas de las reglas de validación y su utilización.
Reglas de Validación
El elemento HTML <input> es el que recibe las reglas de validación en dos formas.
Primero con la propiedad type donde especificando un tipo de elemento como email,
number o url AngularJS validará que sean correctos por su definición. La segunda forma
de validación es mediante propiedades como es required que hace que el elemento tenga
al menos un valor. Esta última es una de las reglas más utilizadas y podemos utilizarla
de dos formas. Una de ellas es especificándolo como el estándar de HTML5 como en el
ejemplo siguiente.
167
Capítulo 11: Formularios y Validación
1
2
3
4
168
...
<input type="text" name="nombre" required>
<input type="password" name="password" ng-required="true">
...
La otra vía para su uso es mediante la directiva ng-required la cual toma un valor
verdadero. En cualquiera de los dos casos AngularJS validará que el elemento HTML
tenga algún contenido como valor antes de ser procesado.
Además de las reglas básicas de HTML5 angular provee otras tres directivas de validación
aplicable a la mayoría de los elementos. Estas reglas son las siguientes.
Valor mínimo
Para validar un elemento <input> y hacer que posea un valor mínimo de caracteres
utilizaremos la directiva ng-minlength que recibe como valor un número.
1
2
3
..
<input type="text" name="nombre" ng-minlength=3>
..
Valor máximo
Al contrario de la directiva anterior la directiva ng-maxlength es definida para validar
un máximo de caracteres.
1
2
3
...
<input type="text" name="nombre" ng-maxlength=15>
...
Expresión regular
La directiva ng-pattern asegura que el valor cumpla con una expresión regular de
JavaScript para que este sea válido.
1
2
3
...
<input type="text" name="nombre" ng-pattern="/a-zA-Z/">
...
Creando una regla de validación
Además de estas reglas de validación AngularJS nos permite crear nuestras propias reglas
para alguna especificidad de nuestra aplicación. A continuación, crearemos una regla
para validar que el email introducido por el usuario es único en un arreglo de correos.
Capítulo 11: Formularios y Validación
169
Archivo: index.html
1
2
3
4
5
6
...
<body ng-controller="mainController">
<h1>Formularios y Validación</h1>
Nombre <input type="email" unique="isUnique" ng-model='email'><br>
</body>
...
En esta vista estamos definiendo un elemento <input> de tipo email lo cual tiene su
propia validación. Además, estamos haciendo uso de la directiva unique que crearemos
a continuación y le estamos pasando un valor isUnique que será un método del $scope
que definiremos en el controlador, este comprobará si el email es único o ya está en el
arreglo de email. También estamos utilizando la directiva ng-model para poder utilizarlo
en la directiva.
Archivo: app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
app.directive('unique', [
function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl){
var original;
ctrl.$formatters.unshift( function(modelValue) {
original = modelValue;
return modelValue;
});
ctrl.$parsers.push(function(val){
if (val && val !== original) {
ctrl.$setValidity( 'unique' , scope[attrs.unique](val));
}
return val;
})
}
};
}
]);
...
En la directiva unique hacemos que la directiva ng-model sea requerida para poder
acceder al controlador de ese modelo. En la función de la directiva primero obtenemos el
Capítulo 11: Formularios y Validación
170
valor del modelo y lo guardamos en la variable original luego añadimos una función en
el arreglo $parsers que será la encargada de definir si el modelo es válido o no mediante
el método $setValidity del controlador del modelo. Esta función recibe dos parámetros
el primero es la llave por la que llamaremos a esta regla de validación a la hora de ver los
errores y el segundo es el valor verdadero o falso de la regla, en este caso ejecutamos la
función del controlador con el valor como parámetro para analizar si es único o no. Esta
función es definida en el controlador.
Archivo: app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
app.controller('mainController', ['$scope', function ($scope) {
var mailList = [
'[email protected]',
'[email protected]',
'[email protected]'
];
$scope.isUnique = function(val){
var res;
for (var i = 0; i < mailList.length; i++) {
if (mailList[i] == val) {
res = false;
break;
} else res = true;
}
return res;
};
}]);
...
En el controlador tenemos definido un arreglo mailList con las direcciones de correo
que no deben ser utilizadas por el usuario. Exponemos al $scope el método isUnique
que es el que se ejecutará para comprobar si el email introducido por el usuario es único.
Este método recibe como parámetro el valor del elemento <input> proporcionado por la
directiva, itera sobre el arreglo de correos y devuelve un valor verdadero o falso si existe
o no el correo.
Mejoras creando reglas de validación
A partir de la versión 1.3 de Angular, se definió una nueva propiedad llamada $validators
para crear reglas de validación personalizadas. Anteriormente necesitábamos hacer uso
Capítulo 11: Formularios y Validación
171
de las propiedades $parsers y $formatters para crear una regla. La nueva vía para crear
reglas de validación hace que el proceso sea mucho más sencillo y fácil de implementar.
A continuación, vamos a crear un ejemplo donde tendremos dos campos de fecha para
un evento, uno fecha de inicio y otra fecha fin. Necesitaremos una directiva para validar
que la fecha de fin no sea posterior a la fecha de inicio del evento. Además, pondremos
un mensaje de validación para cuando falle la validación.
1
2
3
4
5
6
7
8
9
10
11
12
13
...
<form name="formulario">
Inicio:
<input type="date" ng-model="inicio" name="inicio"><br><br>
Fin:
<input type="date" ng-model="fin" name="fin" validador-rango="inicio">
<span ng-if="formulario.fin.$dirty" ng-messages="formulario.fin.$error">
<span ng-message="fueraRango">
La fecha de fin de evento no puede ser anterior a la de inicio
</span>
</span>
</form>
...
Para continuar debemos definir en el controlador los valores por defecto de los elementos del formulario y pasamos a crear la directiva. Recuerden al nombrar la directiva, debe
tener un nombre en notación de camello (Camel Case) para poder utilizarla en la vista
separado por guiones (Snake Case). Restringimos la directiva para que solo pueda ser
utilizada como atributo de un elemento. Requerimos el modelo para crear la regla de
validación. Definimos el scope con la propiedad fechaInicio igual a el valor de la directiva
y pasamos a crear la función link.
1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('app',['ngMessages'])
.controller('ctrl', ['$scope', function($scope){
$scope.inicio = new Date();
$scope.fin = new Date();
}] )
.directive('validadorRango', function(){
return {
restrict: 'A',
require: 'ngModel',
scope: { fechaInicio: '=validadorRango'},
link: function(scope, element, attrs, ngModel) {
ngModel.$validators.fueraRango = function(val){
return Date.parse(val) >= Date.parse(scope.fechaInicio);
Capítulo 11: Formularios y Validación
14
15
16
17
18
19
20
172
};
scope.$watch('fechaInicio', function(){
ngModel.$validate();
});
}
}
});
En la función link de la directiva inyectamos el modelo para poder acceder a la nueva
propiedad de los validadores. En esta definimos un nuevo validador con el nombre
fueraRango, este es el nombre que será utilizado en el objeto $error para saber si la
validación ha sido correcta o no. Esta función acepta el primer parámetro que es el valor
introducido en el elemento. Para comprobar si la fecha es posterior o no simplemente
utilizamos el método parse del objeto Date de Javascript y lo comprobamos con el de la
fecha de inicio. Este valor lo devolvemos ya que será un verdadero o false dependiendo
de los valores de los elementos. De esta forma ya está listo el validador para las fechas.
Como puede pasar que en vez de cambiar la fecha de fin puede que se cambie la fecha
de inicio, es necesario observar los cambios en el elemento de la fecha de inicio y en
cada cambio volver a validar la fecha de fin. Para esto implementamos un $watch en el
elemento fechaInicio del scope y volvemos a validar el modelo en caso de cambios.
Como habrás podido notar utilizando la nueva propiedad $validators del modelo para
definir nuevas reglas de validación, hace que el proceso sea mucho más simple y fácil de
implementar.
Ejecutando validación asíncrona
En versiones de Angular anteriores a 1.3 realizar la validación de un elemento mediante
un servidor remoto era un poco trabajoso. En esta nueva versión se ha definido una
nueva propiedad para los validadores asíncronos. La propiedad $asyncValidators es un
arreglo de validadores que se ejecutaran de manera asíncrona y siempre después de haber
ejecutado los validadores síncronos.
Ahora para detallar bien el proceso vamos a crear una directiva para evitar que un
usuario se registre dos veces en la aplicación con la misma dirección de correo. Para
ello necesitaremos un servidor que permita comprobar si la dirección de correo existe
y devolvernos una respuesta para la validación. La directiva que crearemos será la
encargada de hacer la petición y validar dependiendo de la respuesta del servidor. Para
esta directiva necesitamos el servicio de promesas $q y el servicio para comunicarnos
con el servidor $http. Es importante mencionar que todos los validadores registrados
como asíncronos deben devolver una promesa. Si la promesa es rechazada el validador
fallará y si se resuelve la validación será verdadera.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
7
8
9
10
11
173
...
<form name="formulario">
Correo:
<input type="email" ng-model="email" name="email" validar-email-duplicado>
<span ng-if="formulario.email.$dirty" ng-messages="formulario.email.$error">
<span ng-message="emailDuplicado">
Esta direccion de correo está siendo utilizada por otro usuario
</span>
</span>
</form>
...
En el formulario anterior se ha definido un elemento de tipo email y este utiliza la
directiva validar-email-duplicado. Además, se ha incluido un mensaje de error con el
modulo ngMessages para el error emailDuplicado. Ahora vamos a crear la directiva que
valide el email de forma asíncrona con el servidor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.directive('validarEmailDuplicado', [ "$q", "$http",
function($q, $http){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$asyncValidators.emailDuplicado = function (val) {
var def = $q.defer();
$http.get('/emails', {params:{email: val}}).then(function(res){
def.reject("Existe ese email en la base de datos.");
}).catch(function(err){
def.resolve();
});
return def.promise;
}
}
}
}]);
Primero que todo necesitamos inyectar los servicios $q y $http para crear el funcionamiento. Restringimos la directiva a que solo pueda ser utilizada como atributo e
inyectamos el modelo para acceder al objeto ** $asyncValidators*. En la función link
de la directiva utilizamos el modelo para crear el nuevo validador *emailDuplicado. Primero
creamos un objeto defer y pasamos a hacer la petición al servidor enviando por método
GET la dirección email que ha introducido el usuario. Si esta petición devuelve una
Capítulo 11: Formularios y Validación
174
respuesta satisfactoria el email ya existe en el servidor, por lo que tendremos que rechazar
la promesa para que falle el validador. Si la respuesta es un error 404 resolveremos la
promesa. Para finalizar devolvemos la promesa.
Las formas de comprobar las respuestas para rechazar o resolver la promesa pueden
variar dependiendo del servidor, pero en esencia el comportamiento de la directiva está
claro. Debes considerar el uso de la directiva ng-model-options para actualizar el modelo
solo cuando el usuario deja el campo. De lo contrario en cada vez que se cambie el valor
se hará una comprobación, lo que quiere decir que se realizaran muchas peticiones al
servidor que son innecesarias.
El formulario
Hasta el momento hemos aprendido a validar los elementos <input> pero aún no hemos
hablado del formulario. En el momento en que declaramos un formulario, AngularJS
enriquece este con varios estados que podremos utilizar para dar información al usuario
de la veracidad de sus datos. Para poder referirnos al formulario este debe tener definida
la propiedad name la cual será automáticamente definida en el $scope para referirnos a
este.
Estados del formulario
AngularJS definirá automáticamente cuatro estados en el formulario dependiendo de los
datos introducidos por el usuario. Los estados son los siguientes.
• $valid: Devolverá verdadero o falso dependiendo de si el contenido del formulario
es válido. Cada uno de sus elementos <input> debe ser válido.
• $invalid: Devolverá verdadero o falso dependiendo de si el contenido del formulario es erróneo. Si alguno de los elementos <input> es erróneo el resultado de este
será falso.
• $dirty: Devolverá verdadero si el usuario ha interactuado con el formulario introduciendo algún dato o modificando alguno de los que están de lo contrario tendrá
valor falso.
• $pristine: Devolverá verdadero si el usuario no ha interactuado con el formulario
aún. Desde que el usuario introduzca algún dato devolverá falso.
• $submitted: En la versión 1.3 de Angular se añadió un nuevo estado al formulario.
Este estado nos devolverá verdadero o falso dependiendo si el formulario ha sido
procesado o no.
Estos estados serán propiedades del formulario desde el momento en que lo definamos
con un nombre para referirnos a él. Veamos un ejemplo donde mostramos los estados.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
175
...
<form name="formulario">
Nombre:
<input type="text"
ng-minlength=3
ng-maxlength=10
ng-model="nombre"
name="nombre">
</form>
Válido: <strong>{{formulario.$valid}}</strong><br>
Errores: <strong>{{formulario.$invalid}}</strong><br>
Modificado: <strong>{{formulario.$dirty}}</strong><br>
No Modificado: <strong>{{formulario.$pristine}}</strong>
</div>
...
Estilos en el formulario
Mediante estos estados podremos comprobar si el formulario es válido o si ha sido
modificado. Ahora que tenemos control sobre estos estados podremos informar al
usuario en caso de que haya introducido datos incorrectos. AngularJS define en cada
elemento del formulario unas clases CSS que permiten que muestres al usuario algún
tipo de información respecto a los datos que está introduciendo. Un ejemplo clásico de
esto es cambiar el borde del <input> a rojo cuando el dato introducido es incorrecto o
usar un color verde cuando lo está.
Las clases que define Angular en cada elemento son las siguientes:
•
•
•
•
ng-valid: Cuando todas las reglas aplicadas al elemento son válidas.
ng-invalid: Cuando alguna de las reglas aplicadas al elemento es inválida.
ng-pristine: Cuando el elemento no ha sido modificado.
ng-dirty: Cuando el elemento ha sido modificado de alguna forma.
A continuación, vamos a ver un ejemplo del uso de estas clases para mostrar al usuario
si sus datos son válidos.
Capítulo 11: Formularios y Validación
176
Archivo: index.html
1
2
3
4
5
6
7
8
9
10
11
...
<form name="formulario">
Nombre:
<input type="text"
ng-minlength=3
ng-maxlength=10
ng-model="nombre"
name="nombre">
Email: <input type="email" ng-model="email" name="email">
</form>
...
En el formulario anterior definimos dos elementos, uno para el nombre y otro para el
correo. El primero tiene dos tipos de validación donde especificamos que el mínimo de
caracteres es de tres y un máximo de 10. Para el segundo solo especificamos que es de
tipo email y el framework lo validará como una dirección de correo.
Haciendo uso de las reglas CSS que le añade AngularJS a cada elemento dependiendo de
su validación, podemos especificar en el archivo de estilos algunas reglas para mostrar al
usuario un feedback.
Archivo: app.css
1
2
3
4
5
6
7
8
9
10
11
...
input.ng-invalid.ng-dirty {
border: 2px solid #e51c23;
background-color: #ff5177;
}
input.ng-valid.ng-dirty {
border: 2px solid #259b24;
background-color: #5af158;
}
...
En el código anterior declaramos dos reglas CSS para los elementos <input> una para
los que tengan las clases ng-invalid donde le ponemos un borde y fondo rojo. Otra para
los que tienen la clase ng-valid con un borde y fondo verde. Ambas tienen que tener a su
vez la clase ng-dirty por qué no tendría sentido que mostráramos colores antes de haber
tocado los controles.
El ejemplo anterior nos devolverá algo como la siguiente imagen.
Capítulo 11: Formularios y Validación
177
Uso de clases CSS en la validación de formularios
En la imagen tenemos seis controles, tres para nombre y tres para correo. La primera fila
de nombre y correo han sido introducidos correctamente por lo que reciben un color
verde. La segunda fila es todo lo contrario, se han introducido datos erróneos ya que el
nombre debe tener al menos 3 caracteres y la dirección de correo es incorrecta. En la
tercera fila el nombre ha sido modificado, y aunque se elimine su contenido y quede en
blanco continuará con la clase ng-dirty por lo que toma el color verde al ser válido, no
toma color rojo ya que no es requerido, si hubiésemos puesto una regla de validación
required en ese elemento tomaría color rojo. El último elemento correo está en color
por defecto ya que no se ha tocado aun y tiene la clase ng-pristine.
Además de las cuatro clases antes mencionadas AngularJS también añade clases para cada
una de las validaciones. Un ejemplo de lo antes mencionado lo podemos ver en el segundo
campo nombre de la imagen. Este campo además de tener las clases que hemos discutido
antes también tiene una clase ng-invalid-minlength, a su vez en campo de correo de
su derecha tiene la clase ng-invalid-email. AngularJS declara clases para cada una de
las validaciones que hayamos especificado en cada elemento. Estas son declaradas con el
siguiente patrón: ng-invalid-regla o ng-valid-regla. Este comportamiento está para las
reglas que trae el framework como para las que creemos nosotros para la aplicación que
desarrollamos. Un ejemplo de esto es cuando creamos la regla unique anteriormente,
el elemento obtenía una clase ng-invalid-unique o ng-valid-unique dependiendo del
dato introducido.
Mostrando errores de validación
Aunque mostrar este tipo de aviso al usuario es un buen comienzo, no es lo suficiente
bueno. En elementos que tengamos varias reglas de validación mostraríamos solo color
para válido o inválido. Aun así, el usuario no puede saber qué es lo que está incorrecto
en el dato introducido. Para una correcta comunicación con el usuario debemos mostrar
un mensaje de error por cada una de las validaciones en cado de error.
Para lograr lo antes mencionado AngularJS define propiedades para cada uno de los
elementos del formulario en el siguiente formato.
1
formulario.input.propiedad
Capítulo 11: Formularios y Validación
178
El primer objeto es el nombre del formulario por el que se creó en el $scope recuerden
que a través de este objeto podemos tener acceso a las propiedades $dirty, $pristine,
$valid e $invalid. El segundo nivel es el nombre del elemento <input> al que queremos
acceder. El tercero es el nombre de la propiedad definida por AngularJS, por cada
elemento también tenemos acceso a las propiedades antes mencionadas del formulario.
En otras palabras, podemos comprobar si es válido o ha sido modificado un elemento
específico del formulario.
Ahora necesitamos mostrar mensajes de error para que el usuario conozca cual es el error
en la información introducida. Para lograrlo AngularJS define una propiedad $error en
cada elemento del formulario. Esta propiedad es un objeto que tendrá una propiedad
con el nombre de cada regla que se haya validado de forma incorrecta. Utilizando este
objeto podemos mostrar mensajes de validación individuales para cada elemento y para
cada regla. En el siguiente ejemplo veremos cómo mostrar errores dependiendo de cada
uno de los errores de validación. Para lograrlo solo haremos uso de HTML y CSS, para
obtener una vista más atractiva puedes utilizar el framework CSS: Twitter Bootstrap.
Para comenzar definimos el formulario, es importante asignarle un nombre ya que
mediante este nos referiremos a él para poder mostrar los errores de validación.
1
2
3
4
5
6
7
8
9
...
<form name="form" novalidate>
<input type="submit"
class="btn btn-info"
ng-disabled="form.$invalid">
Crear
</input>
</form>
...
En el botón hemos hecho uso de la directiva ng-disabled para deshabilitar el botón y que
el formulario no sea enviado hasta que sea completamente válido utilizando la propiedad
$invalid del formulario. A continuación, definiremos el <input> para introducir el
nombre y le asignaremos algunas reglas de validación. También mediante la directiva
ng-class aplicaremos los estilos CSS de Bootstrap para los elementos de formularios.
Cuando el valor es válido se le aplicará la clase has-success y has-error cuando tenga
errores.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
7
8
9
179
...
<div class="form-group" ng-class="{
'has-error': (form.nombre.$invalid && form.nombre.$dirty),
'has-success': form.nombre.$valid && form.nombre.$dirty}">
<label>Nombre</label>
<input type="text" class="form-control" name="nombre"
ng-minlength=3 ng-maxlength=15 required ng-model="nombre">
</div>
...
Al <input> nombre se le ha aplicado varias reglas de validación como son un mínimo
de tres caracteres y un máximo de quince. Además, se ha especificado que este es
requerido. Ahora especificaremos un mensaje de error para cada una de las validaciones
anteriores. Utilizaremos la directiva ng-show para mostrar cada error independiente si
está definido en la propiedad $error.
1
2
3
4
5
6
7
8
9
10
11
12
...
<p class="text-danger" ng-show="form.nombre.$error.minlength">
El campo <strong>Nombre</strong> debe tener al menos 3 caracteres
</p>
<p class="text-danger" ng-show="form.nombre.$error.maxlength">
El campo <strong>Nombre</strong> excede el máximo de 15 caracteres
</p>
<p class="text-danger"
ng-show="form.nombre.$error.required && form.nombre.$dirty">
El campo <strong>Nombre</strong> es requerido
</p>
...
Como pueden observar se ha utilizado cada una de las propiedades de $error para
mostrar los mensajes de cada una de las validaciones de forma independiente. Ahora
para campo de correo lo realizaremos de la misma forma.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
180
...
<div class="form-group"ng-class="{
'has-error': form.email.$invalid && form.email.$dirty,
'has-success': form.email.$valid && form.email.$dirty}">
<label>Correo</label>
<input type="email" class="form-control" name="email"
required ng-model="email">
</div>
<p class="text-danger" ng-show="form.email.$error.email">
La dirección de <strong>Correo</strong> es incorrecta
</p>
<p class="text-danger"
ng-show="form.email.$error.required && form.email.$dirty">
El campo <strong>Correo</strong> es requerido
</p>
...
Con el ejemplo anterior obtendremos un resultado como el de la siguiente imagen.
Debajo de cada uno de los <input> se muestra el error que definimos para cada una de
las reglas de validación.
Uso de la propiedad $error de cada elemento input
Estado de los elementos de formulario
Además de los estados que posee el formulario, los elementos del formulario poseen
dos estados añadidos en la versión 1.3 del framework. Estos estados son $touched y
$untouched, Es importante mencionar que, aunque su nombre tenga que ver con la
palabra touch esto no significa que sea para pantallas táctiles.
Estas dos nuevas propiedades tendrán un valor verdadero o falso los cuales serán
definidas en el momento que el usuario entre a un elemento y salga de él. Específicamente
Capítulo 11: Formularios y Validación
181
el elemento obtendrá el valor verdadero en la propiedad $touched en el momento en que
el elemento pierda el foco. A su vez la propiedad $untouched recibirá el valor falso.
Hasta el momento habías utilizado la propiedad $dirty para mostrar los errores de
validación. Pero para los elementos requeridos la propiedad $dirty no es la más ideal
ya que el usuario puede entrar y salir del elemento sin modificarlo y no se mostraría el
error de requerido. Si en ese caso utilizamos la propiedad $touched, aunque el usuario no
escriba nada en el elemento, al salir este mostrará los errores de validación ya que ha sido
tocado.
Mostrando errores con ngMessages
Como habrás podido comprobar, mostrar errores de validación es una tarea un poco
engorrosa por la gran cantidad de repetición de código que se necesita. Para mostrar
correctamente los errores hasta el momento hemos necesitado una serie de ng-show
para cada uno de los errores. Además, hemos estado repitiendo constantemente el
nombre del formulario, el nombre del elemento, el objeto $error en cada una de las
validaciones. Existe una vía para solucionar estos problemas y es la que describiré a
continuación.
Con la llegada de Angular 1.3 fue creado un módulo que soluciona el problema a la hora
demostrar errores de validación. Este módulo no forma parte del núcleo de Angular
lo que quiere decir que tendremos que instalarlo de manera independiente, e incluirlo
como un script en la aplicación, así como en las dependencias del módulo que estas
desarrollando. Veamos los pasos uno a uno.
Para comenzar lo primero es obtener el módulo. Este lo podemos obtener por las mismas
vías que obtenemos Angular. Para este ejemplo utilizaremos bower* para simplificar el
proceso. En la consola vamos hasta el directorio donde se encuentra nuestra aplicación
y ejecutamos el siguiente comando.
1
bower install angular-messages
Bower se encargará de obtener el módulo por nosotros y lo pondrá en directorio
de los componentes de bower. Ahora necesitamos incluirlo en nuestra aplicación. Es
importante que se incluya después de haber incluido el framework.
1
2
3
4
...
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-messages/angular-messages.js"></script>
...
Ahora que ya lo tenemos disponible en la aplicación, es necesario añadirlo como dependencia del módulo que estamos desarrollado. Para ellos vamos a la declaración del
módulo y especificamos como dependencia ngMessages.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
182
<script>
angular.module('app',['ngMessages'])
.controller('ctrl', ['$scope', function($scope){
}]);
</script>
Con este nuevo módulo en la aplicación, ganamos 3 nuevas directivas que explicaremos
a continuación. Anteriormente para mostrar los mensajes de validación teníamos que
implementar mensajes basados en la directiva ng-show y las propiedades del objeto $error
de cada elemento. Ahora Con la nueva directiva ng-messages podemos especificar el
objeto $error de un elemento. Después anidado dentro podremos especificar mensajes de
validación para cada uno de los errores de validación utilizando la directiva ng-message.
Vamos a ver un ejemplo donde utilizamos el nuevo soporte para los inputs de tipo date.
En este especificaremos una fecha mínima y una fecha máxima, además lo haremos
requerido. Para este elemento mostraremos mensajes de validación para cada uno de
los errores.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body ng-controller="ctrl">
<form name="formulario">
Fecha:
<input type="date"
min="2015-09-22"
max="2015-10-22"
ng-model="fecha" name="fecha" required>
<span ng-if="formulario.fecha.$dirty" ng-messages="formulario.fecha.$error">
<span ng-message="required">
La fecha es requerida</span>
<span ng-message="min">
La fecha debe ser posterior a 2015-09-22</span>
<span ng-message="max">
La fecha debe ser antes de 2015-10-22</span>
</span><br><br>
Correo:
<input type="email"
minlength="3"
required
ng-model="email" name="email" >
<span ng-if="formulario.email.$dirty" ng-messages="formulario.email.$error">
<span ng-message="required">
El correo es requerido</span>
<span ng-message="minlength">
Capítulo 11: Formularios y Validación
25
26
27
28
29
30
31
32
33
34
35
36
37
38
183
El correo debe terner al menos 3 caracteres</span>
<span ng-message="email">
El correo es inválido</span>
</span>
</form>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-messages/angular-messages.js"></script>
<script>
angular.module('app',['ngMessages'])
.controller('ctrl', ['$scope', function($scope){
$scope.fecha = new Date();
}]);
</script>
</body>
En el ejemplo anterior tenemos dos inputs dentro del formulario, uno de tipo date y
otro de tipo email. Debajo de cada elemento tenemos una etiqueta span donde se define
la directiva ng-messages especificando el objeto error del elemento. Anidados dentro
tenemos una serie de span definiendo la directiva ng-message con solo el nombre del
error dentro del objeto $error. Estos mensajes aparecerán en el orden en que se hayan
definido. Es importante mencionar que para que funcione, los elementos de formulario
necesitan un modelo definido con la directiva ng-model.
En algunas ocasiones necesitamos que se muestren varios errores de validación a la
vez. Por ejemplo, en el elemento del correo posee un límite mínimo de 3 caracteres,
si escribimos solo dos letras en elemento, la validación para mínimo y para correo se
dispararán. Pero por el comportamiento por defecto que tiene la directiva ng-messages
solo mostrara el primer error de validación. Para solucionar este problema podremos
utilizar una tercera directiva que proporciona el módulo ngMessages y es ng-messagesmultiple. Esta directiva debe estar en el mismo elemento donde se ha utilizado ngmessages.
1
2
3
4
5
6
...
<span
ng-if="formulario.email.$dirty"
ng-messages="formulario.email.$error"
ng-messages-multiple>
...
En resumen, la directiva ng-messages** observa los cambios en el objeto error del elemento
de formulario especificado. Esta va activando y desactivando los mensajes de error dependiendo
del orden definido en los mensajes. La directiva *ng-message debe ser definida como hijo del
elemento que posee la directiva ng-messages y es la encargada de mostrar y ocultar un
Capítulo 11: Formularios y Validación
184
mensaje de validación específico. La directiva ng-messages-multiple debe ser definida en el
elemento que posee la directiva ng-messages, esta añadirá el comportamiento de mostrar
varios mensajes de error a la vez.
Reusando mensajes de validación
Con el uso del módulo ngMessages es posible reutilizar mensajes de validación genéricos.
En casos que en tu aplicación no sea tan importante utilizar mensajes genéricos en la
validación, como son los mensajes para un elemento que es requerido o que el elemento
debe tener un mínimo de caracteres específico. Para estos casos tendremos una nueva
directiva que permitirá incluir un archivo HTML con la definición de los mensajes
genéricos. De esta forma podríamos ahorrarnos gran cantidad de código repetido.
Para hacer uso de esta funcionalidad necesitamos crear un archivo HTML con los
mensajes definidos. Este archivo es incluido mediante la directiva ng-messages-include
que recibe como valor la dirección del archivo a incluir. Veamos un ejemplo de su uso.
Primero creamos un archivo con el nombre errores.html con los siguientes errores como
contenido.
1
2
3
4
5
6
7
8
9
<span ng-message="required">
Este elemento es requerido.
</span>
<span ng-message="minlength">
No ha completado el mínimo de caracteres para este elemento.
</span>
<span ng-message="maxlength">
Ha sobrepasado el máximo de caracteres para este elemento.
</span>
A continuación, incluimos el archivo errores.html con la directiva ng-messages-include.
1
2
3
4
5
6
7
8
9
10
<body ng-controller="ctrl">
<form name="formulario">
Correo:
<input type="email"
minlength="3"
required
ng-model="email" name="email" >
<span
ng-if="formulario.email.$dirty"
ng-messages="formulario.email.$error"
Capítulo 11: Formularios y Validación
11
12
13
14
15
16
17
18
19
20
21
22
185
ng-messages-include="errores.html"
ng-messages-multiple>
<span ng-message="email">El correo es inválido</span>
</span>
</form>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-messages/angular-messages.js"></script>
<script>
angular.module('app',['ngMessages'])
.controller('ctrl', ['$scope', function($scope){}]);
</script>
</body>
Como habrás podido observar he dejado un mensaje de error para la comprobación
del correo ya que este no funcionaría para otros elementos. Es importante mencionar
que además de los mensajes incluidos, la directiva ng-messages mostrará con prioridad
los mensajes definidos como hijos. Esto nos da la posibilidad de poder reemplazar
mensajes específicos para un elemento. Este mismo archivo lo podemos reutilizar en otro
elemento que requiera mensajes de validación para un mínimo y máximo de caracteres
y sea requerido.
Soporte para nuevos elementos de HTML5
Con la llegada de HTML5 obtuvimos nuevos tipos de input de los cuales hemos estado sirviéndonos. En la nueva versión de Angular se ha ampliado el soporte para los
elementos de formularios. Concretamente en la versión 1.3 se añadió soporte para los
elementos de tipo date, time, datetime-local, month y week. En versiones anteriores del
framework teníamos que utilizar un elemento de tipo texto para las fechas, no habíamos
podido utilizar los controles que brinda el navegador ya que Angular no brindaba soporte
para estos elementos.
Elementos date, month y datetime-local en Google Chrome
Capítulo 11: Formularios y Validación
186
En muchas ocasiones cuando trabajamos con fechas, estas son obtenidas desde un API
remoto donde se reciben en un formato JSON. En esta nueva versión de Angular para
poder utilizar los elementos de HTML5 es necesario convertir estas fechas desde el
formato String hacia un objeto Date de Javascript. De lo contrario Angular tirara un
error y los campos serán mostrados como una caja de texto.
Supongamos que estas creando un API para el control de tareas de donde estas son
almacenadas en formato JSON. El Estándar JSON no tiene un formato definido para las
fechas lo que quiere decir que estas serán almacenadas como cadenas de texto. Veamos
un ejemplo de JSON el cual utilizaremos más adelante.
1
2
3
4
5
6
7
{
"tareaId": 171,
"nombre": "Evento Circle",
"descripcion": "Participar en el evento Circle 2015",
"fechaInicio": "09-23-2015",
"fechaFin": "09-25-2015"
}
Como podrás observar en el JSON anterior tenemos fechaInicio y fechaFin los cuales
son dos fechas. Para poder utilizar elementos date en la aplicación para mostrar estas
fechas, primero necesitamos convertirlos en objetos Date de Javascript. Para ello utilizaremos el constructor de la clase Date.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body ng-app="app">
<div style="margin: 0 auto; width: 50%" ng-controller="ctrl">
<h1>Tarea</h1>
<p>Nombre: {{tarea.nombre}}</p>
<p>Descripción: {{tarea.descripcion}}</p>
<p>Inicio: <input type="date" ng-model="tarea.fechaInicio"> </p>
<p>Fin: <input type="date" ng-model="tarea.fechaFin"> </p>
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', function($scope){
$scope.tarea = {
"tareaId": 171,
"nombre": "Evento Circle",
"descripcion": "Participar en el evento Circle 2015",
"fechaInicio": "09-23-2015",
"fechaFin": "09-25-2015"
Capítulo 11: Formularios y Validación
20
21
22
23
24
25
187
};
$scope.tarea.fechaInicio = new Date($scope.tarea.fechaInicio);
$scope.tarea.fechaFin = new Date($scope.tarea.fechaFin);
});
</script>
</body>
En el ejemplo anterior se ha asignado una tarea al $scope una tarea y posteriormente se
han convertido las fechas a objetos Date de Javascript con el constructor. De esta forma
podemos hacer uso de los elementos input de tipo date que aparecen en la vista.
Validación de HTML5
En versiones 1.2.x de AngularJS existe soporte para alguno de las validaciones de
HTML5. En la nueva versión del framework la validación de HTML5 está completamente soportada. Ya no sería necesario utilizar las directivas ng-minlength y ng-maxlength para
definir la cantidad mínima y máxima de caracteres. Ahora todos los errores de validación
están correctamente unidos a la propiedad $error de cada elemento del formulario.
En esta nueva versión podremos utilizar la validación de HTML5 como min, max,
minlength, maxlength y pattern. Ahora que disponemos de soporte para elementos de
formulario tipo date la validación min y max serán de gran ayuda para definir errores
para un mínimo y un máximo de fechas. Para lograrlo, ahora disponemos de la nueva
propiedad date en el objeto **$error$ del elemento de formulario.
1
<span class="error" ng-show="formulario.fecha.$error.date">
Otras formas de validación
Ya hemos visto como validar el formulario, pero en la versión 1.3 de Angular se introdujo
una nueva directiva que va a hacer aún más sencillo la validación. Esta nueva directiva
es ng-model-options y nos permite definir algunas opciones para la forma en que
queremos que se actualice el modelo. Como parámetro recibe un objeto con varios
elementos de configuración. Vamos a detallarlos y después los veremos con ejemplos.
Esta directiva es de tipo atributo y en el objeto que recibe como configuración, figuran
los siguientes elementos.
• updateOn: Recibirá una cadena de texto con el nombre de un evento el cual será el
encargado de actualizar el modelo. Como ejemplo podremos utilizar el evento blur,
entonces el modelo se actualizará cuando el elemento pierda el foco. Varios eventos
Capítulo 11: Formularios y Validación
188
pueden ser utilizados, especificando cada uno separados por un espacio. Además,
existe un evento con el nombre default, el cual realizará el evento por defecto del
control donde se utilice.
• debounce: Recibirá un número especificando la cantidad de milisegundos que se
esperará antes de actualizar el modelo después de la última actualización. Esta
opción funciona esencialmente de la siguiente forma. Cuando una modificación
se haya realizado se disparará un temporizador con la cantidad de milisegundos
especificados y al finalizar este se ejecutará la acción de actualizar el modelo. En
caso de que alguna modificación se realice antes de que termine el temporizador,
este es reiniciado y comenzará nuevamente. Si esta propiedad en vez de recibir un
número, recibe un objeto, en él se podría especificar un tiempo en milisegundos
para cada uno de los eventos. De esta forma se escribiría evento:tiempo por ejemplo
{‘default’: 1000, ‘blur’: 0} lo que haría que normalmente espere 1 segundo para
actualizar, pero si pierde el foco actualiza de forma instantánea.
• getterSetter: Esta opción recibe como parámetro un valor booleano que definirá
si este modelo estará unido a una función getter/setter.
• allowInvalid: Recibirá un valor de tipo booleano que define si permitiremos o no
que el modelo sea actualizado con valores inválidos. Este comportamiento previene
que el modelo sea undefined cuando el valor introducido en el campo es invalido.
Ahora veamos varios ejemplos de su uso.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body ng-app="app">
<div ng-controller="ctrl">
<form action="#" name="form">
<input type="email" name="email" id="email"
ng-model="email" ng-model-options="{updateOn: 'blur' }">
<span ng-show="form.email.$invalid">El correo es incorrecto.</span>
<p>{{email}}</p>
</form>
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', ['$scope', function($scope) {}]);
</script>
</body>
En ejemplo anterior podemos comprobar como solo se actualiza el modelo cuando la
caja de texto pierde el foco. Con esto adicionalmente obtenemos que la validación no
se ejecutará en cada una de las ocasiones donde el usuario presione una tecla. Por este
Capítulo 11: Formularios y Validación
189
motivo mostrar los mensajes de validación se hace un poco más sencillo y se necesita
escribir menos código.
Si en la directiva especificamos varios tipos de configuración, aun así, esta funcionaria
de forma esperada. En el caso de que especifiquemos los eventos en que queremos que se
actualice, pero además especificamos el tiempo que queremos esperar antes de actualizar
el modelo, podríamos utilizar las dos propiedades de configuración sin ningún problema.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body ng-app="app">
<div ng-controller="ctrl">
<form action="#" name="form">
<input type="email" name="email" id="email"
ng-model="email"
ng-model-options="{
updateOn: 'default blur',
allowInvalid: true,
debounce: { 'default': 2000, 'blur': 0}
}">
<span ng-show="form.email.$invalid">El correo es incorrecto.</span>
<p>{{email}}</p>
</form>
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', ['$scope', function($scope) {}]);
</script>
</body>
De esta forma cuando dejemos de escribir se disparará el temporizador y dos segundos
después se actualizará el modelo. O de lo contrario podemos salir de la caja de texto y
esta se actualizará de forma instantánea.
En el siguiente ejemplo utilizaremos la opción getterSetter para convertir una fecha que
obtenemos en formato string a una instancia del objeto Date. Con esta funcionalidad
podremos hacer uso del input de tipo date de HTML.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<body ng-app="app">
<div style="margin: 0 auto; width: 50%" ng-controller="ctrl">
<h1>Tarea</h1>
<p>Nombre: {{tarea.nombre}}</p>
<p>Descripción: {{tarea.descripcion}}</p>
<p>Inicio: <input type="date" ng-model="tarea.fecha.inicio"
ng-model-options="{getterSetter: true}"></p>
<p>Fin: <input type="date" ng-model="tarea.fecha.fin"
ng-model-options="{getterSetter: true}"> </p>
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', function($scope){
$scope.tarea = {
"tareaId": 171,
"nombre": "Evento Circle",
"descripcion": "Participar en el evento Circle 2015",
"fechaInicio": "09-23-2015",
"fechaFin": "09-25-2015"
};
var _inicio = new Date($scope.tarea.fechaInicio);
var _fin = new Date($scope.tarea.fechaFin);
$scope.tarea.fecha = {
inicio: function(val) {
if (angular.isDefined(val)){
_inicio = val;
$scope.tarea.fechaInicio = _inicio;
}
return _inicio;
},
fin: function(val){
if (angular.isDefined(val)){
_fin = val;
$scope.tarea.fechaInicio = _fin;
}
return _fin;
}
}
});
</script>
190
Capítulo 11: Formularios y Validación
43
191
</body>
Lo primero que necesitamos hacer es establecer la directiva ng-model-options con la
propiedad getterSertter a un valor verdadero en cada uno de los elementos de tipo date
del formulario. Lo siguiente es crear las funciones que estarán ejecutándose cuando estos
modelos sean requeridos. Creare dos variables una para cada una de las fechas. Cómo
trabajan las funciones getters y setters en Javascript es de la siguiente forma. Cuando
es invocada sin parámetros, esta devuelve el valor. Si es invocada con un valor como
parámetro esta cambia el valor.
Estas funciones las pondremos dentro de un objeto con el nombre fecha dentro de la
tarea. Además, necesitamos definir dos variables para convertir la fecha inicialmente,
las cuáles serán las devueltas por las funciones. Para terminar, necesitamos cambiar las
propiedades en las directivas ng-model que apunten a las nuevas fechas.
En casos muy específicos podremos especificar la configuración allowInvalid para permitir que el modelo sea actualizado con valores inválidos. En la mayoría de las ocasiones
esto no es lo que quisiéramos para una aplicación.
Si te has dado cuenta que si utilizas un botón con la directiva ng-click donde utilizamos
alguno de los elementos del formulario, y estos aún no han actualizado el modelo,
podríamos obtener comportamientos indeseados. Por este motivo cuando hagas uso de
la directiva ng-model-options asegúrate de no utilizar la directiva ng-click para el evento de
acción, sino la directiva ng-submit. Esta directiva ejecutará de manera instantánea todos
los eventos en espera y se actualizará el modelo antes de ejecutar las acciones.
Resetear elementos de formulario
En versiones anteriores a Angular 1.3, si necesitáramos implementar una funcionalidad
donde pudiéramos resetear los elementos del formulario, tendrías que hacerla de forma
manual. En esta nueva versión se incluye esta funcionalidad a nivel de formulario o de
un elemento en específico. Es importante mencionar que esta funcionalidad solo estará
disponible para los elementos que tengan la directiva ng-model-options con una de las
propiedades updateOn o debounce.
Para implementarlo a nivel de formulario, podemos utilizar la directiva ng-modeloptions con la propiedad updateOn en el evento submit. Veámoslo en el ejemplo que se
muestra a continuación.
Capítulo 11: Formularios y Validación
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
192
<body ng-app="app">
<div ng-controller="ctrl">
<form name="form" ng-model-options="{updateOn: 'submit'}">
<div><label for="nombre">
Nombre: <input type="text" name="nombre" id="nombre" ng-model="nombre">
</label></div><br>
<div><label for="email">
Email: <input type="email" name="email" id="email" ng-model="email">
</label></div>
<div><br>
<button ng-click="form.$rollbackViewValue()">Resetear</button>
</div>
<hr>
<div><p>{{nombre}}</p><p>{{email}}</p></div>
</form>
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', ['$scope', function($scope) {
$scope.nombre = 'John Doe';
$scope.email = '[email protected]';
}]);
</script>
</body>
En el ejemplo anterior se ha creado un botón que resetea el formulario con sus valores a
su estado inicial. Esta funcionalidad la podemos implementar a nivel de elemento. En el
siguiente ejemplo implementaremos esta funcionalidad para los elementos. Queremos
que cuando el usuario presione la tecla escape sin haber salido del elemento, este vuelva
a su estado inicial.
1
2
3
4
5
6
7
8
9
10
<body ng-app="app">
<div ng-controller="ctrl">
<form name="form">
<div><label for="nombre">Nombre:
<input type="text" id="nombre" name="nombre"
ng-model="nombre" ng-model-options="{updateOn: 'blur'}"
ng-keyup="cancelar(form.nombre, $event)">
</label></div><br>
<div><label for="email">Email:
<input type="email" id="email" name="email"
Capítulo 11: Formularios y Validación
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
193
ng-model="email" ng-model-options="{updateOn: 'blur'}"
ng-keyup="cancelar(form.email, $event)">
</label></div>
<hr>
<div><p>{{nombre}}</p><p>{{email}}</p></div>
</form>
</div>
<script src="bower_components/angular/angular.js"></script>
<script>
angular.module('app', [])
.controller('ctrl', ['$scope', function($scope) {
$scope.nombre = 'John Doe';
$scope.email = '[email protected]';
$scope.cancelar = function(control, evt) {
if ( evt.keyCode == 27 ) {
control.$rollbackViewValue();
}
}
}]);
</script>
</body>
En este ejemplo he creado un método que recibe dos parámetros. El primero es el
elemento del formulario que necesitamos resetear y el segundo es el evento. Compruebo
si la tecla presionada es la 27 la cual es escape y restauro el modelo a su estado inicial.
Es importante recordar que esta funcionalidad solo estará disponible para los elementos
que tengan definido la directiva ng-model-options en sí.
Para la última propiedad no necesitamos un ejemplo, solo mencionar que si agregamos
allowInvalid como true a la configuración de ng-model-options; el modelo será actualizado, aunque la validación falle.
Con todos los elementos anteriormente mencionados tienes la posibilidad de crear formularios realmente amigables y cómodos para el usuario. Esto ayuda a que tu aplicación
sea más fácil de utilizar y que el usuario se sienta más confiado utilizándola.
Nombre de elementos interpolables
En versiones anteriores a la 1.3 de AngularJS los nombres de los elementos no podían
ser interpolables, estos debían ser escritos directamente en la vista. Si los nombres de
los elementos del formulario eran obtenidos mediante una llamada al servidor remoto
o especificados dentro del controlador, estos no podían ser expuestos a la vista e
Capítulo 11: Formularios y Validación
194
interpolados con la sintaxis {{ }}. A partir de la versión 1.3 la propiedad name de los
elementos de formulario puede ser interpolada.
En el siguiente ejemplo veremos cómo definimos el nombre del elemento dentro del
controlador. En la vista el nombre del elemento es interpolado. Para comprobar que ha
funcionado vamos a mostrar el formulario mediante el filtro json.
Vista
1
2
3
4
5
6
<body ng-controller="AppCtrl as vm">
<form name="miForm">
<input type="email" name="{{vm.elemCorreo}}" ng-model="vm.email">
<pre>{{miForm | json}}</pre>
</form>
</body>
Controlador
1
2
3
4
5
6
7
8
angular.module('app', [])
.controller('AppCtrl', AppCtrl);
function AppCtrl(){
var vm = this;
vm.elemCorreo = 'correo';
vm.email = '';
}
Como habrás podido observar el elemento ha tomado el nombre correo ya que en el
controlador lo definimos con ese nombre. Esta nueva funcionalidad nos permitirá crear
elementos de formularios para modelos directamente desde el controlador. Además,
para crear formularios en los que el usuario pueda añadir campos, por ejemplo, en un
formulario de contacto donde se puedan añadir campos que no hayan sido definidos por
defecto.
Servidor API RESTful
Para propósitos de este libro como contenido extra he desarrollado un servidor utilizando NodeJS, Express.js y MongoDB. En este servidor podrás hacer prueba de todos
los ejemplos expuestos en este libro. También dispone de un API RESTful para realizar
peticiones y es el que he utilizado en el Capítulo 10 para demostrar el uso del servicio
$resource de Angular. A continuación, explicaré el uso de este servidor paso a paso.
Requerimientos
Al estar desarrollado con NodeJS es necesario que node esté disponible en tu sistema
para ejecutar la aplicación. Si aún no tienes node instalado puedes obtenerlo desde el sitio
oficial http://nodejs.org¹⁵ y seguir los pasos de la instalación hasta tenerlo disponible.
Para asegurarte que node está listo para ser usado puedes ir a la consola en Mac y Linux
o el intérprete de comandos en Windows y ejecutar.
1
node --version
Deberás obtener una respuesta similar a esta v0.10.35 de lo contrario necesitarás volver a
realizar los pasos de la instalación. Además de node necesitas npm que es el encargado de
gestionar las dependencias en node, esta utilidad viene en la misma instalación de node
y es la que utilizaremos para instalar las dependencias del servidor.
Ahora que ya tenemos listo node y npm necesitamos instalar bower. Bower es un gestor
de paquetes para el frontend con el cual obtendremos las librerías de angular y angularresource. Para instalar bower en el sistema lo hacemos mediante npm.
1
npm install -g bower
Después de haber instalado bower podemos ejecutar en la consola el siguiente comando
para asegurarnos de que se ha instalado correctamente.
1
node --version
¹⁵http://nodejs.org
195
Servidor API RESTful
196
Si obtenemos una respuesta similar a 1.3.12 estamos listos para comenzar a obtener las
dependencias del servidor.
Otro de los requisitos que necesitamos para poder correr el servidor es un servidor
de MongoDB, podemos tener un propio servidor local obteniendo MongoDB desde su
sitio oficial http://www.mongodb.org¹⁶. O podremos utilizar uno de los servidores en
internet como Mongolab.com¹⁷ que incluso se puede utilizar gratis.
Estos son todos los requisitos para ejecutar el servidor, ahora necesitaremos instalar las
dependencias y configurar el servidor.
Instalando dependencias
Para instalar las dependencias abrimos la consola y vamos hasta la carpeta donde
tenemos el servidor. Ejecutamos el comando npm install y esperamos a que termine de
instalar. Cuando npm finalice ejecutará bower para gestionar las librerías.
Bower instalará las dependencias en la carpeta public/lib para que estén disponibles
como archivos estáticos desde el servidor. Generalmente bower instala las dependencias
en una carpeta llamada bower_components, este comportamiento ha sido cambiando
mediante el archivo .bowerrc que está en la carpeta del servidor.
Configurando el servidor
El servidor en si es solo el archivo llamado server.js que está en la raíz. Para configurarlo
abre el archivo en tu editor de texto favorito y ve hasta la línea 18. Aquí se definen 4
variables.
1.
2.
3.
4.
appPort: El puerto por el que el servidor estará esperando conexiones.
dbServer: Dirección del servidor de base de datos MongoDB
dbPort: Puerto del servidor de base de datos MongoDB
dbName: Nombre de la base de datos que utilizará este servidor.
Configurando cada una de estas variables con los datos reales que necesites utilizar,
quedará configurado el servidor listo para usarse.
Iniciando el servidor
Para iniciar el servidor dirígete en la consola hasta la carpeta del servidor y ejecuta node
server y el servidor comenzará a esperar conexiones por el puerto que has definido en la
configuración.
¹⁶http://www.mongodb.org
¹⁷https://mongolab.com
Servidor API RESTful
197
Uso del servidor
La primera vez que el servidor inicie intentará introducir mensajes de prueba en la
base de datos para posteriormente utilizarlos en los ejemplos. En caso de que no pueda
imprimirá el error en la consola.
El servidor posee una API RESTful para mensajes. Solo tiene definido dos rutas conformando así un recurso REST.
• Para acceder a la lista de los mensajes puedes hacerlo mediante una petición GET
a /api/mensajes.
• Para acceder a un mensaje especifico ejecuta una petición GET a /api/mensajes/:mid
donde :mid sea la id del mensaje. Este se puede obtener en la propiedad mid que
posee cada mensaje.
• Para crear un nuevo mensaje ejecuta una petición POST /api/mensajes con un objeto
json como cuerpo de la petición. El objeto debe contener dos propiedades. 1:
usuario y 2: mensaje.
• Para actualizar un mensaje debes hacer una petición PUT a /api/mensajes/:mid con
un objeto json en el cuerpo de la petición con las propiedades usuario y mensaje.
• Para eliminar un mensaje ejecuta una petición DELETE a /api/mensajes/:mid.
En caso de que exista algún error en alguna de las peticiones el servidor devolverá un
objeto json con la propiedad mensaje explicando el error ocurrido.
Las peticiones POST y PUT devuelven el nuevo objeto para que pueda ser utilizado en
el cliente.
Para todas las demás peticiones GET que no cumplan con ninguna de las rutas anteriormente mencionadas se devolverá el archivo public/main.html como respuesta. En este
archivo reside la aplicación angular detallada en el Capítulo 10. Para cualquier prueba
que necesites realizar puedes utilizar este archivo, así como el de la aplicación que reside
en public/js/app.js.
Descargar