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 angular@googlegroups.com 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)">&times;</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)">&times;</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": "yo@dominio.com", "lenguajes": ["en", "es"] }, { "nombre": "john", "apellidos": "Doe", "email": "johndoe@example.com", "lenguajes": ["en"] }, { "nombre": "Jane", "apellidos": "Doe", "email": "janedoe@example.com", "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": "junior@example.com", "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: 'lorem@example.com', 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: 'johndoe@example.com' }; }]) 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: 'johndoe@example.com' }; $scope.usuario2 = { nombre: 'Jane', apellido: 'Doe', email: 'janedoe@example.com' }; }]) 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: "yo@dominio.com", 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: "johndoe@example.com", lenguajes: ["en"] }, { nombre: "Jane", apellidos: "Doe", email: "janedoe@example.com", 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: 'yo@dominio.com', tel: '123456789', }, { nombre: 'john Doe', email: 'johndoe@example.com', 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 &lt;sc\ ript&gt; 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 &lt;script&gt; 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 = [ 'john@example.com', 'jane@example.com', 'jimmy@example.com' ]; $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 = 'john.doe@dominio.com'; }]); </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 = 'john.doe@dominio.com'; $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.