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
¡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.
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 . . . .
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
<meta charset="UTF-8"> Titulo
Hola Mundo!
Feedback El feedback de los lectores siempre es bienvenido. Me gustaría saber qué piensas acerca de este libro que te ha gustado más y que no te ha gustado. Lo tendré presente para próximas actualizaciones. Para enviar un feedback envía un tweet a @mriverodorta.
Errata Este es el primer libro que escribo así que asumo que encontraran varios errores. Tú puedes ayudarme a corregirlos enviándome un tweet con el error que has encontrado a @mriverodorta junto con los detalles del error. Los errores serán solucionados a medida que sean encontrados. De esta forma estarán arreglados en próximas versiones del libro.
Preguntas Si tienes alguna pregunta relacionada con algún aspecto del libro puedes hacerla a @mriverodorta con tus dudas.
Recursos AngularJS posee una gran comunidad a su alrededor además del equipo de Google que trabaja dedicado a este framework. A continuación, mencionaré algunos de los sitios donde puedes encontrar recursos y documentación relacionada al desarrollo con AngularJS.
Prólogo
v
Sitios de referencia • • • • • •
Sitio web oficial http://www.angularjs.org² Google+ https://plus.google.com/u/0/communities/115368820700870330756³ Proyecto en Github https://github.com/angular/angular.js⁴ Grupo de Google [email protected] Canal en Youtube http://www.youtube.com/user/angularjs⁵ Twitter @angularjs
Extensiones La comunidad alrededor de AngularJS ha desarrollado gran cantidad de librerías y extensiones adicionales que agregan diferentes funcionalidades al framework y tienen sitio en: http://ngmodules.org⁶.
IDE y Herramientas Si eres un desarrollador web, para trabajar con AngularJS no es necesario que utilices algo diferente de lo que ya estés acostumbrado, puedes seguir usando HTML y Javascript como lenguajes y si estás dando tus primeros pasos en este campo podrás utilizar un editor de texto común. Aunque te recomendaría usar un ⁷IDE que al comienzo te será de mucha ayuda con alguna de sus funciones como el auto-completamiento de código, hasta que tengas un mayor entendimiento de las propiedades y funciones. A continuación, recomendare algunos: • WebStorm: Es un potente IDE multiplataforma que podrás usar lo mismo en Mac, Linux o Windows. Además, se le puede instalar un Plugin para el trabajo con AngularJS que fue desarrollado por la comunidad. • SublimeText: También multiplataforma y al igual posee un plugin para AngularJS pero no es un IDE es sólo un editor de texto. • Espreso: Sólo disponible en Mac enfocado para su uso en el frontend.
Navegador Nuestra aplicación de AngularJS funciona a través de los navegadores más populares en la actualidad (Google Chrome, Safari, Mozilla Firefox). Aunque recomiendo Google Chrome ya que posee una extensión llamada Batarang para inspeccionar aplicaciones AngularJS y la misma puede ser instalada desde Chrome Web Store. ²http://www.angularjs.org ³https://plus.google.com/u/0/communities/115368820700870330756 ⁴https://github.com/angular/angular.js ⁵http://www.youtube.com/user/angularjs ⁶http://ngmodules.org ⁷Integrated Development Environment
Alcance Este libro abarcará la mayoría de los temas relacionados con el framework AngularJS. Está dirigido a aquellos desarrolladores que ya poseen conocimientos sobre el uso de AngularJS y quisieran indagar sobre algún tema en específico. A continuación, describiré por capítulos los temas tratados en este libro.
Capítulo 1: Primeros pasos En este capítulo se abordarán los temas iniciales para el uso del framework, sus principales vías para obtenerlo y su inclusión en la aplicación. Además de la definición de la aplicación, usos de las primeras directivas y sus ámbitos. La creación del primer controlador y su vinculación con la vista y el modelo. Se explicarán los primeros pasos para el uso del servicio $scope.
Capítulo 2: Estructura Este capítulo se describirá la importancia de tener una aplicación organizada. La estructura de los directorios y archivos. Comentarios sobre el proyecto angular-seed para pequeñas aplicaciones y las recomendaciones para aquellas de estructura medianas o grandes. Además de analizar algunos de los archivos esenciales para hacer que el mantenimiento de la aplicación sea sencillo e intuitivo.
Capítulo 3: Módulos En este capítulo comenzaremos por aislar la aplicación del entorno global con la creación del módulo. Veremos cómo definir los controladores dentro del módulo. También veremos cómo Angular resuelve el problema de la minificación en la inyección de dependencias y por último los métodos de configuración de la aplicación y el espacio para tratar eventos de forma global con el método config() y run() del módulo.
Capítulo 4: Servicios AngularJS dispone de una gran cantidad de servicios que hará que el desarrollo de la aplicación sea más fácil mediante la inyección de dependencias. También comenzaremos a definir servicios específicos para la aplicación y se detallarán cada una de las vías para crearlos junto con sus ventajas. vi
Alcance
vii
Capítulo 5: Peticiones al servidor Otra de las habilidades de AngularJS es la interacción con el servidor. En este capítulo trataremos lo relacionado con las peticiones a los servidores mediante el servicio $http. Como hacer peticiones a recursos en un servidor remoto, tipos de peticiones y más.
Capítulo 6: Directivas Las directivas son una parte importante de AngularJS y así lo reflejará la aplicación que creemos con el framework. En este capítulo haremos un recorrido por las principales directivas, con ejemplos de su uso para que sean más fáciles de asociar. Además, se crearán directivas específicas para la aplicación.
Capítulo 7: Filtros En este capítulo trataremos todo lo relacionado con los filtros, describiendo los que proporciona angular en su núcleo. También crearemos filtros propios para realizar acciones específicas de la aplicación. Además de su uso en las vistas y los controladores y servicios.
Capítulo 8: Rutas Una de las principales características de AngularJS es la habilidad que tiene para crear aplicaciones de una sola página. En este capítulo estaremos tratando sobre el módulo ngRoute, el tema del manejo de rutas sin recargar la página, los eventos que se procesan en los cambios de rutas. Además, trataremos sobre el servicio $location.
Capítulo 9: Eventos Realizar operaciones dependiendo de las interacciones del usuario es esencial para las aplicaciones hoy en día. Angular permite crear eventos y dispararlos a lo largo de la aplicación notificando todos los elementos interesados para tomar acciones. En este capítulo veremos el proceso de la propagación de eventos hacia los $scopes padres e hijos, así como escuchar los eventos tomando acciones cuando sea necesario.
Capítulo 10: Recursos En la actualidad existen cada vez más servicios RESTful en internet, en este capítulo comenzaremos a utilizar el servicio ngResource de Angular. Realizaremos peticiones a un API REST y ejecutaremos operaciones CRUD en el servidor a través de este servicio.
Alcance
viii
Capítulo 11: Formularios y Validación Hoy en día la utilización de los formularios en la web es masiva, por lo general todas las aplicaciones web necesitan al menos uno de estos. En este capítulo vamos a ver como emplear las directivas para validar formularios, así como para mostrar errores dependiendo de la información introducida por el usuario en tiempo real.
Extra: Servidor API RESTful En el Capítulo 10 se hace uso de una API RESTful para demostrar el uso del servicio $resource. En este extra detallaré el proceso de instalación y uso de este servidor que a la vez viene incluido con el libro y estará disponible con cada compra. El servidor esta creado utilizando NodeJs, Express.js y MongoDB.
Introducción A lo largo de los años hemos sido testigo de los avances y logros obtenidos en el desarrollo web desde la creación de World Wide Web. Si comparamos una aplicación de aquellos entonces con una actual notaríamos una diferencia asombrosa, eso nos da una idea de cuan increíble somos los desarrolladores, cuantas ideas maravillosas se han hecho realidad y en la actualidad son las que nos ayudan a obtener mejores resultados en la creación de nuevos productos. A medida que el tiempo avanza, las aplicaciones se hacen más complejas y se necesitan soluciones más inteligentes para lograr un producto final de calidad. Simultáneamente se han desarrollado nuevas herramientas que ayudan a los desarrolladores a lograr fines en menor tiempo y con mayor eficiencia. Hoy en día las aplicaciones web tienen una gran importancia, por la cantidad de personas que utilizan Internet para buscar información relacionada a algún tema de interés, hacer compras, socializar, presentar su empresa o negocio, en fin, un sin número de posibilidades que nos brinda la red de redes. Una de las herramientas que nos ayudará mucho en el desarrollo de una aplicación web es AngularJS, un framework desarrollado por Google, lo que nos da una idea de las bases y el soporte del framework por la reputación de su creador. En adición goza de una comunidad a su alrededor que da soporte a cada desarrollador con soluciones a todo tipo de problemas. Por estos tiempos existen una gran cantidad de frameworks que hacen un increíble trabajo a la hora de facilitar las tareas de desarrollo. Pero AngularJS viene siendo como el más popular diría yo, por sus componentes únicos, los cuales estaremos viendo más adelante. En este libro estaremos tratando el desarrollo de aplicaciones web con la ayuda de AngularJS y veremos cómo esta obra maestra de framework nos hará la vida más fácil a la hora de desarrollar aplicaciones web.
ix
Segunda Edición En esta segunda edición se cubrirán los cambios y nuevas funcionalidades de la versión 1.3 de AngularJS en adelante. Esta nueva versión del framework tiene gran cantidad de cambios en las funcionalidades ya existentes. Además, tiene algunos cambios que debes considerar antes de cambiar de versión ya que podría poner en riesgo la cobertura de tu aplicación con respecto a los navegadores. En esta revisión del libro encontrarás la información necesaria para sacar un mejor provecho de las nuevas funcionalidades. Si estas a punto de comenzar a crear una nueva aplicación puedes hacer uso del contenido sin preocupaciones. Si ya tienes una aplicación y deseas migrar a la nueva versión de Angular, antes de hacerlo debes estar consciente de los problemas que podría presentar. En esta segunda edición del libro describiré los cambios relacionados en cada capítulo del libro donde hablare al detalle sobre las modificaciones del framework para esta nueva versión. En la versión 1.3 de AngularJS hay grandes mejoras en el rendimiento. Con solo cambiar de una versión anterior a la nueva versión, sin hacer cambios en el código de la aplicación, el rendimiento será mucho mejor. Esta versión incluye mejoras en el procesamiento y en el manejo de operaciones con el DOM. Además, se incluyen nuevas funcionalidades con un API más sencillo que permitirá utilizar las nuevas funcionalidades de forma más fácil y con menos código. Estas nuevas funcionalidades brindan más control sobre los elementos como los formularios, mensajes de validación, modelos, controladores y directivas. Todos estos cambios están orientados a hacerte más productivo con la nueva versión del framework. Aunque tiene muchas partes buenas podría tener algunos inconvenientes. En esta versión AngularJS ha retirado el soporte para la versión 8 de Internet Explorer. Esto quiere decir que si tu aplicación está enfocada para usuarios de Windows XP no sería una buena idea hacer un cambio a esta nueva versión sin considerar la pérdida de usuarios. Aunque este es uno de los cambios que nos hace pensar en cambiar de versión, es uno de los que ha hecho que el rendimiento de angular se haya mejorado considerablemente además de la reducción del código base del framework. Otro de los cambios importantes es que el framework ha dejado el soporte de jQuery con versiones menores a la 2.1.1, esto afecta a los desarrolladores que hacen uso del jQuery en sus aplicaciones en sustitución a la versión jqLite. Para finalizar con los cambios inconvenientes debemos agregar que en esta versión se ha eliminado la posibilidad de utilizar funciones globales como controladores. Aunque x
Segunda Edición
xi
Este último cambio no debería afectarte ya que es una mala práctica el uso de funciones globales como controladores y deberías evitar su uso, aunque uses una versión anterior a la 1.3 de Angular.
Entorno de desarrollo Es esencial que para sentirnos cómodos con el desarrollo tengamos a la mano cierta variedad de utilidades para ayudarnos a realizar las tareas de una forma más fácil y en menor tiempo. Esto lo podemos lograr con un buen editor de texto o un IDE. No se necesita alguno específicamente, podrás continuar utilizando el que estás acostumbrado si ya has trabajado Javascript anteriormente.
Seleccionando el editor Existen una gran variedad de editores e IDE en el mercado hoy en día, pero hay algunos que debemos prestar especial atención. Me refiero a editores como Visual Studio Code o Sublime Text 2/3 y al IDE JetBrains WebStorm, los tres son multi plataforma. Personalmente uso Visual Studio Code para mi desarrollo de día a día, con este editor podremos escribir código de una forma muy rápida gracias a las posibilidades que brinda el uso de las referencias a los archivos de definición.
Visual Studio Code
Para Sublime Text existen plugins que te ayudarán a aumentar la productividad. El primer plugin es AngularJs desarrollado por el grupo de Angular-UI, solo lo uso para el auto completamiento de las directivas en las vistas así que en sus opciones deshabilito el auto completamiento en el Javascript. El segundo plugin es AngularJS Snippets el cual uso para la creación de controladores, directivas, servicios y más en el Javascript. Estos dos plugins aumentan en gran cantidad la velocidad en que escribes código.
1
2
Entorno de desarrollo
Sublime Text
Por otra parte WebStorm es un IDE con todo tipo de funcionalidades, auto completamiento de código, inspección, debug, control de versiones, refactorización y además también tiene un plugin para el desarrollo con AngularJS que provee algunas funcionalidades similares a los de Sublime Text.
WebStorm
Preparando el servidor Habiendo seleccionado ya el editor o IDE que usarás para escribir código el siguiente paso es tener listo un servidor donde poder desarrollar la aplicación. En esta ocasión también tenemos varias opciones, si deseas trabajar online Plunker⁸ es una buena opción y Cloud9⁹ es una opción aún más completa donde podrás sincronizar tu proyecto ⁸http://plnkr.co ⁹http://cloud9.io
Entorno de desarrollo
3
mediante git y trabajar en el pc local o en el editor online. En caso de que quieras tener tu propio servidor local para desarrollo puedes usar NodeJs con ExpressJS para crear una aplicación. Veamos un ejemplo. Archivo: App/server.js
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
# 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 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]
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.
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"> Si vamos a usar el CDN de Google seria de la siguiente forma:
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.
<script src="lib/angular.js"> 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
{{ mensaje }}
<script> function miCtrl ($scope) { $scope.mensaje = 'Mensaje desde el controlador'; }
<script src="lib/angular.js"> 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
<script src="lib/angular.js"> <script> function miCtrl ($scope) { $scope.mensaje = 'Mensaje desde el controlador'; }
{{ mensaje }}
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
<script> function miCtrl ($scope) { $scope.mensaje = 'Mensaje desde el controlador'; }
<script src="lib/angular.js"> 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
<script> function padreCtrl ($scope) { $scope.padre = 'Soy el padre'; $scope.logPadre = function(){
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 src="lib/angular.js"> 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
<script src="lib/angular.js"> <script> function ctrl($scope) { $scope.mensaje = ''; }
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.
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
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
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.
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
{{ mensaje }}
<script src="lib/angular-1.3.js"> <script> var app = angular.module('app',[]); app.config(function($controllerProvider){ $controllerProvider.allowGlobals(); }); function miCtrl ($scope) { $scope.mensaje = 'Mensaje desde el controlador'; }
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 ├── │ │ │ │ ├── │ │ ├── │ │ ├── ├── ├──
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 ├── ├── ├── ├── │ │ │ │ │ │ │ ├──
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:
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
{{ mensaje }}
<script src="lib/angular.js"> <script> angular.module('miApp', []) .controller('miCtrl', function ($scope) { $scope.mensaje = 'AngularJS Paso a Paso'; });
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'; }]);
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
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
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
<script src="lib/angular.js"> <script src="js/app.js"> <script src="js/Services/Playlist.js"> <script src="js/Controllers/PlaylistCtrl.js"> <script src="js/Controllers/PlaylistMetodosCtrl.js"> 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
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
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
{{ titulo }}
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.
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
<script src="lib/angular.js"> <script src="js/app.js"> <script src="js/Services/Playlist.js"> <script src="js/Controllers/PlaylistMetodosCtrl.js"> 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.
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
{{ titulo }}
<script src="lib/angular.js"> <script src="js/app.js"> <script src="js/Services/PlaylistService.js"> <script src="js/Controllers/PlaylistServiceCtrl.js"> 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
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
¿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
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.
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
Estado del servidor: {{ status }}
Estado del servicio HTTP: {{ http }}
Estado del servicio de Base de Datos: {{ db }}
Estado de las conexiones seguras: {{ ssl }}
<script src="lib/angular.js"> <script src="js/app.js"> <script src="js/Controllers/PromiseCtrl.js"> 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);
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
{{resuelta}}
{{rechazada}}
<script src="bower_components/angular/angular.js"> <script src="app.js"> 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
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
<script src="bower_components/angular/angular.js"> <script src="app.js"> 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
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
<script src="bower_components/angular/angular.js"> <script src="app.js"> 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
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
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
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
Nombre: {{ usuario.nombre }}
Apellidos: {{ usuario.apellidos }}
Email: {{ usuario.email }}
Lenguajes: | {{ lenguaje }} |
Capítulo 5: Peticiones al servidor
13 14 15 16 17 18
71
<script src="lib/angular.js"> <script src="js/app.js"> <script src="js/Controllers/UsersCtrl.js"> 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
$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
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
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.
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.
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.
<meta charset="UTF-8"> Test <script src="../lib/angular.js"> Flotar Fondo Rojo Bordes Redondeados
Ejemplo de los usos de ng-class
<script> angular.module('miApp', []);
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
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.
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
{{ mensaje }}
{{ mensaje }}
<script> angular.module('miApp', []) .controller('miCtrl', ['$scope', function ($scope) { $scope.mensaje = 'Hola desde el controlador.'; }])
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.
<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; }]);
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
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.
<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; }]);
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.
<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; }]);
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
<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; }]);
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
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.
Estas directivas que se han expuesto hasta ahora pueden ser usadas en varios elementos. Ahora trataremos sobre directivas específicas de algunos elementos HTML.
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 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
...
Nombre:
Email:
...
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
En el código anterior declaramos dos reglas CSS para los elementos 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 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
...
Crear
...
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 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
...
...
Al 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
...
El campo Nombre debe tener al menos 3 caracteres
El campo Nombre excede el máximo de 15 caracteres
El campo Nombre es requerido
...
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
...
La dirección de Correo es incorrecta
El campo Correo es requerido
...
Con el ejemplo anterior obtendremos un resultado como el de la siguiente imagen. Debajo de cada uno de los 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
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.
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.
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
... ...
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
Este elemento es requerido.
No ha completado el mínimo de caracteres para este elemento.
Ha sobrepasado el máximo de caracteres para este elemento.
A continuación, incluimos el archivo errores.html con la directiva ng-messages-include.
1 2 3 4 5 6 7 8 9 10
Correo:
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> El correo es inválido
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
Tarea
Nombre: {{tarea.nombre}}
Descripción: {{tarea.descripcion}}
Inicio:
Fin:
<script src="bower_components/angular/angular.js"> <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); });
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
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
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
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.
<script src="bower_components/angular/angular.js"> <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; } } });
190
Capítulo 11: Formularios y Validación
43
191
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.
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.
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
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.