Des Arrollo de Apps Con Ionic

Desarrollo de Apps con IONIC Aprenda a crear aplicaciones para móviles desde cero con el framework más poderoso y versát

Views 104 Downloads 1 File size 4MB

Report DMCA / Copyright

DOWNLOAD FILE

Recommend stories

Citation preview

Desarrollo de Apps con IONIC Aprenda a crear aplicaciones para móviles desde cero con el framework más poderoso y versátil Victor Hugo Garcia Este libro está a la venta en http://leanpub.com/desarrollodeappsconionic Esta versión se publicó en 2017-02-07

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2017 Victor Hugo Garcia

Para Laura, mi familia, amigos, y todos aquellos que me han enseñado algo

Índice general Capítulo 1: ¿Qué es IONIC? . . . . . . . . . . ¿Qué significa eso de aplicaciones híbridas? Configurando el entorno de desarrollo . . . Sobre el formato del libro . . . . . . . . . . Primera aplicación con ionic . . . . . . . . Estructura de una aplicación en IONIC . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

1 2 2 4 5 12

Capítulo 2: Primera Aplicación Completa . Creación del Proyecto . . . . . . . . . . Creando una nueva página . . . . . . . Seteando la página raíz . . . . . . . . . Creando un modelo de datos . . . . . . Listas y estructuras repetitivas . . . . . Crear una ventana modal . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

31 33 34 37 38 39 51

Capítulo 3: Servidor de desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Api Rest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Servicios Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

86 88 88

. . . . . . .

. . . . . . .

Capítulo 4: Búsquedas y filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 Pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 Gravatar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 Capítulo 5: Corriendo la aplicación desde el emulador APK . . . . . . . . . . . . . . . . . . . . . . . . . . Publicando una aplicación para Android . . . . . . . Palabras de despedida . . . . . . . . . . . . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

135 140 141 144

Capítulo 6: Apéndices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145

Capítulo 1: ¿Qué es IONIC? Si están leyendo este libro, probablemente ya sepan que INONIC es un framework para el desarrollo de aplicaciones móviles híbridas, que permite desplegar dichas apps en dispositivos con Android, IOs y Windows a partir de una única base de código. Sin embargo, hay algunos conceptos que conviene tratar con mayor detalle para sacar luego mayor provecho a esta herramienta. Desde hace un tiempo, el desarrollo de aplicaciones ha estado limitado a un grupo de personas que se especializan en entornos y lenguajes tales como Java, Objective C, Swift y otros. Esta variedad, se desprende de la existencia de distintos sistemas operativos en multitud de dispositivos, por lo que desarrollar una app para diferentes plataformas implica escribir código que pueda ser compilado para el entorno destino, con el consecuente incremento en los esfuerzos de desarrollo. Ante esta situación, algunas personas pensaron que sería interesante poder escribir una única base de código que pudiera luego ser compilado o traducido de alguna manera para obtener versiones de la misma aplicación que funcionen en móviles, tabletas, y otros; prácticamente sin modificaciones. Estos esfuerzos no son nuevos y en ese sentido IONIC no es el pionero. Sin embargo, existen ciertas características que hacen de IONIC un framework único con el que desarrollar aplicaciones móviles se convierte en una experiencia placentera y para nada traumática. Estas son: • IONIC permite utilizar las mismas tecnologías que se emplean para el desarrollo web (HTML5, SASS, JavaScript, TypeScript, Angular) para la construcción de aplicaciones móviles. Esto significa que se abre el juego y muchos desarrolladores web pueden pasar a participar del mundo del desarrollo móvil, aplicando los conocimientos que ya tienen. Esto es algo fantástico para todos aquellos que desean comenzar a desarrollar apps pronto, sin tener que meterse de narices a aprender Java, Objective C, u otro lenguaje que puede resultar críptico. • Existe una única base de código, que permite desplegar las aplicaciones construidas con IONIC a las principales app store de la actualidad. • Es un framework completamente gratuito y open source, con licencia MIT, lo que significa que nunca se deberá pagar algún tipo de comisión por su uso. Existen, por supuesto, otras características que iremos explorando a lo largo de este libro, pero las arriba mencionadas son suficientes para percibir que se trata de una herramienta fantástica a la que podemos sacarle mucho provecho.

Capítulo 1: ¿Qué es IONIC?

2

¿Qué significa eso de aplicaciones híbridas? Al comienzo de este capítulo, se mencionó que IONIC es un framework para la construcción de aplicación móviles híbridas. Veamos ahora qué significa eso y qué implicaciones tiene para nosotros. Una aplicación híbrida, básicamente es una aplicación web que se ejecuta en un dispositivo móvil dentro de un wrapper o “envoltorio”, que es el que en definitiva tiene acceso a la plataforma del dispositivo. Es decir, podemos pensar en una app desarrollada con IONIC como un conjunto de páginas web que son encapsuladas en un envoltorio, que es el que permite que este conjunto de páginas se comporte como una aplicación nativa. El wrapper que utiliza IONIC es Cordova. ¿Esto significa que las apps construidas con IONIC son ejecutadas en un navegador web? Bueno, sí y no. Las apps construidas con IONIC no se ejecutan a través de una aplicación de un navegador móvil como Safari y Chrome, sino a través del navegador de bajo nivel del dispositivo (UIWebView en IOS y WebView en Android) que a su vez tiene como wrapper a Cordova. Esta es una consideración importante, ya que muchos de ustedes habrán notado, esta capa extra tiene efectos sobre el rendimiento de las aplicaciones construidas con IONIC, e incluso algunos de ustedes habrán oído críticas de que las aplicaciones híbridas son “lentas” y proporcionan una mala experiencia al usuario. Sobre este punto hay que señalar que en efecto, la capa extra necesaria para ejecutar las aplicaciones híbridas provoca una sobrecarga que puede ir en detrimento de la performance. Es algo a tener en cuenta sin dudas, pero esto por sí solo no determina que las aplicaciones híbridas sean lentas o que el usuario tenga una mala experiencia con ellas. Veremos cuáles son las prácticas a seguir para que la diferencia en performance entre nuestras aplicaciones híbridas y las aplicaciones nativas, sea prácticamente inexistente. En definitiva, IONIC es un framework maravilloso que nos permite introducirnos de lleno en el mundo del desarrollo de aplicaciones para móviles, que se verán y comportarán estupendamente. Vamos a por ello.

Configurando el entorno de desarrollo En todo libro como éste, la configuración del entorno de desarrollo es una de esas partes que desearíamos obviar, pero no podemos hacerlo. Vamos a ver cómo instalar las herramientas necesarias para comenzar a construir aplicaciones de inmediato. Una cosa que debe tenerse en cuenta, sin embargo, es que existen lógicamente multitud de variantes posibles en cuanto a sistemas operativos, plataformas, etc.; y por lo tanto no es posible contemplar aquí todos los casos. Lo bueno es que las herramientas que necesitamos no son demasiadas y para cada problema que se pueda presentar con su instalación la solución está a una búsqueda en Google de distancia. Comencemos pues.

Capítulo 1: ¿Qué es IONIC?

3

Navegador web Aunque en principio podría utilizarse cualquiera, recomiendo utilizar Google Chrome. Créanme, me lo van a agradecer cuando echemos mano a sus herramientas de depuración. Editor de texto Aunque en principio podríamos trabajar con cualquier editor de texto, es conveniente contar con algún editor de texto inteligente, o incluso aún mejor una IDE para que el trabajo sea más provechoso. Particularmente, a mí me gusta mucho Sublime Text¹. Es un muy buen editor, con funcionalidades tales como el resaltado de texto, y multitud de plugins disponibles para agregar aún más poder si hiciera falta. Si trabajan con la versión gratuita, de vez en cuando tendrán que lidiar con mensajes que les sugieren comprar el producto, pero esta molestia se ve compensada con creces por la versatilidad del producto. Otra herramienta muy buena es Visual Studio Code², un editor de código muy potente que incluye: opciones de depuración, integración con Git, entre muchas otras. Cualquiera de estas dos opciones servirán muy bien a nuestros propósitos. Apache y MySQL Si bien nos ocuparemos del desarrollo móvil, lógicamente nuestras aplicaciones consumirán datos. Estos datos habitualmente provendrán de algún servicio web. Para que podamos ver cómo se efectúan las peticiones a servicios web, construiremos los nuestros. No se preocupen, no nos enfrascaremos en demasiados detalles. Lo importante es que tengamos una fuente a partir de la cual podamos consumir datos. Una manera sencilla de configurar Apache y MySQL sin liarnos demasiado, es utilizar por ejemplo Xampp, una aplicación disponible para las principales plataformas y que nos permitirá tener todo funcionando de manera sencilla. En los apéndices correspondientes, se muestra como instalar Xampp en Windows y Linux. Shell Este paso no es estrictamente necesario, pero lo incluyo porque puede ser de utilidad. Si están desarrollando con Windows, puede ser útil contar con una terminal de comandos poderosa que puede aumentar nuestra productividad. En dicho caso, recomiendo Cygwin, para darle un sabor Linux al entorno Windows. Android SKD Para poder desplegar nuestras apps en Android (lo que implica generar el apk, firmarla y alinearla antes de poder publicarla), debemos contar con el Android SDK. Para nuestros propósitos, no es necesario instalar todo el Android Studio (el IDE de desarrollo) y podemos simplemente instalar el SKD. Podemos encontrar ambas opciones aquí³. ¹https://www.sublimetext.com/3 ²https://code.visualstudio.com/ ³https://developer.android.com/studio/index.html

4

Capítulo 1: ¿Qué es IONIC?

Node JS Para crear proyectos en IONIC, necesita contar con una versión actualizada de CLI (command line interface) y Cordova, para lo cual previamente deberán tener instalado Node.js. Es conveniente que instalen la última versión, la cual pueden encontrar aquí⁴. Para comprobar qué versión de node tienen instalada, pueden ejecutar el comando node –v desde la línea de comandos:

Versión de NodeJS

Esas son todas las herramientas que necesitamos para trabajar. Todas ellas vienen en la forma de un sencillo instalador, por lo que no deberíamos tener problemas para ponerlas en marcha. He incluido apéndices para aquellos casos que pudieran resultar más complicados. En caso de que encuentren alguna dificultad, no desesperen. Configurar el entorno de desarrollo es la parte más tediosa de todo el proceso. Lo bueno es que las dificultades con las que puedan encontrarse, seguramente ya se le han presentado a alguien más, y una búsqueda en Google puede resultar de suma utilidad. También existen sitios como Stackoverflow donde mucha gente está dispuesta a echar una mano. Recuerden siempre ser concisos y amables en sus preguntas, y les irá bien. Asimismo, si alguna respuesta les ha sido de utilidad, no olviden agradecer y reconocer la respuesta como válida. La persona que les haya respondido lo valorará mucho y estará más dispuesta a echar una mano en la próxima oportunidad.

Sobre el formato del libro En este libro encontrarán todo el código necesario para construir las aplicaciones que desarrollaremos. El código está copiado directamente desde mi IDE para asegurarme de que es totalmente funcional. Sin embargo y como podrán notar, debido a las restricciones de formato, puede que no resulte muy legible tal y como aparece aquí. Puede utilizar algún formateador de código en línea para embellecerlo. ⁴https://nodejs.org/es/

5

Capítulo 1: ¿Qué es IONIC?

En cualquier caso, para cada fragmento de código relevante que aparece en libro, incluyo el correspondiente Gist. Para los que no están al tanto, un Gist es un fragmento de código compartido a través de la plataforma github. Dicho código está correctamente formateado y resaltado, por lo que será mucho más agradable de ver. Esto es todo como introducción. Gracias por su paciencia, y ahora sí vamos a ensuciarnos las manos.

Primera aplicación con ionic Una vez que tenemos todo el entorno de desarrollo configurado, debemos comenzar por instalar la CLI de ionic y cordova. Para ello empleamos el comando: npm install -g ionic cordova Nótese que estamos empleando la bandera –g. Esto indica que deseamos instalar la utilidad de forma global, con lo que automáticamente estará disponible para todos nuestros proyectos. Hecho esto, podemos dirigirnos al directorio en el que deseamos crear nuestra aplicación. Por ejemplo, podemos tener un directorio MisApps, dentro del cual pondremos todas las aplicaciones de prueba que desarrollemos. Ubicados dentro de este directorio, ejecutamos el comando: ionic start ionicTestOne –v2 Con esto, habremos creado una aplicación denominada ionicTestOne. El modificador –v2 se utiliza para indicar que deseamos trabajar con la versión 2 de ionic.

Primer Proyecto

Una vez que el proceso ha finalizado, ingresamos al directorio recientemente creado, y desde allí ejecutamos: ionic serve

6

Capítulo 1: ¿Qué es IONIC?

Lo que estamos haciendo con esto, es ejecutar el servidor embebido que tiene ionic para probar nuestra aplicación en el navegador. Para detener el servidor debemos presionar la letra q. NOTA: puede ser que al momento de ejecutar ionic serve obtengamos una salida similar a esta en el navegador:

Error el ejecutar ionic serve

¿Por qué ocurre esto? Para comprenderlo abramos nuestro proyecto en nuestro editor de texto. La estructura es la siguiente:

Estructura del proyecto

En la raíz, vemos un archivo denominado package.json, que define cosas tales como los scripts a ejecutar cuando lanzamos un comando, dependencias, etc. El contenido de este paquete variará de acuerdo a la forma en que nuestra aplicación fue creada originalmente. Ionic ofrece un número de plantillas que sirven como punto de partida para nuestros proyectos. Cuando nosotros ejecutamos al comienzo ionic start ionicTestOne –v2, se creó un proyecto con una plantilla por defecto que nos proporciona una aplicación con 3 tabs o pestañas. Al momento de escribir este libro, el contenido del archivo package.json es el siguiente:

Capítulo 1: ¿Qué es IONIC?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

{ "name": "ionic-hello-world", "author": "Ionic Framework", "homepage": "http://ionicframework.com/", "private": true, "scripts": { "clean": "ionic-app-scripts clean", "build": "ionic-app-scripts build", "ionic:build": "ionic-app-scripts build", "ionic:serve": "ionic-app-scripts serve" }, "dependencies": { "@angular/common": "2.2.1", "@angular/compiler": "2.2.1", "@angular/compiler-cli": "2.2.1", "@angular/core": "2.2.1", "@angular/forms": "2.2.1", "@angular/http": "2.2.1", "@angular/platform-browser": "2.2.1", "@angular/platform-browser-dynamic": "2.2.1", "@angular/platform-server": "2.2.1", "@ionic/storage": "1.1.7", "ionic-angular": "2.0.0-rc.4", "ionic-native": "2.2.11", "ionicons": "3.0.0", "rxjs": "5.0.0-beta.12", "zone.js": "0.6.26" }, "devDependencies": { "@ionic/app-scripts": "0.0.47", "typescript": "2.0.9" }, "description": "ionicTestOne: An Ionic project", "cordovaPlugins": [ "cordova-plugin-device", "cordova-plugin-console", "cordova-plugin-whitelist", "cordova-plugin-splashscreen", "cordova-plugin-statusbar", "ionic-plugin-keyboard" ], "cordovaPlatforms": []

7

Capítulo 1: ¿Qué es IONIC?

43

}

El problema se encuentra en la sección “scripts”. Debemos reemplazar 1 2 3 4 5 6

"scripts": { "clean": "ionic-app-scripts clean", "build": "ionic-app-scripts build", "ionic:build": "ionic-app-scripts build", "ionic:serve": "ionic-app-scripts serve" },

Por Del libro: 1 2 3 4 5 6 7 8 9

"scripts": { "build": "ionic-app-scripts build", "watch": "ionic-app-scripts watch", "serve:before": "watch", "emulate:before": "build", "deploy:before": "build", "build:before": "build", "run:before": "build" },

Gist⁵ Por ahora no se preocupen por la razón y simplemente hagan el cambio. Ahora, si ejecutan ionic serve, podrán finalmente ver la aplicación funcionando en el navegador: ⁵https://gist.github.com/vihugarcia/3cbcd246475b2ba03289b7403ed02ecd

8

9

Capítulo 1: ¿Qué es IONIC?

Primera Aplicación

Vemos que se trata de una muy sencilla aplicación con tres pestañas visibles en la parte inferior. La apariencia que muestra se debe a que estamos ejecutándola en el navegador. Sin embargo, si estamos utilizando Chrome podemos utilizar las herramientas que nos proporciona para obtener una vista similar a la de un móvil.

Consola de desarrollador

10

Capítulo 1: ¿Qué es IONIC?

Desde esta vista, tenemos disponibles herramientas de depuración que nos serán extremadamente útiles. Analicemos brevemente algunas de las pestañas de la consola de desarrollador. En la pestaña Elements podemos visualizar los elementos del DOM (Document Object Model) que conforman la página que estamos visualizando en ese momento. En la porción inferior, podemos observar además que tenemos acceso a los estilos que están aplicados a los elementos que tenemos seleccionados.

Pestaña Elements

Por ejemplo, si seleccionamos en la pestaña el tag html ion-app, podemos ver como en la parte inferior se muestran los estilos aplicados a esa sección del documento. La pestaña Console será probablemente la que más visitemos. Allí podremos ver los errores y/o

11

Capítulo 1: ¿Qué es IONIC?

advertencias relacionadas a los scripts que se están ejecutando. Será nuestra principal fuente de información para solucionar errores. Si la seleccionamos en este momento, veremos que nos muestra dos advertencias.

Pestaña Console

Estas advertencias simplemente se producen porque estamos corriendo la aplicación en un navegador en lugar de un dispositivo o simulador, por lo que no tenemos acceso a ciertas propiedades y funciones nativas. No es algo que deba preocuparnos por ahora. En la pestaña Network se muestran todas las peticiones que se realizan a recursos tales como imágenes, datos de servicios web y otros. También la veremos frecuentemente ya que toda aplicación tiene la necesidad de consumir datos. La comunicación es en ambos sentidos. Por un lado por ejemplo podemos obtener una serie de posts a través de un servicio web, y por otro podemos enviar comentarios sobre dicho post.

Pestaña Network

Por ahora es suficiente. Ahora comencemos a analizar la estructura de nuestra aplicación.

12

Capítulo 1: ¿Qué es IONIC?

Estructura de una aplicación en IONIC Nuestro trabajo, lo haremos principalmente en el directorio src. Este contiene el código que da vida a nuestra aplicación.

Directorio src

Dentro de este directorio, encontramos los siguientes: App: en este directorio encontramos los archivos necesarios para inicializar nuestra aplicación. Iremos viendo cada uno de ellos a medida que avancemos. Assets: aquí se guardan recursos estáticos tales como imágenes. Pages: en este directorio se encuentran cada una de las páginas que componen nuestra aplicación. Recordemos que una aplicación de IONIC está compuesta por un conjunto de páginas. Cada una de estas páginas vive en su propio directorio, lo cual contribuye a mantener la estructura ordenada y manejable. Theme: en este directorio, se encuentra por defecto un único archivo denominado variables.scss. Vale la pena analizarlo con mayor detenimiento. ¿Qué es un archivo scss? Seguramente, en sus proyectos habrán trabajado con hojas de estilo, archivos con extensión css que

Capítulo 1: ¿Qué es IONIC?

13

permiten definir la apariencia de secciones de una página o incluso del sitio completo. Por ejemplo, una entrada típica de css puede lucir así: 1 2 3

#header { background-color: #387ef5; }

Ahora bien, ¿qué ocurre si el color #387ef5 es usado en distintos lugares de nuestra aplicación? Entonces tenemos que repetirlo tantas veces como sea necesario. ¿Y qué ocurre si en el futuro deseamos reemplazarlo por otro? Entonces deberemos buscar cada ocurrencia en nuestro proyecto y reemplazarlo. Una solución sería poder guardar dicho valor en una variable, algo así como: 1

$primary = #387ef5;

Lamentablemente, css no permite esto. La solución es emplear una extensión de css denominada Sass (Syntactically Awesome Style Sheet), a la que podemos definir como un lenguaje de hojas de estilo. Lo que debemos saber, es que un archivo sass (identificado por su extensión scss), es traducido a css. Sass nos proporciona una serie de ventajas sobre css que lo convierten en una herramienta extremadamente útil. Entre ellas, se encuentra la posibilidad de poder definir variables. Por ejemplo, si observamos el archivo variables.scss, podemos ver la siguiente sección: 1 2 3 4 5 6 7

$colors: ( primary: secondary: danger: light: dark: );

#387ef5, #32db64, #f53d3d, #f4f4f4, #222

Lo que se está haciendo aquí, es definir un mapa de colores denominado $colors, dentro del cual se encuentran una serie de colores definidos por nombre de manera que podamos reutilizarlos a lo largo de nuestra aplicación. Podemos cambiar los códigos de color, y agregar o remover colores. El único color que es obligatorio definir en el mapa $colors es primary. Pronto veremos todo esto en acción. Ahora vamos a centrarnos en las páginas que componen nuestra aplicación. Si observamos el directorio pages, veremos que dentro de él se encuentran 4 directorios, que se corresponden a las

Capítulo 1: ¿Qué es IONIC?

14

páginas de nuestra aplicación: home, about y contact; el otro directorio, tabs, contiene la página que se encarga de mostrar las 3 pestañas anteriores. Veamos dentro del directorio home. Podemos observar que contiene 3 archivos: home.html, home.scss y home.ts. El archivo home.html, es el que contiene el código html que conforma la página. Si lo abrimos, podemos ver que el código es el siguiente: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Home

Welcome to Ionic!

This starter project comes with simple tabs-based layout for apps that are going to primarily use a Tabbed UI.

Take a look at the src/pages/ directory to add or change tabs, update any existing page or create new pages.



En él podemos reconocer etiquetas html ordinarias, tales como h2 y p, y otras que son específicas de ionic y que comienzan con el prefijo ion. Por ejemplo, en la parte superior tenemos: 1 2 3 4 5

Home

Se define un encabezado, dentro de él una barra de navegación (aquí pondríamos típicamente botones o un menú), y dentro de la barra de navegación un título. Las etiquetas que comienzan con el prefijo ion nos permiten definir elementos que son específicos de ionic, pero lo bueno es que son semánticamente compatibles con html5. Anteriormente vimos como en el archivo variables.scss se definen variables globales que determinan la presentación de

15

Capítulo 1: ¿Qué es IONIC?

nuestra aplicación en su conjunto. Veamos cómo usarlas. Reemplacemos la sección anterior por la siguiente: Del libro: 1 2 3 4 5

Home

Gist⁶ Podemos ver ahora cómo ha cambiado la apariencia de la aplicación

Barra de Navegación Home

Lo que hemos hecho es agregar la propiedad color=”primary” para la etiqueta navbar. Realicemos el mismo cambio en las páginas about.html y contact.html. La barra de navegación de todas las páginas mostrará ahora el mismo color. Vamos a aprovechar lo que hemos visto hasta ahora para personalizar un poco el estilo de nuestra aplicación. Regresamos al archivo variables.scss dentro del directorio theme y reemplacemos el mapa $colors por lo siguiente: Del libro: ⁶https://gist.github.com/vihugarcia/650c8411e8103da98b2ec0fdfaff707f

16

Capítulo 1: ¿Qué es IONIC?

1 2 3 4 5 6 7 8

$colors: ( primary: #D93240, secondary: #638CA6, danger: #F19143, light: #9E99A7, dark: #313628, favorite: #7FB069 );

Gist⁷ Podemos ver ahora que la barra de navegación de todas las páginas presenta un lindo color personalizado.

Nuevo Color de la Barra

Es importante que el estilo de nuestra aplicación se adapte al tipo de producto que estamos desarrollando y que éste difiera del diseño genérico que proporciona IONIC. Una buena imagen es de fundamental importancia para el éxito de nuestra aplicación. Sigamos modificando la página home. De regreso al archivo home.html Reemplacemos el contenido, delimitado por las etiquetas ion-content, por lo siguiente: Del libro: 1 2 3 4 5 6

¡Hola Ionic!

Esta es la página inicial de nuestro proyecto.



⁷https://gist.github.com/vihugarcia/c509daff7fa7224d85632ad011e869e0

17

Capítulo 1: ¿Qué es IONIC?

Gist⁸ Si estamos ejecutando el servidor embebido mediante ionic serve, habremos notado que los cambios se reflejan automáticamente. Nuestra página de inicio ahora se verá así:

Nueva Página de Inicio

Fantástico. Vamos progresando. Ahora centremos nuestra atención en el archivo home.ts. Los archivos con extensión ts, son archivos typescript. Typescript es un lenguaje de scripting basado en el estándar ECMA6. El estándar ECMA es el mismo en el que se basa el ultra popular lenguaje Javascript, pero a diferencia de este, Typescript agrega nuevas características tales como tipado y verdaderas clases, cosas que en Javascript sólo pueden simularse. Prácticamente toda página en un proyecto de IONIC tiene asociado un archivo ts, que posteriormente será traducido a un archivo js. El contenido del archivo home.ts es el siguiente: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

import { Component } from '@angular/core'; import { NavController } from 'ionic-angular'; @Component({ selector: 'page-home', templateUrl: 'home.html' }) export class HomePage { constructor(public navCtrl: NavController) { } }

Las primeras dos líneas, importan clases necesarias para el funcionamiento de nuestra página. ⁸https://gist.github.com/vihugarcia/3684f20d971ba3ea8f0d63d5d39d279d

Capítulo 1: ¿Qué es IONIC?

18

Es importante aquí entender que IONIC es un framework orientado a componentes, y por lo tanto, cada elemento que conforma nuestra aplicación (desde una página hasta una sección de la misma) es un componente. De hecho, determinar qué componentes conformarán nuestra aplicación es una de las tareas fundamentales del diseño. Con esto en mente, analicemos las líneas siguientes: 1 2 3 4

@Component({ selector: 'page-home', templateUrl: 'home.html' })

Con esto estamos declarando un componente, en este caso una página. Vemos que la declaración tiene un par de propiedades. selector, hace referencia la etiqueta html que identificará al componente. En este caso, estamos diciendo que cuando en nuestro código html se encuentre un par de etiquetas , dichas etiquetas contendrán el código html de nuestro componente. templateUrl contiene el nombre del archivo donde está definida la estructura de nuestro componente. Se trata del archivo home.html que ya hemos analizado y modificado. Alternativamente, el código html se podría definir sin recurrir a un archivo html, utilizando la propiedad template y poniendo el código html entre comillas simples o dobles. Esto, sin embargo, no es recomendable a menos que se trate de un contenido super sencillo. Veamos ahora las últimas líneas: 1 2 3 4 5 6 7

export class HomePage { constructor(public navCtrl: NavController) { } }

Lo que hacemos aquí es exportar la clase que contiene nuestro componente, asignándole en este caso el nombre HomePage. Esto permitirá que podamos usar el componente desde cualquier otro componente donde sea necesario, utilizando previamente una sentencia import. Veremos esto cuando analicemos el componente Tabs. Los componentes que corresponden a las páginas about y contact son prácticamente iguales. Tenemos las sentencias import, la definición del componente, y luego la exportación de la clase para que el componente pueda ser utilizado. Analicemos ahora el componente Tabs.

Capítulo 1: ¿Qué es IONIC?

19

Cuando creamos el proyecto, IONIC construyó por defecto una aplicación que tiene un layout gobernado por pestañas, es decir, la navegación entre páginas se realiza mediante pestañas. Esto quiere decir que además de los componentes correspondientes a cada una de las páginas que se muestran en pestañas, necesitamos un componente que contenga la navegación. Dicho componente es Tabs. Examinemos el archivo tabs.ts. Su contenido es el siguiente: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

import { Component } from '@angular/core'; import { HomePage } from '../home/home'; import { AboutPage } from '../about/about'; import { ContactPage } from '../contact/contact'; @Component({ templateUrl: 'tabs.html' }) export class TabsPage { // this tells the tabs component which Pages // should be each tab's root Page tab1Root: any = HomePage; tab2Root: any = AboutPage; tab3Root: any = ContactPage; constructor() { } }

Es ligeramente más complejo que lo visto hasta ahora. La primera línea, es un import de los componentes del núcleo de angular, necesario para poder trabajar con componentes: 1

import { Component } from '@angular/core';

Algo que podamos notar, es que a diferencia de las páginas vistas anteriormente, no se encuentra un import del componente NavController. Esto es así porque en esta página no se hace uso de una barra de navegación. Las tres líneas siguientes son imports de las páginas home, about y contact, que son las que pueden visualizarse en las pestañas.

Capítulo 1: ¿Qué es IONIC?

1 2 3

20

import { HomePage } from '../home/home'; import { AboutPage } from '../about/about'; import { ContactPage } from '../contact/contact';

Si estos imports no estuvieran, no podríamos usar los componentes asociados a cada una de dichas páginas. Luego viene la declaración del componente: 1 2 3

@Component({ templateUrl: 'tabs.html' })

En este caso, a diferencia de los casos anteriores, no estamos definiendo un selector, es decir una etiqueta html asociada al componente. Esto es así porque se trata del componente raíz, a partir del cual se agregarán todos los siguientes. A continuación, como siempre, se debe exportar la clase para que pueda ser utilizada: 1 2 3 4 5 6 7 8 9 10 11

export class TabsPage { // this tells the tabs component which Pages // should be each tab's root Page tab1Root: any = HomePage; tab2Root: any = AboutPage; tab3Root: any = ContactPage; constructor() { } }

Obsérvese que se definen tres variables, y a cada una de ellas se le asigna cada una de las clases que hacen referencia a las páginas que se mostrarán en las pestañas. Nótese también que después de cada variable, se tienen dos puntos y una declaración de tipo antes de la asignación. Como habíamos mencionado anteriormente, typescript es un lenguaje tipado, es decir, permite declarar los tipos asociados a cada variable de la forma nombreVariable: tipo. Esta declaración del tipo puede estar seguida o no de una asignación. También podemos observar que al igual que en las clases vistas anteriormente, se tiene un método constructor sin implementar.

21

Capítulo 1: ¿Qué es IONIC?

Típicamente allí realizaríamos todas las tareas de inicialización requeridas. Por ejemplo, dentro del constructor podríamos llamar a un método encargado de iniciar una petición a un servicio web para obtener datos necesarios. El archivo tabs.html es muy simple. 1 2 3 4 5 6





Tenemos un componente tabs donde se definen cada una de las pestañas que estarán disponibles. Vayamos ahora un poco más lejos y veamos cómo podemos agregar una nueva página y mostrarla en su correspondiente pestaña. Una de las mejores características de IONIC es su CLI (Command Line Interface), que nos permite utilizar una serie de comandos que simplifican enormemente el trabajo. Siempre desde la raíz de nuestro proyecto, ejecutemos el comando: ionic g page privacy Si todo ha ido bien, deberíamos ver un nuevo directorio denominado privacy, dentro del directorio pages.

Página privacy

Estupendo. Podemos observar que el directorio privacy contiene los tres mismo tipos de archivo con los que ya estamos familiarizados. El contenido del archivo privacy.html es el siguiente:

Capítulo 1: ¿Qué es IONIC?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

22



privacy



Tenemos un encabezado, dentro de él una barra de navegación, y dentro de esta última un título. En primer lugar, asignemos a la barra de navegación el mismo color que el de las otras páginas. La etiqueta debería quedarnos como:

Luego, reemplacemos el título por el siguiente: Política de Privacidad Podemos ver que el contenido de la página está vacío: 1 2 3



En primer lugar, agreguemos un título utilizando la etiqueta Luego, agreguemos un texto de ejemplo que podríamos utilizar para señalar la política de privacidad de nuestra aplicación. El texto fue generado a partir de una plantilla en https://politicadeprivacidadplantilla.com/⁹ Es un recurso que puede serles de utilidad para un proyecto web o móvil. En definitiva, el contenido debería quedarnos de la siguiente manera: Del libro: ⁹https://politicadeprivacidadplantilla.com/

Capítulo 1: ¿Qué es IONIC?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

Política de Privacidad

El presente Política de Privacidad establece los términos en que TestOne usa y protege la información que es proporcionada por sus usuarios al momento de utilizar su sitio web. Esta compañía está comprometida con la seguridad de los datos de sus usuarios. Cuando le pedimos llenar los campos de información personal con la cual usted pueda ser identificado, lo hacemos asegurando que sólo se empleará de acuerdo con los términos de este documento. Sin embargo esta Política de Privacidad puede cambiar con el tiempo o ser actualizada por lo que le recomendamos y enfatizamos revisar continuamente esta página para asegurarse que está de acuerdo con dichos cambios.

Información que es recogida

Nuestro sitio web podrá recoger información personal por ejemplo: Nombre, información de contacto como su dirección de correo electrónica e información demográfica. Así mismo cuando sea necesario podrá ser requerida información específica para procesar algún pedido o realizar una entrega o facturación.

Uso de la información recogida

Nuestro sitio web emplea la información con el fin de proporcionar el mejor servicio posible, particularmente para mantener un registro de usuarios, de pedidos en caso que aplique, y mejorar nuestros productos y servicios. Es posible que sean enviados correos electrónicos periódicamente a través de nuestro sitio con ofertas especiales, nuevos productos y otra información publicitaria que consideremos relevante para usted o que pueda brindarle algún beneficio, estos correos electrónicos serán enviados a la dirección que usted proporcione y podrán ser cancelados en cualquier momento.

TestOne está altamente comprometido para cumplir con el compromiso de mantener su información segura. Usamos los sistemas más avanzados y los actualizamos constantemente para asegurarnos que no exista ningún acceso no autorizado.

Enlaces a Terceros

Este sitio web pudiera contener enlaces a otros sitios que pudieran ser de su interés. Una vez que usted dé clic en estos enlaces y abandone nuestra página, ya no tenemos control sobre al sitio al que es redirigido y por lo tanto

23

Capítulo 1: ¿Qué es IONIC?

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

24

no somos responsables de los términos o privacidad ni de la protección de sus datos en esos otros sitios terceros. Dichos sitios están sujetos a sus propias políticas de privacidad por lo cual es recomendable que los consulte para confirmar que usted está de acuerdo con estas.

Control de su información personal

En cualquier momento usted puede restringir la recopilación o el uso de la información personal que es proporcionada a nuestro sitio web. Cada vez que se le solicite rellenar un formulario, como el de alta de usuario, puede marcar o desmarcar la opción de recibir información por correo electrónico. En caso de que haya marcado la opción de recibir nuestro boletín o publicidad usted puede cancelarla en cualquier momento.

Esta compañía no venderá, cederá ni distribuirá la información personal que es recopilada sin su consentimiento, salvo que sea requerido por un juez con un orden judicial.

TestOne Se reserva el derecho de cambiar los términos de la presente Política de Privacidad en cualquier momento.



Gist¹⁰ Fantástico. Ya tenemos la plantilla correspondiente a nuestra página. Sin embargo, no la hemos ubicado aún en una pestaña. Regresemos al archivo tabs.ts Al igual que lo que ocurre con las otras páginas, debemos agregar la sentencia import para la nueva página que hemos creado: import { PrivacyPage } from ‘../privacy/privacy’; Debemos también agregar una nueva variable que hará referencia a la clase: tab4Root: any = PrivacyPage; Ahora, vayamos al archivo tabs.html y agreguemos la pestaña correspondiente.

Si ahora ejecutamos ionic serve, podremos ver la nueva pestaña: ¹⁰https://gist.github.com/vihugarcia/8ae00162e2501f081b94a79192e21364

25

Capítulo 1: ¿Qué es IONIC?

Nueva Pestaña

Sin embargo, si intentamos seleccionarla, veremos que se producen errores:

26

Capítulo 1: ¿Qué es IONIC?

Errores

Aquí es donde la información que obtenemos de la consola de depuración comienza o mostrar su valor. Podemos ver que la primera entrada en la lista de errores nos señala: error_handler.js:47 EXCEPTION: Error in ./Tabs class Tabs - inline template:0:43 caused by: No component factory found for PrivacyPage Esto ocurre, porque si bien hemos creado nuestro componente de manera correcta, y hemos utilizado el import correspondiente en el archivo tabs.ts, nuestra aplicación no sabe aún cómo construir un componente PrivacyPage. Vayamos al directorio app, y dentro de él busquemos y abramos el archivo app.module.ts. Su contenido actualmente es el siguiente: 1 2 3 4 5 6 7 8 9 10 11 12

import import import import import import import

{ { { { { { {

NgModule, ErrorHandler } from '@angular/core'; IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; MyApp } from './app.component'; AboutPage } from '../pages/about/about'; ContactPage } from '../pages/contact/contact'; HomePage } from '../pages/home/home'; TabsPage } from '../pages/tabs/tabs';

@NgModule({ declarations: [ MyApp, AboutPage,

Capítulo 1: ¿Qué es IONIC?

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

27

ContactPage, HomePage, TabsPage ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp, AboutPage, ContactPage, HomePage, TabsPage ], providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}] }) export class AppModule {}

Este archivo, representa a toda nuestra aplicación como un módulo. Cada componente que es utilizado en nuestro proyecto, debe ser declarado aquí de lo contrario nuestra aplicación no se dará por enterada de que existe y obtendremos un error como el que se ha presentado. Como podemos ver, existen cuatro sentencias import para cada una de nuestras páginas: 1 2 3 4

import import import import

{ { { {

AboutPage } from '../pages/about/about'; ContactPage } from '../pages/contact/contact'; HomePage } from '../pages/home/home'; TabsPage } from '../pages/tabs/tabs';

Debemos agregar la sentencia correspondiente a nuestra nueva página. Tendremos ahora: 1 2 3 4 5

import import import import import

{ { { { {

AboutPage } from '../pages/about/about'; ContactPage } from '../pages/contact/contact'; HomePage } from '../pages/home/home'; PrivacyPage } from '../pages/privacy/privacy'; TabsPage } from '../pages/tabs/tabs';

Además, debemos incluirla en las secciones declarations y entryComponents. Para estar completamente seguros, aquí está todo el código del archivo con las modificaciones realizadas:

Capítulo 1: ¿Qué es IONIC?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

import import import import import import import import

{ { { { { { { {

28

NgModule, ErrorHandler } from '@angular/core'; IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; MyApp } from './app.component'; AboutPage } from '../pages/about/about'; ContactPage } from '../pages/contact/contact'; HomePage } from '../pages/home/home'; PrivacyPage } from '../pages/privacy/privacy'; TabsPage } from '../pages/tabs/tabs';

@NgModule({ declarations: [ MyApp, AboutPage, ContactPage, HomePage, PrivacyPage, TabsPage ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp, AboutPage, ContactPage, HomePage, PrivacyPage, TabsPage ], providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}] }) export class AppModule {}

Como habíamos mencionado anteriormente, si el servidor se está ejecutando aún, podremos ver que los cambios que realicemos se reflejarán en el navegador. Si ahora nos dirigimos a la nueva pestaña, podremos visualizarla correctamente:

29

Capítulo 1: ¿Qué es IONIC?

Página Privacidad

¡Fantástico! Realicemos algunos cambios más a nuestra página recientemente creada. Si analizamos el código, veremos que tenemos dos ocurrencias del mismo texto: Política de Privacidad. Vamos a quitar esa redundancia. En el archivo privacy.ts, agreguemos una variable de tipo String de la siguiente manera: 1

titulo: String = 'Política de Privacidad';

Ahora, en el archivo privacy.html, reemplacemos el ion-title por lo siguiente: 1

{{titulo}}

Y en el contenido, reemplacemos el h2: 1

{{titulo}}

Las llaves dobles indican que todo lo que se encuentre entre ellas debe ser evaluado como una expresión.

Capítulo 1: ¿Qué es IONIC?

30

En este caso, la variable titulo será reemplazada por su valor por lo que la salida es exactamente la misma. En muy poco tiempo, hemos aprendido mucho. Hemos creado un proyecto, hemos visto cómo se estructuran las páginas de una aplicación, como puede crearse una nueva página y cómo debe agregarse su declaración para que sea reconocida por la aplicación. En el próximo capítulo comenzaremos con un nuevo proyecto que desarrollaremos hasta obtener una aplicación totalmente funcional. Estamos ya en la senda para crear aplicaciones móviles espectaculares.

Capítulo 2: Primera Aplicación Completa Vamos a una construir una aplicación para gestionar contactos. Podremos agregar, editar y eliminar contactos. En un primer momento nuestros datos estarán en memoria, pero luego los obtendremos a partir de servicios web. Adquiriremos así los fundamentos necesarios para realizar aplicaciones complejas. Cuando comenzamos con el desarrollo de una aplicación (sea móvil o no) es muy útil contar con bosquejos de las pantallas que el usuario encontrará. Esto es muy útil en la comunicación con el cliente ya que nos permite capturar requisitos de una manera rápida y eliminar potenciales problemas antes de que se presenten. Existen muchas herramientas para realizar sketchs de pantallas. Algunas incluyen funcionalidad avanzada como por ejemplo la simulación del funcionamiento de una aplicación proporcionando la posibilidad de interactuar con las pantallas. No es necesario que incurramos en grandes costos para adquirir un software tan avanzado. Particularmente yo uso una aplicación denominada Pencil¹¹. Es una herramienta de prototipado de código abierto, totalmente gratuita, y que proporciona muy buenos resultados. Por supuesto, no es obligatorio que utilicemos un programa para realizar el diseño de pantallas, simplemente es algo recomendable. El tiempo que invirtamos en esta actividad será más que compensado por la comprensión de las necesidades del cliente que obtendremos. El programa tiene distintas colecciones de elementos de interface que podemos utilizar para diseñar las pantallas. Por ejemplo en la figura siguiente se muestra la sección correspondiente a Android: ¹¹http://pencil.evolus.vn/

32

Capítulo 2: Primera Aplicación Completa

Android

También existe una sección denominada Desktop – Sketchy UI, cuyos tienen una apariencia de dibujo a mano alzada, lo que resalta el concepto de que se trata de un diseño preliminar y esto útil en el trato con los clientes. Sea cual sea el grupo de componentes que se utilice, siempre debemos señalar a nuestros clientes que el diseño presentado es simplemente un bosquejo a fin de poder plasmar y discutir ideas. El primer diseño de pantalla es el siguiente:

33

Capítulo 2: Primera Aplicación Completa

Pantalla Inicial

Muy bien. Con este sencillo diseño como guía, podemos comenzar a trabajar en nuestra aplicación.

Creación del Proyecto Vamos a crear un nuevo proyecto, pero esta vez el comando que utilizaremos será: ionic start contactMgrApp blank –v2 La diferencia con el proyecto anterior es que al utilizar el modificador blank, estamos indicando que en este caso deseamos una aplicación basada en una plantilla básica, que contendrá una única página. A partir de este lienzo en blanco desarrollaremos la aplicación. Como en el proyecto anterior, debemos reemplazar la sección scripts del archivo package.json por lo siguiente:

34

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9

"scripts": { "build": "ionic-app-scripts build", "watch": "ionic-app-scripts watch", "serve:before": "watch", "emulate:before": "build", "deploy:before": "build", "build:before": "build", "run:before": "build" },

Al ejecutar ionic serve debemos ver la siguiente pantalla:

Primera pantalla

Tenemos una única página con un poco de texto. Abramos el proyecto con nuestro editor para ver la estructura. Podemos comprobar que dentro del directorio pages existe una única carpeta denominada home. Dejémosla tal como está.

Creando una nueva página Vamos a crear una nueva página ejecutando: ionic g page contactos Obtendremos un nuevo directorio denominado contactos. El contenido del archivo contactos.ts es el siguiente:

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

35

import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; /* Generated class for the Contactos page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contactos', templateUrl: 'contactos.html' }) export class ContactosPage { constructor(public navCtrl: NavController, public navParams: NavParams) {} ionViewDidLoad() { console.log('ionViewDidLoad ContactosPage'); } }

Como primera medida, agregaremos una variable para contener el título que mostraremos en la página: Luego de export class ContactosPage { agregamos: 1

titulo: String = 'Administrador de Contactos';

El contenido de contactos.html es el siguiente: 1 2 3 4 5 6 7 8 9 10



contactos

Capítulo 2: Primera Aplicación Completa

11 12 13 14 15 16 17 18

36





Comenzaremos por utilizar la variable que hemos creado en el ion-title y agregar una etiqueta h2 en el contenido de la página. Ahora tendremos: Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18



{{titulo}}



{{titulo}}

Gist¹² Perfecto. Sin embargo, tal como está ahora la aplicación, no tenemos modo de ver la página creada. Vamos a modificar el proyecto de modo que la página que se cargue al comienzo sea la nueva página en lugar de la página que viene por defecto. ¹²https://gist.github.com/vihugarcia/2882bb440b1bbd6cccaad24cffee4751

Capítulo 2: Primera Aplicación Completa

37

Seteando la página raíz Si examinamos el archivo app.component.ts dentro del directorio app, encontraremos la siguiente línea: rootPage = HomePage; Esta es la línea que indica que la página Home es la que debe tomarse como raíz. Antes de modificar esta línea, sin embargo, debemos agregar la sentencia import para poder usar la nueva página: import { ContactosPage } from ‘../pages/contactos/contactos’; Ahora podemos indicar la nueva raíz: rootPage = ContactosPage; Sin embargo, si ahora ejecutamos ionic serve obtendremos un error. Si recordamos del capítulo anterior, esto sucede porque debemos informarle a la aplicación sobre la nueva página creada. Esto lo hacemos editando el archivo app.module.ts 1

import { ContactosPage } from '../pages/contactos/contactos';

Además, debemos agregar la página en las secciones declarations y entryComponents. El contenido completo del archivo quedará de la siguiente manera. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

import import import import import

{ { { { {

NgModule, ErrorHandler } from '@angular/core'; IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; MyApp } from './app.component'; HomePage } from '../pages/home/home'; ContactosPage } from '../pages/contactos/contactos';

@NgModule({ declarations: [ MyApp, HomePage, ContactosPage ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp,

38

Capítulo 2: Primera Aplicación Completa

19 20 21 22 23 24

HomePage, ContactosPage ], providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}] }) export class AppModule {}

Gist¹³ Ahora lancemos ionic serve.

Nueva pantalla inicio

Nada extraordinario, pero seguimos avanzando.

Creando un modelo de datos Sabemos que nuestra aplicación mostrará un listado de contactos. Es decir, tenemos una entidad fundamental denominada contacto. Esta entidad tendrá atributos tales como nombre y dirección, y algún otro que sirva como identificador. En nuestra aplicación, tendremos que modelar todas las entidades que la conformen. Es decir, por cada entidad que componga nuestra aplicación será conveniente construir un modelo de datos. No se preocupen, suena más complicado de lo que en realidad es. Creemos al mismo nivel que el directorio pages una carpeta denominada model. No es obligatorio que agrupemos todos nuestros modelos en un único directorio, pero es una práctica recomendable ya que nos permite mantener nuestra aplicación ordenada y mantenible. Dentro de la carpeta model creemos un archivo denominado contacto.ts. Agreguemos a dicho archivo el siguiente contenido. Del libro: ¹³https://gist.github.com/vihugarcia/e1d7aad010c37e09ec0be903891e7d7a

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7

39

export class Contacto{ constructor( public id: number, public nombre: string, public direccion: string ){} }

Gist¹⁴ Estamos definiendo una clase denominada Contacto con tres propiedades o atributos: id, nombre, y dirección. Nótese que cada una de estas propiedades está precedida del modificador public. Esto señala que los valores de dichas propiedades pueden ser accedidos desde afuera de la clase. Si no se indica el nivel de visibilidad de una propiedad, se considera que es public. Es decir: public id: number es equivalente a id: number. Habiendo definido la clase Contacto, podemos hacer ya uso de ella. En el archivo contactos.ts agreguemos el siguiente import: 1

import { Contacto } from '../../model/contacto';

Ahora definamos una variable que contenga algunas instancias de la clase definida: 1 2 3 4 5

public contactos = [ new Contacto(1, "Andrea Gómez", "Calle Uno 123"), new Contacto(2, "Juan Perez", "Calle Dos 567"), new Contacto(3, "Martín Álvarez", "Calle del Pueblo 628") ];

Lo que estamos haciendo, es definir un arreglo que contiene objetos del tipo Contacto. En el momento de crear los objetos (con la sentencia new) le pasamos los parámetros que espera el constructor de la clase Contacto. Debemos ahora modificar el archivo contactos.html para mostrar los datos de los contactos que hemos creado.

Listas y estructuras repetitivas Podemos hacerlo utilizando una lista de ítems. Para ello ionic nos brinda un componente denominado ion-list. Dentro de cada ion-list existirán ítems. Cada ítem corresponderá a un contacto. Luego de la etiqueta h2 agreguemos lo siguiente. Del libro: ¹⁴https://gist.github.com/vihugarcia/af8bf72b86aaa1dbd1d03261bdd096c4

40

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6

{{contacto.nombre}} {{contacto.direccion}}

Gist¹⁵ La lista está comprendida entre las etiquetas y Cada ítem queda determinado por el par de etiquetas y Dentro de ion-item, tenemos la siguiente sentencia: *ngFor=”let contacto of contactos” La directiva *ngFor nos permite utilizar un ciclo for para repetir un elemento. En ese caso, como lo que queremos es repetir tantos ítems como contactos, agregamos la sentencia en la etiqueta de apertura Dentro del *ngFor, estamos definiendo una variable ** contacto **. La variable contacto tomará un valor distinto en cada iteración del ciclo. Es decir, por cada elemento del arreglo contactos, la variable ** contacto ** tomará dicho valor. Cada uno de dichos elementos es en este caso un objeto. Ejecutemos ahora ionic serve. La salida que obtenemos es la siguiente:

Lista de Contactos

Estamos más cerca. Tenemos nuestra lista de contactos. Lógicamente, una aplicación de contactos real necesitará manejar más datos que el nombre y la dirección. Podemos agregar tantos como sea necesario en la definición de nuestra clase Contacto. Por ejemplo, agreguemos teléfono y dirección, ambos como tipo String. La nueva definición de la clase quedará de la siguiente manera. Del libro: ¹⁵https://gist.github.com/vihugarcia/f847ea364b2eba7d56097fcdeedbc6f3

41

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9

export class Contacto{ constructor( public public public public public ){} }

id: number, nombre: string, direccion: string, telefono: string, email: string

Gist¹⁶ Sin embargo, ahora nos encontraremos con que la línea de comandos nos muestra mensajes de error:

Error de número de parámetros

Esto ocurre porque ahora el constructor de la clase espera cinco parámetros, pero en el momento de crear los objetos en el archivo contactos.ts, estamos proporcionando tres. Corrijamos.

¹⁶https://gist.github.com/vihugarcia/f572f4db6387a8d9f4b6d3cdc71ea1a8

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6

42

public contactos = [ new Contacto(1, "Andrea Gómez", "Calle Uno 123", "12345", "[email protected]"), new Contacto(2, "Juan Perez", "Calle Dos 567", "23456", "[email protected]"), new Contacto(3, "Martín Álvarez", "Calle del Pueblo 628", "34567", "[email protected]") ];

Los errores han desaparecido y ahora nuestra aplicación funciona nuevamente. En la pantalla principal, se siguen mostrando como únicos datos el nombre y dirección. Lo que haremos ahora es agregar la opción para permitir ver los detalles de un contacto seleccionado. Una forma elegante de agregar opciones para trabajar con cada uno de los ítems, muy utilizada en las apps, es la de presentar una serie de botones al deslizar el ítem ya sea a izquierda o derecha. Para ello, modifiquemos la ion-list de la siguiente manera. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15



{{contacto.nombre}} {{contacto.direccion}}



Ver



Gist¹⁷ Las diferencias son las siguientes: Se utiliza un ion-item-sliding como grupo repetitivo en lugar de ion-item. Este es el elemento que nos permitirá agregar un conjunto de botones para cada ítem. Luego tenemos el ion-list donde se muestran los datos del contacto.

¹⁷https://gist.github.com/vihugarcia/5ceeee3868c76b4cf6513689095e9f1e

43

Capítulo 2: Primera Aplicación Completa

1 2 3 4

{{contacto.nombre}} {{contacto.direccion}}

A continuación, añadimos un elemento ion-item-options. La propiedad side determina la alineación de los botones. En este caso los botones aparecerán a la derecha cuando se realice un deslizamiento de derecha a izquierda. 1 2 3 4 5 6



Ver

En este caso se mostrará un solo botón. Al hacer clic en este botón se convocará a un método verContacto que recibe como parámetro un objeto del tipo Contacto. Escribamos dicho método. Por ahora estará vacío. 1 2 3

verContacto(contacto: Contacto) { }

El resultado es el siguiente:

Botón ver

Vamos a crear ahora la página que mostrará los detalles del contacto seleccionado. El comando para crear una nueva página ya debe resultarnos familiar.

44

Capítulo 2: Primera Aplicación Completa

ionic g page contacto Concentrémonos ahora en el archivo contacto.html. Modifiquémoslo de la siguiente manera. Del libro: 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



Contacto





{{contacto.nombre}}

{{contacto.direccion}}

{{contacto.telefono}}

{{contacto.email}}





Gist¹⁸ Muy bien. Hay unas cuantas cosas interesantes en el fragmento de código anterior. Por una lado, tenemos la siguiente etiqueta: Estamos usando aquí una estructura de control. Un if, que en IONIC representamos con *ngIf. Esta estructura de control, evalúa una condición y devuelve verdadero o falso. Al agregarla al ¹⁸https://gist.github.com/vihugarcia/d0a4ab385ca285a9d8cea16ee19a7ddf

Capítulo 2: Primera Aplicación Completa

45

elemento div, estamos indicando que deseamos visualizar dicho elemento sólo cuando la condición sea verdadera. En este caso, cuando exista un objeto contacto a mostrar. Luego introducimos un nuevo elemento de IONICS. Una ion-card. Este elemento, permite presentar contenido en un bloque que tiene una bonita presentación y cuya apariencia podemos configurar. Una ion-card se compone de un ion-card-header y de un ion-card-content. Un encabezado y un cuerpo respectivamente. Cada vez que tengamos dudas sobre el uso de un componente, o querramos ampliar nuestros conocimientos, podemos recurrir a la muy buena documentación de IONIC disponible en https://ionicframework.com/docs/v2/components/¹⁹ Existen multitud de ejemplos y además tenemos se nos presentan simulaciones de aplicaciones donde podemos ver los ejemplos ejecutándose en vivo. No podría ser mejor. Ahora que tenemos la página de un contacto individual creada, regresemos a contactos.ts y completemos el método verContacto. 1 2 3

verContacto(contacto: Contacto) { this.navCtrl.push(ContactoPage, {contacto}); }

Analicemos lo que ocurre aquí. En toda aplicación móvil, las páginas se muestran en un stack (pila). Una nueva página puede apilarse encima de las páginas existentes, utilizando una operación push. Para ello, invocamos al método push de un objeto del tipo NavController. Como habremos notado, cuando creamos una página mediante la línea de comandos, ionic se encarga de pasar como parámetro al método constructor de la página un objeto navCtrl del tipo NavController. El método push tiene la forma: push(página, {parámetros}) En este caso, estamos indicando que debe apilarse la página ContactoPage, y que esta recibirá como parámetro un objeto de tipo Contacto. Para que esto funcione, debemos agregar el import de la página contacto en la parte superior. Para estar seguros, aquí está el código completo de contactos.ts Del libro:

¹⁹https://ionicframework.com/docs/v2/components/

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

import import import import

{ { { {

46

Component } from '@angular/core'; NavController, NavParams } from 'ionic-angular'; Contacto } from '../../model/contacto'; ContactoPage } from '../contacto/contacto';

/* Generated class for the Contactos page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contactos', templateUrl: 'contactos.html' }) export class ContactosPage { titulo: String = 'Administrador de Contactos'; public contactos = [ new Contacto(1, "Andrea Gómez", "Calle Uno 123", "12345", "[email protected]"), new Contacto(2, "Juan Perez", "Calle Dos 567", "23456", "[email protected]"), new Contacto(3, "Martín Álvarez", "Calle del Pueblo 628", "34567", "[email protected]") ]; constructor(public navCtrl: NavController, public navParams: NavParams) {} ionViewDidLoad() { console.log('ionViewDidLoad ContactosPage'); } verContacto(contacto: Contacto) { this.navCtrl.push(ContactoPage, {contacto}); } }

Gist²⁰ El parámetro pasado a la página contacto, tiene el nombre contacto, y recibe el objeto contacto que está en la declaración del método verContacto. Cuando el parámetro tiene el mismo nombre que el objeto que recibe, se puede utilizar una notación abreviada como la de arriba. Equivalente a lo anterior sería: ²⁰https://gist.github.com/vihugarcia/2aa608d26ad04d931039daf21ee58e9c

Capítulo 2: Primera Aplicación Completa

this.navCtrl.push(ContactoPage, {contacto: contacto}); Ahora, el parámetro enviado debe recibirse. Aquí está el código completo de contacto.ts. Del libro: 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

import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; import { Contacto } from '../../model/contacto'; /* Generated class for the Contacto page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contacto', templateUrl: 'contacto.html' }) export class ContactoPage { public contacto: Contacto; constructor(public navCtrl: NavController, public navParams: NavParams) { this.contacto = this.navParams.get('contacto'); } ionViewDidLoad() { console.log('ionViewDidLoad ContactoPage'); } }

Gist²¹ Estupendo. Ejecutemos la aplicación para ver los resultados: ²¹https://gist.github.com/vihugarcia/fb76df64154d601804af89c24f20bce9

47

48

Capítulo 2: Primera Aplicación Completa

Detalle de Contacto

Nada mal. Vamos a agregar ahora botones con opciones para editar y eliminar un contacto. Para ello, deberemos modificar el archivo contactos.html de la siguiente manera. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12

Ver

Editar

Eliminar

Gist²² Se han resaltado en negrita las partes a agregar. Debemos agregar los dos métodos que se invocan en contactos.ts. Por ahora dejaremos en suspenso su implementación. Simplemente escribiremos:

²²https://gist.github.com/vihugarcia/239061523f3b3f2d3c12a84aa97ef8e5

49

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7

editarContacto(contacto: Contacto) { } eliminarContacto(contacto: Contacto) { }

Con los cambios realizados, podremos ver que ahora al deslizar la pantalla hacia le derecha sobre un contacto, visualizamos tres botones.

Nuevos botones

En este punto, y antes de avanzar con el resto de la funcionalidad de la aplicación, vamos a tomarnos un breve tiempo para mejorar la apariencia de la misma. Vayamos al archivo variables.scss dentro del directorio theme y reemplacemos el mapa $colors por lo siguiente. Del libro: 1 2 3 4 5 6 7 8

$colors: ( primary: secondary: favorite: danger: light: dark: );

#638CA6, #32db64, #17A697, #D93240, #BFD4D9, #F2671F

Gist²³ ²³https://gist.github.com/vihugarcia/a3be82bd10e372c3da12eae4934761ff

50

Capítulo 2: Primera Aplicación Completa

Ahora indiquemos tanto en contactos.html como en contacto.html que nuestra barra de navegación debe usar el color primary.

La apariencia de nuestra aplicación debería ser ahora la siguiente:

Nuevos colores

Creo que se ve mejor. Siéntanse en libertad de usar cualquier combinación de colores que sea de su agrado. Algo que estamos echando en falta, es la posibilidad de agregar un nuevo contacto. Vamos a comenzar a resolver el problema. Para ello, añadamos en contactos.html luego de la etiqueta de cierre lo siguiente: 1 2 3 4 5





Debemos añadir también el método correspondiente en contactos.ts: 1 2 3

mostrarAgregarContacto() { }

Podemos ver que ahora nuestra pantalla principal presenta un botón en la esquina inferior derecha.

51

Capítulo 2: Primera Aplicación Completa

Botón agregar contacto

Desde luego, el botón no hace nada aún. Mejor dicho, convoca a un método que todavía no realiza ninguna función. Vamos a resolver eso.

Crear una ventana modal Vamos a crear ahora una nueva página ionic g page add-contacto-modal Esta página, que mostraremos de forma modal, contendrá un formulario que nos permitirá agregar un nuevo contacto.

Capítulo 2: Primera Aplicación Completa

52

Editemos el archivo add-contacto-modal.html y agreguemos lo siguiente dentro del elemento ioncontent. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38





El nombre es obligatorio



La dirección es obligatoria



El teléfono es obligatorio



Enviar

Capítulo 2: Primera Aplicación Completa

39

53

Gist²⁴ Varias cosas suceden aquí. En primer lugar, estamos añadiendo un formulario con las etiquetas y . Esto es exactamente igual a cuando creamos un formulario en una página web común y corriente. #formContacto=”ngForm” agrega un identificador a nuestro formulario, y además le indica a IONIC que se trata de un ngForm, lo que le agrega al formulario una serie de capacidades muy interesantes. (ngSubmit)=”onSubmit()” intercepta el evento submit del formulario (representado por ngSubmit) y le asigna un método a ser ejecutado, en este caso onSubmit. Luego tenemos varios inputs que están contenidos dentro de respectivos ion-item. Como ven, un ion-item no tiene que estar forzosamente asociado a un ion-list. Un cuadro de texto en IONIC se corresponde con el elemento ion-input. Este tiene las mismas características que un input ordinario, pero está específicamente pensado para aplicaciones móviles por lo que debemos utilizarlo. Si analizamos el primer ion-input podemos ver las propiedades required=”required” placeholder=”Nombre” required es un atributo html5 que indica que el ingreso de datos es obligatorio. Un placeholder es un texto que se muestra dentro del cuadro de texto para indicar al usuario qué dato debe ingresar. Este texto desaparece en cuanto el usuario comienza a escribir y reaparece si el usuario borra el texto. Analicemos ahora lo siguiente: #nombre=”ngModel” [(ngModel)]=”contacto.nombre” 1

#nombre="ngModel"

Asigna un identificador al cuadro de texto, y le indica a IONIC que se trata de un campo correspondiente a un modelo de datos. [(ngModel)]=”contacto.nombre” es la forma de indicar un databinding. ¿Qué es eso de data binding? Vamos a ver un ejemplo ahora mismo antes de continuar avanzando. En el archivo contactos.html, justo debajo de la etiqueta h2, agregue lo siguiente:

²⁴https://gist.github.com/vihugarcia/2081d84a3c97f3862024e43f3a4a49aa

54

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6

{{texto}}





Verán la siguiente pantalla:

Data binding

Si ahora comienzan a escribir en el cuadro de texto que ha agregado, verán que el texto aparece arriba del mismo.

55

Capítulo 2: Primera Aplicación Completa

Data binding

Esto se debe a que estamos utilizando databinding. Cuando escribimos [(ngModel)]=”texto” enlazamos el texto que contiene el ion-input con el valor de una variable texto. Justo arriba del cuadro de texto tenemos

{{texto}}

Las llaves doblen indican que lo que está entre ellas debe evaluarse como una expresión. Cuando escribimos un texto, el valor de la variable texto se modifica y este cambio automáticamente se ve reflejado. Angular (que está detrás de IONIC) se encarga de modificar el DOM (Document Object Model) de forma transparente para nosotros. No se preocupen si no ha quedado claro del todo. Lo iremos comprendiendo cada vez mejor. Ahora continuemos. El contenido del archivo add-contacto-modal.ts actualmente es el siguiente: 1 2 3 4 5 6 7 8 9 10 11 12

import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; /* Generated class for the AddContactoModal page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-add-contacto-modal', templateUrl: 'add-contacto-modal.html'

Capítulo 2: Primera Aplicación Completa

13 14 15 16 17 18 19 20 21 22

56

}) export class AddContactoModalPage { constructor(public navCtrl: NavController, public navParams: NavParams) {} ionViewDidLoad() { console.log('ionViewDidLoad AddContactoModalPage'); } }

Como primera medida, vamos a cambiar el nombre de la clase de AddContactoModalPage a AddContactoModal, ya que no pretendemos que funcione como una página ordinaria sino como una ventana modal. No es obligatorio cambiarle el nombre, pero me parece prudente. Ya que nuestro propósito es agregar un nuevo contacto, debemos importar nuestro modelo Contacto a fin de poder usarlo. 1

import { Contacto } from '../../model/contacto';

Ahora declaremos una variable para contener una instancia vacía de nuestro modelo. public contacto = new Contacto(0, ‘’, ‘’, ‘’, ‘’); Nótese que debemos pasar los parámetros requeridos al constructor, aun cuando se trate de cero o cadenas vacías, de lo contrarios obtendremos un error. Perfecto. De regreso ahora a contactos.ts, debemos importar nuestra ventana modal para poder usarla. Luego cambiemos el import: 1

import { NavController, NavParams } from 'ionic-angular';

Por: 1

import { NavController, NavParams, ModalController } from 'ionic-angular';

Estamos incluyendo el componente necesario para mostrar una pantalla como modal. Los argumentos del constructor ahora deben cambiar de: constructor(public navCtrl: NavController, public navParams: NavParams) a:

57

Capítulo 2: Primera Aplicación Completa

constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController) Estamos declarando una nueva variable de tipo ModalController y la estamos haciendo pública, de manera de poder utilizarla en métodos de la clase. Ahora podemos reescribir el método que mostrará la pantalla modal. 1 2 3 4 5 6

mostrarAgregarContacto() { let modal = this.modalCtrl.create(AddContactoModal); modal.present(); modal.onDidDismiss(data => {}); }

No estamos en condiciones de ver todo en funcionamiento aún. Debemos agregar el import correspondiente en app.module.ts. 1 2

import { AddContactoModal } from '../pages/add-contacto-modal/add-contacto-modal';

Y agregar la clase en las secciones declarations y entryComponents. Ahora sí, al presionar el botón agregar, veremos nuestra pantalla modal.

Ventana modal

Podemos descartar la pantalla presionando la tecla Escape. Realicemos algunas modificaciones. En primer lugar, asignémosle el estilo a la barra de navegación. Luego, creemos en el archivo add-contacto-modal.ts una variable titulo.

Capítulo 2: Primera Aplicación Completa

1

58

public titulo : String = 'Agregar Contacto';

Y utilicemos las llaves dobles para mostrar el valor de la variable en la plantilla. La barra de navegación tendrá ahora el siguiente contenido: 1 2 3

{{titulo}}

Muy bien. Ahora tenemos que ocuparnos de otro detalle. En estos momentos, estamos descartando la ventana modal mediante la tecla Escape. Vamos a agregar un botón que se ocupe de esto. Justo después del ion-title añadamos lo siguiente: 1 2 3 4 5 6 7

Cancelar



ion-buttons es un componente que nos permite agrupar botones. Luego viene un componente button. Nótese que tenemos dos formas alternativas de presentar el contenido del botón. Por un lado, cuando el sistema operativo sea ios, se mostrará el texto Cancelar. Por otro, para android y Windows mostraremos un ícono. Podemos ver las diferencias a continuación:

59

Capítulo 2: Primera Aplicación Completa

Botón cerrar en Android

Botón cerrar en IOS

Muy bien. Hasta ahora tenemos lo siguiente: • Una pantalla principal que muestra una lista de contactos. Al deslizar la pantalla de derecha

Capítulo 2: Primera Aplicación Completa

60

a izquierda estando sobre un contacto, se muestran tres botones con opciones para ver, editar y eliminar el contacto. – Al presionar el botón ver podemos acceder a una pantalla con los detalles del contacto seleccionado. – En la pantalla inicial, esquina inferior derecha, tenemos un botón que al ser presionado, muestra una ventana modal con un formulario que permite agregar un nuevo contacto. Sin embargo, la funcionalidad para agregar, editar y eliminar contactos aún no se encuentra implementada. Vamos a resolver esto. Comenzaremos por permitir agregar un nuevo contacto. En el archivo add-contacto-modal.ts cambiemos el import: 1

import { NavController, NavParams } from 'ionic-angular';

Por: 1

import { NavController, NavParams, ViewController } from 'ionic-angular';

Luego, agreguemos un nuevo parámetro al constructor. Debe quedar de la siguiente manera: constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController) {} En la plantilla, es decir en el archivo add-contacto-modal.html, interceptamos el envío del formulario de la siguiente manera: (ngSubmit)=”onSubmit()” Nótese que la sintaxis para asociar una función a un evento es: (evento)=”funcion()” Aún no hemos implementado el evento onSubmit, vamos a hacerlo ahora. Nuevamente en addcontacto-modal.ts. Antes de generar la implementación real del método, vamos a realizar una prueba para asegurarnos de que el formulario está funcionando de la manera deseada. Agreguemos lo siguiente: 1 2 3

onSubmit() { console.log(this.contacto); }

61

Capítulo 2: Primera Aplicación Completa

Aquí mostraremos por consola el contacto con los valores recibidos para sus propiedades de parte del formulario. Ejecutemos la aplicación y presionemos el botón agregar. Veremos nuestra ventana modal.

Ventana modal

Nótese que el botón Enviar está desactivado. Sólo se activará cuando el formulario sea válido. Esto lo logramos con las siguiente línea presente en el archivo add-contacto-modal.html: 1 2

Enviar

Lo que hace el fragmento [disabled]=”!formContacto.form.valid” es enlazar el valor de la propiedad disabled con el valor al que evalúa la expresión que se encuentra entre comillas. Dicha expresión es !formContacto.form.valid. El signo de admiración que se encuentra delante es el operador lógico de negación. formContacto.form.valid tomará un valor verdadero cuando el formulario sea válido. Por lo tanto, al poner el operador de negación delante estamos haciendo que la expresión evalúe a verdadero cuando el formulario NO sea válido. Esto está bien, porque nosotros queremos deshabilitar el botón de envío exactamente cuando ocurre eso. El poder validar el formulario en tiempo real, es gracias a la magia de IONIC. Recordemos que al crear el formulario, en su etiqueta de apertura, pusimos: 1

#formContacto="ngForm"

Habíamos mencionado que esto le indica a IONIC que debe tratar a este formulario como un formulario especial al que le agrega funcionalidad extra.

62

Capítulo 2: Primera Aplicación Completa

Esta funcionalidad es la que estamos utilizando para habilitar o deshabilitar el botón de envío. Continuemos. Cuando se han completado todos los campos que hemos definido como obligatorios, el botón cambiará a su estado habilitado como podemos ver a continuación:

Formulario completo

Si presionamos el botón ENVIAR, veremos el siguiente mensaje en la consola:

Contacto en consola

Podemos hacer clic en la flecha de la izquierda y desplegar los detalles:

Detalles de contacto

Como vemos nuestro formulario funciona correctamente. Una vez probado, podemos cambiar la implementación del método onSubmit por la siguiente:

Capítulo 2: Primera Aplicación Completa

1 2 3

63

onSubmit() { this.viewCtrl.dismiss(this.contacto); }

Lo que estamos haciendo ahora es cerrar la vista o pantalla modal, utilizando el componente ViewController, pero además estamos pasando como parámetro al método dismiss el contacto que queremos incorporar. Eso es todo lo que tenemos que hacer en add-contacto-modal.ts. De regreso a contactos.ts La implementación que tenemos del método que muestra la pantalla modal hasta ahora es la siguiente: 1 2 3 4 5 6

mostrarAgregarContacto() { let modal = this.modalCtrl.create(AddContactoModal); modal.present(); modal.onDidDismiss(data => {}); }

Analicemos la línea: modal.onDidDismiss(data ⇒ {}) Tenemos una variable (denominada modal) que referencia nuestra ventana normal. Cuando esta ventana ejecuta el evento dismiss, nosotros lo capturamos para nuestros propósitos. La variable data, representa un dato devuelto por la ventana modal. Recordemos que en el archivo add-contacto-modal.ts nosotros pasábamos como parámetro al método dismiss el objeto conteniendo el nuevo contacto a agregar. Por lo tanto, data viene a representar ese contacto devuelto por la venta modal y que ahora podemos manipular. El operador ⇒ indica que sobre ese dato devuelto operará una función. En este caso se trata de una función vacía, representada por llaves sin contenido. Para comprenderlo mejor: data ⇒ {} es conceptualmente equivalente function(data) {}. Es decir, data es el parámetro de la función cuyo cuerpo está entre llaves. Ahora cambiemos la implementación de todo el método por la siguiente. Del libro:

64

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9 10

mostrarAgregarContacto() { let modal = this.modalCtrl.create(AddContactoModal); modal.present(); modal.onDidDismiss(data => { if (data) { this.contactos.push(data); } }); }

Gist²⁵ La parte nueva, es que ahora la función que opera sobre los datos devueltos deja de estar vacía. Primero verificamos que se hayan recibido datos. Recordemos que data representa un objeto de tipo contacto que ha sido enviado por la ventana modal. Si tenemos un objeto contacto, procedemos a agregar el nuevo contacto a los contactos ya existentes, los cuales se encuentran en nuestra variable contactos que es de tipo array, por lo que utilizamos para ello el método push. Si ahora completamos el formulario y presionamos ENVIAR veremos como el nuevo contacto aparece en nuestra lista de contactos.

Contacto agregado

Podemos ver incluso los detalles del nuevo contacto agregado. ²⁵https://gist.github.com/vihugarcia/df71a58254437d812de6af49bcbd5a2b

65

Capítulo 2: Primera Aplicación Completa

Detalles del contacto agregado

Por supuesto, los datos existen en memoria, y si refrescamos el navegador volveremos a los tres contactos que teníamos originalmente. Vamos a ocuparnos ahora de la funcionalidad para editar un contacto. Como primer paso, vamos a tener que editar nuestro modelo Contacto. Debajo del constructos, añadamos el siguiente método estático: 1 2 3 4

static clone(contacto: Contacto) { return new Contacto(contacto.id, contacto.nombre, contacto.direccion, contacto.telefono, contacto.email); }

Lo que buscamos aquí, es obtener una nueva instancia de un objeto contacto, que es una copia de un contacto determinado. Estamos clonando un contacto. Ya veremos cuál es el propósito de ello. Al hacer el método estático, vamos a poder convocarlo sin tener necesidad de crear una instancia de un objeto Contacto. Ahora si nos dirigimos a la plantilla contactos.html, podemos observar las siguientes líneas de código: 1 2 3 4

Editar

Cuando hacemos clic sobre el botón editar, se invoca un método editarContacto al que se le pasa como dato el contacto seleccionado. Vamos a aprovechar la misma ventana modal que utilizamos para crear un nuevo contacto, y utilizarla para editar los datos del mismo. Por ello, me parece buena idea cambiar el nombre del método editarContacto por mostrarEditarContacto. En el archivo contactos.ts, definamos una variable pública de tipo Contacto:

Capítulo 2: Primera Aplicación Completa

1

66

public contactoOriginal: Contacto;

Esta variable contendrá nuestro contacto antes de las modificaciones. Debemos cambiar el nombre del método editarContacto por el de mostrarEditarContacto. La implementación completa del método es la siguiente. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12

mostrarEditarContacto(contacto: Contacto) { let modal = this.modalCtrl.create(AddContactoModal, {contacto}); this.contactoOriginal = contacto; modal.present(); modal.onDidDismiss(data => { if (data) { console.log(this.contactoOriginal); console.log(data); } }); }

Gist²⁶ Podemos ver que guarda muchas similitudes con el método mostrarAgregarContacto que vimos anteriormente. Una diferencia, es que antes de presentar la ventana normal, guardamos en nuestra variable contactoOriginal una referencia al contacto sin modificar. También, la ventana modal recibe como parámetro el contacto seleccionado. Cuando la ventana modal se cierre (porque se presionó el botón ENVIAR) por ahora simplemente mostraremos por la consola el contacto original y el modificado. Modifiquemos ahora el archivo add-contacto-modal.ts Lo que haremos ahora, es cambiar la implementación del constructor. Reemplacémoslo por lo siguiente: 1 2 3 4 5 6

constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController) { if (this.navParams.get('contacto')) { this.contacto = Contacto.clone(this.navParams.get('contacto')); } } ²⁶https://gist.github.com/vihugarcia/5a9906121767122a12eb3304b1c785ec

67

Capítulo 2: Primera Aplicación Completa

Si la ventana modal recibe como parámetro un contacto, entonces la variable contacto, contendrá ahora una copia del contacto recibido. Aquí vemos la utilidad del método clone. Queremos una copia, y no una referencia al propio objeto. Por ello no podemos usar simplemente this.contacto = contacto. Veamos el funcionamiento hasta ahora. Partamos de nuestra pantalla inicial:

Pantalla Inicial

Y seleccionemos Editar para el primer contacto.

Editar contacto

Vemos como ahora la pantalla modal se carga con los datos del contacto seleccionado. Muy bien. Sin embargo, hay un pequeño detalle. El título muestra Agregar Contacto, lo cual no es correcto.

68

Capítulo 2: Primera Aplicación Completa

Vamos a agregar una nueva línea al constructor de add-contacto-modal.ts 1 2 3 4 5 6 7

constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController) { if (this.navParams.get('contacto')) { this.contacto = Contacto.clone(this.navParams.get('contacto')); this.titulo = 'Editar Contacto'; } }

Se ha resaltado en negrita la nueva línea agregada. Repitamos ahora el proceso.

Editar contacto

Ahora está mejor. Modifiquemos algunos datos del contacto. Si presionamos ENVIAR, veremos lo siguiente en la consola:

Consola

Gracias a ello, sabemos que el formulario está trabajando perfectamente.

Capítulo 2: Primera Aplicación Completa

69

Estupendo. Vamos a quitar ahora los mensajes de la consola, y modificar el método mostrarEditarContacto para que los cambios se vean reflejados en nuestra lista. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

mostrarEditarContacto(contacto: Contacto) { let modal = this.modalCtrl.create(AddContactoModal, {contacto}); this.contactoOriginal = contacto; modal.present(); modal.onDidDismiss(data => { if (data) { let index = this.contactos.indexOf(this.contactoOriginal); this.contactos = [ ...this.contactos.slice(0,index), data, ...this.contactos.slice(index+1) ]; } }); }

Gist²⁷ Como verán, hemos quitado la impresión por consola. Analicemos los cambios: let index = this.contactos.indexOf(this.contactoOriginal); Aquí estamos definiendo una variable local, llamada index, y estamos guardando en ella la posición que ocupa en nuestro arreglo de contactos el contacto original. Recordemos que la variable contactos no es otra cosa que un arreglo de objetos de tipo Contacto. El método indexOf, es un método de búsqueda, que devuelve la posición en el arreglo donde se encuentra un elemento que pasamos como parámetro. Por ello era importante guardar una referencia a nuestro contacto original. Luego tenemos: 1 2 3 4 5

this.contactos = [ ...this.contactos.slice(0,index), data, ...this.contactos.slice(index+1) ]; ²⁷https://gist.github.com/vihugarcia/a015ac35a06f2414f6826905b70ebe45

70

Capítulo 2: Primera Aplicación Completa

¿Qué estamos haciendo aquí? Pues bien, lo queremos hacer, es guardar en la lista de contactos, todos los elementos del arreglo que se encuentran antes del elemento a modificar. En la posición donde se encuentra el elemento original, colocamos el elemento modificado que está disponible en el parámetro data. Luego, tomamos todos los elementos posteriores al elemento modificado. El método slice en un arreglo, nos devuelve una porción del arreglo. Realizar un slice de 0 a index, devuelve una porción del arreglo compuesta por las posiciones 0, 1, etc. Hasta la posición index. Los puntos suspensivos (…) colocados antes de la porción del arreglo, indican que esta porción debe expandirse en cada uno de los elementos que la componen. Es decir, supongamos que index tiene el valor 2. …this.contactos.slice(0, 2) es equivalente a: this.contactos(0), this.contactos(1) Si omitimos el segundo parámetro del método slice, entonces la porción del arreglo que se tomará es desde la posición indicada hasta el final del arreglo. Si ahora editamos un contacto, veremos que los cambios se ven reflejados en nuestra lista.

Contacto editado

Podemos modificar todos los contactos si lo deseamos, y funcionará perfectamente. ¡Genial! Nuestra aplicación va tomando forma ya. Nos resta ocuparnos de la eliminación de un contacto. En contactos.html tenemos las siguientes líneas:

71

Capítulo 2: Primera Aplicación Completa

1 2 3 4

Eliminar

Podemos escribir la implementación del método eliminarContacto como sigue. Del libro: 1 2 3 4 5 6 7

eliminarContacto(contacto: Contacto) { let index = this.contactos.indexOf(contacto); this.contactos = [ ...this.contactos.slice(0,index), ...this.contactos.slice(index+1) ]; }

Gist²⁸ Estamos usando nuevamente indexOf para obtener la posición del elemento, y slice para obtener todos los elementos del arreglo con excepción del elemento que deseamos eliminar. Podemos ver esto en funcionamiento:

Eliminar contacto ²⁸https://gist.github.com/vihugarcia/f8af86a087f0d9909ba2d176dba09723

72

Capítulo 2: Primera Aplicación Completa

Contacto eliminado

Funciona, pero en pos de reducir la posibilidad de errores involuntarios, sería interesante mostrarle al usuario un mensaje de advertencia y darle la oportunidad de decidir si realmente quiere eliminar el contacto o no. Vamos a ver cómo realizar esto, y de paso introduciremos un nuevo componente que seguramente nos será de utilidad en nuestros proyectos. Lo que vamos a hacer, es que en el momento en que el usuario presione el botón Eliminar, se muestre un cuadro de diálogo preguntándole si realmente desea eliminar el contacto, dándole la posibilidad de aceptar o cancelar la eliminación. Para ello, tenemos disponible un componente denominado AlertController. Este nos permite presentar distintos mensajes, tales como cuadros de diálogo y confirmación. En contactos.ts, debemos modificar la línea: 1

import { NavController, NavParams, ModalController } from 'ionic-angular';

Por: 1 2

import { NavController, NavParams, ModalController, AlertController } from 'ionic-angular';

El constructor también debe ser modificado para recibir ahora como parámetro una nueva variable de tipo AlertController para poder utilizarlo. Cambiemos el constructor a:

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6

73

constructor( public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController, public alertCtrl: AlertController ) {}

Ahora vamos a crear un nuevo método que será el encargado de presentar al usuario el cuadro de confirmación, y en caso de que el usuario acepte, se convocará el método que elimina el contacto que no es otro que eliminarContacto. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

confirmarEliminarContacto(contacto: Contacto) { let confirm = this.alertCtrl.create({ title: 'Eliminar contacto', message: '¿Realmente desea eliminar el contacto?', buttons: [ { text: 'Cancelar' }, { text: 'Eliminar', handler: () => { this.eliminarContacto(contacto); } } ] }); confirm.present(); }

Gist²⁹ Finalmente debemos modificar contactos.html para que el presionar el botón Eliminar se convoque a ese nuevo método.

²⁹https://gist.github.com/vihugarcia/64f40a847e92d5457953d4f43865a75c

74

Capítulo 2: Primera Aplicación Completa

1 2 3 4

Eliminar

Si ahora presionamos eliminar, veremos que se nos solicita confirmación:

Diálogo de confirmación

¿No es genial? En muy poco tiempo, hemos podido construir una aplicación completamente funcional con un aspecto decente. Existen por supuesto muchos detalles que podríamos mejorar, pero en este punto espero que haya podido ver el enorme potencial de esta herramienta. Las cosas que podemos hacer, están limitadas solamente por aquello que podamos concebir. Antes de finalizar el capítulo, y simplemente para asegurarnos de que todo funciona correctamente, voy a proporcionar el listado completo de los archivos. Página de Contactos contactos.html Del libro:

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42



{{titulo}}



Contactos

{{texto}}





{{contacto.nombre}} {{contacto.direccion}}



Ver

Editar

75

Capítulo 2: Primera Aplicación Completa

43 44 45 46 47 48 49 50 51 52 53 54 55 56

Eliminar







Gist³⁰ contactos.ts Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

import { Component } from '@angular/core'; import { NavController, NavParams, ModalController, AlertController } from 'ionic-angular'; import { Contacto } from '../../model/contacto'; import { ContactoPage } from '../contacto/contacto'; import { AddContactoModal } from '../add-contacto-modal/add-contacto-modal'; /* Generated class for the Contactos page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contactos', templateUrl: 'contactos.html' }) export class ContactosPage { titulo: String = 'Administrador de Contactos'; public contactos = [ new Contacto(1, "Andrea Gómez", "Calle Uno 123", "12345", "[email protected]"), ³⁰https://gist.github.com/vihugarcia/8331f50647bde1a3051ce59dbcbd14af

76

Capítulo 2: Primera Aplicación Completa

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

new Contacto(2, "Juan Perez", "Calle Dos 567", "23456", "[email protected]"), new Contacto(3, "Martín Álvarez", "Calle del Pueblo 628", "34567", "[email protected]") ]; public contactoOriginal: Contacto; constructor( public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController, public alertCtrl: AlertController ) {} ionViewDidLoad() { console.log('ionViewDidLoad ContactosPage'); } verContacto(contacto: Contacto) { this.navCtrl.push(ContactoPage, {contacto}); } mostrarEditarContacto(contacto: Contacto) { let modal = this.modalCtrl.create(AddContactoModal, {contacto}); this.contactoOriginal = contacto; modal.present(); modal.onDidDismiss(data => { if (data) { let index = this.contactos.indexOf(this.contactoOriginal); this.contactos = [ ...this.contactos.slice(0,index), data, ...this.contactos.slice(index+1) ]; } }); } confirmarEliminarContacto(contacto: Contacto) { let confirm = this.alertCtrl.create({ title: 'Eliminar contacto', message: '¿Realmente desea eliminar el contacto?',

77

Capítulo 2: Primera Aplicación Completa

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

buttons: [ { text: 'Cancelar' }, { text: 'Eliminar', handler: () => { this.eliminarContacto(contacto); } } ] }); confirm.present(); } eliminarContacto(contacto: Contacto) { let index = this.contactos.indexOf(contacto); this.contactos = [ ...this.contactos.slice(0,index), ...this.contactos.slice(index+1) ]; } mostrarAgregarContacto() { let modal = this.modalCtrl.create(AddContactoModal); modal.present(); modal.onDidDismiss(data => { if (data) { this.contactos.push(data); } }); } }

Gist³¹ Página contacto (detalle de un contacto) contacto.html Del libro: ³¹https://gist.github.com/vihugarcia/b1c5a8a22c6fc1645f81c89f3b5cbf2e

78

79

Capítulo 2: Primera Aplicación Completa

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



Contacto





{{contacto.nombre}}

{{contacto.direccion}}

{{contacto.telefono}}

{{contacto.email}}





Gist³² contacto.ts Del libro:

³²https://gist.github.com/vihugarcia/0668d93c567cda8bd8e25766f8959588

Capítulo 2: Primera Aplicación Completa

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

import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; import { Contacto } from '../../model/contacto'; /* Generated class for the Contacto page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contacto', templateUrl: 'contacto.html' }) export class ContactoPage { public contacto: Contacto; constructor(public navCtrl: NavController, public navParams: NavParams) { this.contacto = this.navParams.get('contacto'); } ionViewDidLoad() { console.log('ionViewDidLoad ContactoPage'); } }

Gist³³ Ventana modal Contacto add-contacto-modal.html Del libro:

³³https://gist.github.com/vihugarcia/e2c7812bd8a069fad8383e6ee3e90a30

80

Capítulo 2: Primera Aplicación Completa

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42



{{titulo}}

Cancelar









El nombre es obligatorio



La dirección es obligatoria

81

Capítulo 2: Primera Aplicación Completa

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63



El teléfono es obligatorio



Enviar



Gist³⁴ add-contacto-modal.ts Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14

import { Component } from '@angular/core'; import { NavController, NavParams, ViewController } from 'ionic-angular'; import { Contacto } from '../../model/contacto'; /* Generated class for the AddContactoModal page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-add-contacto-modal', templateUrl: 'add-contacto-modal.html' }) ³⁴https://gist.github.com/vihugarcia/6c8615e9bd2e0f9d9c12366a0221a47c

82

Capítulo 2: Primera Aplicación Completa

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

83

export class AddContactoModal { public contacto = new Contacto(0, '', '', '', ''); public titulo : String = 'Agregar Contacto'; constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController) { if (this.navParams.get('contacto')) { this.contacto = Contacto.clone(this.navParams.get('contacto')); this.titulo = 'Editar Contacto'; } } ionViewDidLoad() { console.log('ionViewDidLoad AddContactoModalPage'); } onSubmit() { this.viewCtrl.dismiss(this.contacto); } }

Gist³⁵ Módulo principal app.module.ts Del libro: 1 2 3 4 5 6 7 8 9 10 11 12

import import import import import import import from

{ NgModule, ErrorHandler } from '@angular/core'; { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; { MyApp } from './app.component'; { HomePage } from '../pages/home/home'; { ContactosPage } from '../pages/contactos/contactos'; { ContactoPage } from '../pages/contacto/contacto'; { AddContactoModal } '../pages/add-contacto-modal/add-contacto-modal';

@NgModule({ declarations: [ MyApp, ³⁵https://gist.github.com/vihugarcia/1358d8e082dc07016b845a4e09c646aahttps:/gist.github.com/vihugarcia/1358d8e082dc07016b845a4e09c646aa

Capítulo 2: Primera Aplicación Completa

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

HomePage, ContactosPage, ContactoPage, AddContactoModal ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp, HomePage, ContactosPage, ContactoPage, AddContactoModal ], providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}] }) export class AppModule {}

Gist³⁶ Componente principal app.component.ts Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14

import { Component } from '@angular/core'; import { Platform } from 'ionic-angular'; import { StatusBar, Splashscreen } from 'ionic-native'; import { HomePage } from '../pages/home/home'; import { ContactosPage } from '../pages/contactos/contactos';

@Component({ templateUrl: 'app.html' }) export class MyApp { rootPage = ContactosPage; ³⁶https://gist.github.com/vihugarcia/bf8d837a12ca8e016ef88b0abd7bff47

84

Capítulo 2: Primera Aplicación Completa

15 16 17 18 19 20 21 22 23

85

constructor(platform: Platform) { platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. StatusBar.styleDefault(); Splashscreen.hide(); }); } }

Gist³⁷ En el próximo capítulo, comenzaremos a trabajar en la configuración de nuestro servidor de desarrollo, de manera que podamos obtener datos desde una fuente externa a nuestra aplicación. Veremos cómo comunicarnos de manera asíncrona con un servidor, manejar errores, y muchas cosas más que llevarán nuestras capacidades a un nivel. Espero que lo disfruten. ³⁷https://gist.github.com/vihugarcia/6e35a7eb61333329ac3900cdfccc44be

Capítulo 3: Servidor de desarrollo A partir de aquí vamos a comenzar a trabajar con datos provenientes de un servicio web. Para esto, es necesario que configuremos completamente nuestro servidor de desarrollo. Si no lo han hecho anteriormente, deben instalar ahora algún software que permita configurar un servidor web. En uno de los apéndices de este libro, se muestra como instalar xampp. Con xampp, podrá tener configurado Apache y MariaDB (si no ha escuchado de MariaDB, se trata de un motor de base de datos desarrollado por el mismo creador de MySQL y totalmente compatible con este mismo). Si están trabajando con Windows, otra alternativa es Wamp. Tiene una funcionalidad muy similar. Por supuesto, no es obligatorio que utilicen alguna de esas herramientas. Si ya cuentan con un entorno de desarrollo, entonces no hay razón para que utilicen otro. Una de las cosas buenas que tiene Xampp, es una interface gráfica para trabajar con bases de datos. Vamos a crear una base de datos denominada contactmgr y seleccionar para ella como codificación utf8_unicode_ci.

Creación BD

Vamos a crear ahora una tabla denominada contactos con cinco columnas:

Tabla de contactos

No olviden marcar la columna id como autoincremental.

87

Capítulo 3: Servidor de desarrollo

Una vez creada la tabla, insertemos unos cuantos registros. Podemos utilizar los mismos datos de nuestro arreglo contactos.

Insertar contactos

La tabla deberá haber quedado similar a:

Registros insertados

Perfecto. Ya tenemos datos para trabajar.

Capítulo 3: Servidor de desarrollo

88

Api Rest Actualmente es muy común que distintas compañías y organizaciones implementen software en la forma de un conjunto de servicios. Un servicio es básicamente un conjunto de funciones que son encapsuladas y están disponibles a través de una interface. Los consumidores del servicio (personas u otro software) no necesitan estar al tanto de cómo es implementado el servicio. Simplemente necesitan conocer cuál es el protocolo que deben utilizar para comunicarse con el servicio. Un tipo de servicios muy común es el de servicio web.

Servicios Web Un servicio web (en inglés, Web Service o Web services) es una tecnología que utiliza un conjunto de protocolos y estándares que sirven para intercambiar datos entre aplicaciones. Distintas aplicaciones de software desarrolladas en lenguajes de programación diferentes, y ejecutadas sobre cualquier plataforma, pueden utilizar los servicios web para intercambiar datos en redes de ordenadores como Internet. La interoperabilidad se consigue mediante la adopción de estándares abiertos. Las organizaciones OASIS y W3C son los comités responsables de la arquitectura y reglamentación de los servicios Web. Para mejorar la interoperabilidad entre distintas implementaciones de servicios Web se ha creado el organismo WS-I, encargado de desarrollar diversos perfiles para definir de manera más exhaustiva estos estándares. Es una máquina que atiende las peticiones de los clientes web y les envía los recursos solicitados.[1] Entre los estándares utilizados por los servicios web están los siguientes: • Web Services Protocol Stack: Así se le denomina al conjunto de servicios y protocolos de los servicios Web. • XML (Extensible Markup Language): Es el formato estándar para los datos que se vayan a intercambiar. • SOAP (Simple Object Access Protocol) o XML-RPC (XML Remote Procedure Call): Protocolos sobre los que se establece el intercambio. • Otros protocolos: los datos en XML también pueden enviarse de una aplicación a otra mediante protocolos normales como HTTP (Hypertext Transfer Protocol), FTP (File Transfer Protocol), o SMTP (Simple Mail Transfer Protocol). • WSDL (Web Services Description Language): Es el lenguaje de la interfaz pública para los servicios Web. Es una descripción basada en XML de los requisitos funcionales necesarios para establecer una comunicación con los servicios Web.

Capítulo 3: Servidor de desarrollo

89

• UDDI (Universal Description, Discovery and Integration): Protocolo para publicar la información de los servicios Web. Permite comprobar qué servicios web están disponibles. • WS-Security (Web Service Security): Protocolo de seguridad aceptado como estándar por OASIS (Organization for the Advancement of Structured Information Standards). Garantiza la autenticación de los actores y la confidencialidad de los mensajes enviados. • REST (Representational State Transfer): arquitectura que, haciendo uso del protocolo HTTP, proporciona una API que utiliza cada uno de sus métodos (GET, POST, PUT, DELETE, etc) para poder realizar diferentes operaciones entre la aplicación que ofrece el servicio web y el cliente. De todas ellas, probablemente es la arquitectura REST aquella que goza de mayor popularidad, y es la que vamos a utilizar para implementar nuestros servicios. Ahora bien. Explicar la forma de desarrollar los servicios web escapa del alcance de este libro. Voy a proporcionarles un enlace para descargar el código fuente que deberían copiar dentro del directorio raíz de su servidor de desarrollo. Esta raíz puede ser htdocs o www, dependiendo de qué entorno de desarrollo esté utilizando. El enlace para descargar el código es el siguiente: https://drive.google.com/file/d/0B0zIX5vLhmRGTUI5b3l5eWRyLUE/view?usp=sharing³⁸ Otra cosa que recomiendo es instalar la extensión Postman de Google Chrome. Esta extensión permite probar distintos tipos de peticiones y es excelente para probar apis. Podemos simular peticiones con distintos parámetros, headers y otras muchas opciones. No es algo obligatorio sin embargo. Al descomprimir el archivo descargado, tendrán una carpeta llamada api-cmgr. Recuerden que esa carpeta debe ir dentro de la raíz de su servidor de desarrollo. Dentro de la carpeta api-cmgr podrán encontrar un archivo .sql que pueden importar con phpMyAdmin para crear la base de datos y la tabla, si es que no lo han hecho anteriormente. Finalmente, encontrarán un archivo denominado contactos-api.php. Este archivo contiene la api para administrar recursos de tipo contacto. No debemos preocuparnos por cómo está construido este archivo, basta con conocer la estructura de los datos que envía y recibe. Si les interesa explorar el archivo sin embargo, verán que es muy sencillo. Utiliza un micro framework llamado Slim³⁹. Es un framework muy simple pero aún así es poderoso y adecuado tanto para el desarrollo de aplicaciones web como en este caso para APIs. En el archivo contactos-api.php hay una línea que debemos editar para poner los datos correspondientes a nuestra configuración del servidor. ³⁸https://drive.google.com/file/d/0B0zIX5vLhmRGTUI5b3l5eWRyLUE/view?usp=sharing ³⁹https://www.slimframework.com/

90

Capítulo 3: Servidor de desarrollo

$db = new mysqli(“localhost”, “root”, “”, “contactmgr”); Eso es todo. No deberíamos tener que modificar nada más. Si abrimos ahora una nueva pestaña de nuestro navegador y vamos a la dirección: http://localhost/apicmgr/contactos-api.php/contactos⁴⁰ Veremos una salida como la siguiente:

Datos de contactos

Si están usando Postman la salida será similar a esta:

Postman

Nuevamente, no es obligatorio instalar esta extensión en el navegador. Es simplemente que se trata de una herramienta tan buena que no puedo dejar de recomendarla. Tenemos todo funcionando apropiadamente. Genial. Ahora podemos comenzar a modificar nuestra aplicación para que consuma recursos desde la fuente de datos que hemos configurado. Como primera medida, creemos un directorio denominado shared a la misma altura que el directorio pages, y dentro de este directorio añadamos un archivo llamado app.settings.ts con el siguiente contenido:

⁴⁰http://localhost/api-cmgr/contactos-api.php/contactos

Capítulo 3: Servidor de desarrollo

1 2 3 4 5

91

export class AppSettings { public static get API_ENDPOINT() { return 'http://localhost/api-cmgr/contactos-api.php'; } }

En este archivo tenemos una función estática que nos devolverá la dirección de nuestro recurso. De esta forma no tendremos que hard codear dicha ruta en las peticiones que realicemos. Continuemos. Ahora creemos un nuevo directorio, a la misma altura que el anterior, denominado services, y dentro de él creemos un archivo denominado contacto.service.ts. Comencemos agregando los siguientes imports al archivo: 1 2 3 4 5 6

import import import import import import

{Injectable} from "@angular/core"; {Http, Response, Headers} from "@angular/http"; "rxjs/add/operator/map"; {Observable} from "rxjs/Observable"; {Contacto} from "../model/contacto"; {AppSettings} from "../shared/app.settings";

Muy bien. En IONIC, podemos considerar un servicio como un componente que nos permite realizar peticiones asíncronas a un recurso, y manejar las respuestas que recibimos como resultado de esa petición, incluyendo situaciones de error. Los cuatro primeros import lidian con dichos aspectos, en tanto que los dos últimos hacen referencia a nuestro modelo de datos y a al archivo que contiene la dirección de la API. A continuación añadamos el siguiente contenido: 1 2 3 4 5 6 7 8 9 10 11

@Injectable() export class ContactoService { constructor(private _http:Http) { } getContactos() { return this._http.get(`${AppSettings.API_ENDPOINT}/contactos`) .map(res => res.json()); } }

Capítulo 3: Servidor de desarrollo

92

Vemos: @Injectable() indica que la clase que estamos definiendo podrá ser “inyectada” como dependencia en otras clases. No nos preocupemos por ello ahora. A continuación realizamos el export de la clase, para que esta pueda ser utilizada, y finalmente agregamos un método que tendrá como finalidad devolvernos nuestra lista de contactos. Noten que en el método getContactos tenemos la siguiente cadena de texto: ${AppSettings.API_ENDPOINT}/contactos

Estamos usando unas comillas especiales, las comillas invertidas (o backticks). Estas comillas son muy útiles, porque nos permiten expandir variables dentro de ellas. En este caso. Para ello se utiliza la notación: ${variable} Dentro de las comillas. En este caso, entre llaves estamos llamando al método estático API_ENDPOINT de la clase AppSettings, el cuál como hemos visto devuelve una cadena con la dirección del recurso a consumir. Eso es todo por ahora en este archivo. Debemos ahora realizar varias modificaciones a nuestro archivo contactos.ts En primer lugar, debemos cambiar el primer import por lo siguiente: 1

import { Component, OnInit } from '@angular/core';

Luego, necesitamos agregar el import del servicio que hemos creado. 1

import { ContactoService } from '../../services/contacto.service';

Cambiemos el export de la clase por: 1

export class ContactosPage implements OnInit

Definamos las siguientes dos variables: 1 2

public status: String; public errorMessage: String;

Modifiquemos el constructor de la siguiente forma:

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7

constructor( public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController, public alertCtrl: AlertController, private _contactoService: ContactoService ) {}

Agreguemos dos nuevos métodos. Del libro: 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

ngOnInit() { this.getContactos(); // console.log("contactos-list component cargado"); // this.getContactos(); } getContactos() { this._contactoService.getContactos() .subscribe( result => { this.contactos = result.data; this.status = result.status; if (this.status != "success") { console.log("Error en el servidor"); } }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } } ); }

Gist⁴¹ Ahora centremos nuestra atención en el archivo app.module.ts. Debemos agregar allí un nuevo import del servicio que hemos creado. ⁴¹https://gist.github.com/vihugarcia/8039852af4da181178f884ad931bd582

93

94

Capítulo 3: Servidor de desarrollo

1

import { ContactoService } from '../services/contacto.service';

Luego necesitamos reemplazar la sección providers por la siguiente: 1 2 3 4

providers: [ {provide: ErrorHandler, useClass: IonicErrorHandler}, ContactoService ]

Nótese que en este caso no hemos agregado la clase ContactoService a las secciones declarations y entryComponents. Esto es así porque ContactoService no es un componente de visualización, sino un servicio inyectable que debe ser declarado como proveedor. Si ahora ejecutamos ionic serve, veremos que en apariencia nuestra aplicación no ha sufrido ningún cambio. Incluso el agregado, edición y eliminación de contactos funciona.

Recursos del servidor

Sin embargo, si seleccionamos la pestaña Network, notaremos lo siguiente:

95

Capítulo 3: Servidor de desarrollo

Petición al servidor

Existe una petición a un recurso externo, que no es otra cosa que nuestra api. Vemos que esta devuelve una respuesta en formato JSON (json es un formato para intercambiar datos entre un cliente y un servidor). Esta respuesta en formato json es convertida en un conjunto de objetos, que son asignados a nuestro arreglo contactos. El método encargado de enviar la petición y manipular la respuesta es getContactos, por lo que procederemos a analizarlo con cuidado. La respuesta en formato json es la siguiente:

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7 8

96

{"status":"success", "data":[{"id":"3","nombre":"Mart\u00edn \u00c1lvarez", "direccion":"Calle del Pueblo 628","telefono":"34567", "email":"[email protected]"}, {"id":"2","nombre":"Juan Perez", "direccion":"Calle Dos 567","telefono":"23456","email":"[email protected]"}, {"id":"1","nombre":"Andrea G\u00f3mez", "direccion":"Calle Uno 123","telefono":"12345","email":"[email protected]"}]}

Tenemos por una lado un elemento denominado status. Este elemento contendrá success o error dependiendo de si la petición ha sido exitosa o no. Luego tenemos un elemento data, que está formado por un conjunto de elementos, que en este caso son los contactos. Resumiendo: status contiene el estado de la petición, y data contiene la carga útil de datos de la respuesta. Vamos ahora al método en cuestión. Voy a mostrarlo primero omitiendo ciertas partes: 1 2 3 4 5 6 7 8 9 10

this._contactoService.getContactos() .subscribe( result => { … }, error => { … } } );

La variable privada _contactoService contiene un objeto de tipo ContactoService. A partir de ese objeto, se invoca el método getContactos que definimos anteriormente en el servicio. Cuando se invoca a un servicio, uno puede suscribirse a dicho servicio. Es decir, podemos solicitar que se nos envíe una notificación cuando el servicio haya devuelto una respuesta. Esto es necesario porque las peticiones son asíncronas y el tiempo de respuesta es variable. El método suscribe recibe como parámetros dos funciones. La primera corresponde a una respuesta positiva (una respuesta con código 200 por parte del servicio web) y la otra a una situación de error. Nótese que se está utilizando aquí la notación de función abreviada en ambos casos. La primera función recibe una respuesta en el parámetro result y la segunda en el parámetro error. Luego cada función opera sobre dicho parámetro de manera apropiada. La función de respuesta positiva es:

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7 8

97

result => { this.contactos = result.data; this.status = result.status; if (this.status != "success") { console.log("Error en el servidor"); } }

Lo que hacemos es tomar la carga útil de datos que se encuentra en la respuesta y asignarla a nuestro arreglo de contactos. Asimismo, tomamos el estado de la petición y preguntamos si la respuesta ha sido exitosa o no. Aquí hay que prestar atención y no confundir dos cosas diferentes. Por un lado, habíamos dicho que esta función se ejecuta en el caso de una respuesta positiva del servidor. Pero eso se refiere simplemente a una respuesta con estado 200, que significa que el recurso existía y fue alcanzado y que este respondió. Por ejemplo, supongamos que realizamos una petición a http://localhost/api-cmgr/contactos-api.php/recursono-existente. Esta dirección no existe. No hay ningún recurso disponible escuchando en ella. Por lo tanto, en este caso la respuesta del servidor hubiera sido un código 404 (u otro similar) en lugar de un código 200 y la primera función no se hubiera ejecutado. Ahora bien. Aunque el recurso se encuentre, es necesaria la segunda verificación 1 2 3

if (this.status != "success") { console.log("Error en el servidor"); }

Que en este caso se refiere específicamente a la respuesta de la aplicación. Por ejemplo, la api podría devolver un código de error en status en el caso de que no hubiera contactos. En el caso de que el servidor responda con un código distinto a 200, se ejecutará la segunda función 1 2 3 4 5 6

error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } }

Capítulo 3: Servidor de desarrollo

98

Que simplemente realiza un log del error. Vamos a agregar ahora a nuestro servicio un método para añadir nuevos contactos. En contacto.service.ts. Del libro: 1 2 3 4 5 6 7 8 9 10 11

addContacto(contacto: Contacto) { let json = JSON.stringify(contacto); let params = "json="+json; let headers = new Headers({"Content-Type":"application/x-www-form-urlencoded"}); return this._http.post(`${AppSettings.API_ENDPOINT}/contactos`, params, {headers: headers}) .map(res => res.json()); }

Gist⁴² Agreguemos también un nuevo método para editar contactos, por ahora vacío. 1 2 3

editContacto(contacto: Contacto) { }

Vamos ahora a trabajar en nuestra ventana modal. Realizando las modificaciones necesarias para agregar un nuevo contacto en la base de datos. Añadamos el import de nuestro servicio: 1

import { ContactoService} from '../../services/contacto.service';

Agreguemos una variable pública denominada acción, otras dos variables para el estado de la petición y mensajes de error, y una variable para contener el id del contacto:

⁴²https://gist.github.com/vihugarcia/be7c74711105e571eb54fc2065ac178a

Capítulo 3: Servidor de desarrollo

1 2 3 4

public public public public

accion : String; status: String; errorMessage: String; id;

Reformulemos el constructor como sigue: 1 2 3 4 5 6 7 8 9 10 11 12

constructor( public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController, private _contactoService: ContactoService) { if (this.navParams.get('contacto')) { this.contacto = Contacto.clone(this.navParams.get('contacto')); this.titulo = 'Editar Contacto'; this.accion = 'editar'; } }

Reemplacemos la anterior implementación del método onSubmit por esta. Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

onSubmit() { let observable; if (this.accion == "editar") { observable = this._contactoService.editContacto(this.id, this.contacto); } else { observable = this._contactoService.addContacto(this.contacto); } observable.subscribe( response => { this.status = response.status; if (this.status != "success") { console.log("Error en el servidor"); } }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage);

99

100

Capítulo 3: Servidor de desarrollo

19 20 21 22 23 24

} } ); this.viewCtrl.dismiss(this.contacto); }

Gist⁴³ De regreso ahora a contactos.ts En el método mostrarAgregarContacto reemplacemos la línea: 1

this.contactos.push(data);

Por: 1

this.getContactos();

Con esas modificaciones el agregado de un nuevo contacto debería funcionar. Verifiquemoslo. Completamos el formulario y vemos aparecer en nuestra lista el nuevo contacto agregado:

Contacto agregado

La diferencia con lo que ocurría anteriormente es que ahora el contacto es agregado en la tabla de la base de datos. ⁴³https://gist.github.com/vihugarcia/f8bde975ae6edd20c869e1926074cdd7

101

Capítulo 3: Servidor de desarrollo

Registros insertados

Vamos a trabajar ahora en la edición de un contacto. Editemos el servicio contacto.service.ts para completar la implementación del archivo editContacto. Debe quedar así. Del libro: 1 2 3 4 5 6 7 8 9 10

editContacto(id, contacto: Contacto) { let json = JSON.stringify(contacto); let params = "json="+json; let headers = new Headers({"Content-Type":"application/x-www-form-urlencoded"}); return this._http.put(`${AppSettings.API_ENDPOINT}/update-contacto/`+id, params, {headers: headers}) .map(res => res.json()); }

Gist⁴⁴ Como pueden ver, el método editContacto es muy similar al método addContacto. Sin embargo hay una diferencia importante. En lugar de enviarse una petición mediante un método POST se utiliza una petición de tipo PUT. En internet hay muchas fuentes valiosas donde se habla del protocolo REST. Ya que la API que se encuentra en el backend en este caso utiliza el framework Slim, me permite indicarles un tutorial muy valioso sobre implementación de una api rest con Slim que pueden encontrar aquí⁴⁵, en caso de que quieran explorar el desarrollo backend. Esta es una convención del protocolo REST. POST se utiliza para la creación de un nuevo recurso (un nuevo contacto), y PUT para la modificación de un recurso existente. Ahora vayamos al archivo add-contacto-modal.ts. Tenemos que agregar una línea en el constructor: ⁴⁴https://gist.github.com/vihugarcia/9b2f38d44be9a7960b06f4c2bbbe8e5e ⁴⁵https://manuais.iessanclemente.net/index.php/Introduccion_a_API_REST_y_framework_Slim_de_PHP#PUT_.28Actualizar.29

102

Capítulo 3: Servidor de desarrollo

1

this.id = this.contacto.id;

A continuación cambiemos la implementación del método mostrarEditarContacto de contactos.ts de la siguiente manera. Del libro: 1 2 3 4 5 6 7 8 9 10 11

mostrarEditarContacto(contacto: Contacto) { let modal = this.modalCtrl.create(AddContactoModal, {contacto}); this.contactoOriginal = contacto; modal.present(); modal.onDidDismiss(data => { if (data) { this.getContactos(); } }); }

Gist⁴⁶ Con estos cambios ya tenemos funcionando la edición de un contacto. Probemoslo.

Editar contacto ⁴⁶https://gist.github.com/vihugarcia/b4c0da414e482859f1789b395fba1f5d

103

Capítulo 3: Servidor de desarrollo

Editar contacto

¡Genial! Sólo falta implementar la eliminación de contactos en el servidor. Vamos a agregar el siguiente método a nuestro servicio. Del libro: 1 2 3 4

deleteContacto(id) { return this._http.delete(`${AppSettings.API_ENDPOINT}/delete-contacto/`+id) .map(res => res.json()); }

Gist⁴⁷ Aquí también quiero señalar algo importante. Como vemos en este caso el tipo de petición que se envía no es ni POST ni PUT, sino DELETE. Nuevamente estamos siguiendo la convención REST. Ahora en contactos.ts modifiquemos la implementación del método eliminarContacto de la siguiente manera. Del libro:

⁴⁷https://gist.github.com/vihugarcia/9292304084ac03f1537ac84a2f7fe850

104

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

eliminarContacto(contacto: Contacto) { this._contactoService.deleteContacto(contacto.id) .subscribe( result => { this.status = result.status; if (this.status != "success") { console.log("Error en el servidor"); } this.getContactos(); }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } } ); }

Gist⁴⁸ Con estos cambios ya debería funcionar la eliminación. Verifiquemoslo.

Eliminar contacto ⁴⁸https://gist.github.com/vihugarcia/dac79aa477a88361046936ade8e4278e

105

Capítulo 3: Servidor de desarrollo

Diálogo de confirmación

Eliminar contacto

¡Fantástico! Nuestra aplicación ya es totalmente funcional. Sin embargo, creo que aún podemos mejorar la experiencia del usuario. Pensemos en la siguiente situación. Supongamos que una petición que estamos realizando a un recurso, demora un tiempo en completarse. Durante ese tiempo, el usuario se encuentra con una pantalla que está inactiva.

Capítulo 3: Servidor de desarrollo

106

Puede ser que se impaciente o se confunda, no sabiendo si ocurrió algo malo y si debe ejecutar la acción nuevamente. Es siempre conveniente darle alguna clase de feedback al usuario, aún cuando se trate de algo tan simple como mostrar un indicador de espera. Afortunadamente, IONIC posee un componente que nos permite mostrar precisamente esto. Este componente recibe el nombre de LoaderController. Vamos a ver como implementarlo. En el archivo contactos.ts modifiquemos el import: 1 2

import { NavController, NavParams, ModalController, AlertController } from 'ionic-angular';

Por: 1 2

import { NavController, NavParams, ModalController, AlertController, LoadingController } from 'ionic-angular';

Estamos agregando el componente mencionado. Como ya debemos saber en este punto, tenemos que modificar el constructor de la clase para recibir un nuevo parámetro cuyo tipo corresponde al componente importado. La nueva implementación de nuestro constructor es como sigue: 1 2 3 4 5 6 7 8

constructor( public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController, public alertCtrl: AlertController, public loadingCtrl: LoadingController, private _contactoService: ContactoService ) {}

Perfecto. Vamos a poner ahora el loader en acción. Supongamos que queremos mostrar un indicador de espera mientras esperamos que la eliminación de un contacto concluya. Modifiquemos el método eliminarContacto de la siguiente manera. Del libro:

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

107

eliminarContacto(contacto: Contacto) { let loader = this.loadingCtrl.create(); loader.present(); this._contactoService.deleteContacto(contacto.id) .subscribe( result => { this.status = result.status; if (this.status != "success") { console.log("Error en el servidor"); } this.getContactos(); loader.dismiss(); }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } loader.dismiss(); } ); }

Gist⁴⁹ Para que podamos apreciar como funciona esto, voy a introducir un retraso intencional en la respuesta del servidor. Si ahora seleccionamos un contacto y hacemos clic en eliminar, veremos un loader mientras se aguarda que la petición concluya. ⁴⁹https://gist.github.com/vihugarcia/23f80ceb3afd550aaaa3c592daac4467

108

Capítulo 3: Servidor de desarrollo

Loader

Este simple detalle mejora mucho la usabilidad de nuestro proyecto. Con esto hemos concluido las operaciones básicas sobre contactos y tenemos ya una aplicación funcional. El próximo capítulo girará en torno a agregar nueva funcionalidad (búsqueda de contacto, ordenamiento, etc.) y al mismo tiempo mejorar la estética general. Antes de avanzar, y para asegurarnos de que todo funcione correctamente, voy a dejar el contenido completo de cada archivo relevante. Si está atascado con algo, puede comparar el contenido de su archivo con el listado correspondiente y de esta manera resolver los errores que se puedan presentar. Página contactos contactos.html Del libro: 1 2 3 4 5 6 7 8 9



Capítulo 3: Servidor de desarrollo

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

{{titulo}}



Contactos

{{texto}}





{{contacto.nombre}} {{contacto.direccion}}



Ver

Editar

Eliminar



109

Capítulo 3: Servidor de desarrollo

52 53 54 55



Gist⁵⁰ contactos.ts Del libro: 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

import { Component, OnInit } from '@angular/core'; import { NavController, NavParams, ModalController, AlertController, LoadingController } from 'ionic-angular'; import { Contacto } from '../../model/contacto'; import { ContactoPage } from '../contacto/contacto'; import { AddContactoModal } from '../add-contacto-modal/add-contacto-modal'; import { ContactoService } from '../../services/contacto.service'; /* Generated class for the Contactos page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contactos', templateUrl: 'contactos.html' }) export class ContactosPage implements OnInit { titulo: String = 'Administrador de Contactos'; public contactos = [ new Contacto(1, "Andrea Gómez", "Calle Uno 123", "12345", "[email protected]"), new Contacto(2, "Juan Perez", "Calle Dos 567", "23456", "[email protected]"), new Contacto(3, "Martín Álvarez", "Calle del Pueblo 628", "34567", "[email protected]") ]; public contactoOriginal: Contacto; public status: String; public errorMessage: String; constructor( ⁵⁰https://gist.github.com/vihugarcia/5d4681c5fc0cae8b14e30ad86780ffb6

110

Capítulo 3: Servidor de desarrollo

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73

public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController, public alertCtrl: AlertController, public loadingCtrl: LoadingController, private _contactoService: ContactoService ) {} ngOnInit() { this.getContactos(); } getContactos() { this._contactoService.getContactos() .subscribe( result => { this.contactos = result.data; this.status = result.status; if (this.status != "success") { console.log("Error en el servidor"); } }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } } ); } ionViewDidLoad() { console.log('ionViewDidLoad ContactosPage'); } verContacto(contacto: Contacto) { this.navCtrl.push(ContactoPage, {contacto}); } mostrarEditarContacto(contacto: Contacto) { let modal = this.modalCtrl.create(AddContactoModal, {contacto});

111

Capítulo 3: Servidor de desarrollo

74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

this.contactoOriginal = contacto; modal.present(); modal.onDidDismiss(data => { if (data) { this.getContactos(); } }); } confirmarEliminarContacto(contacto: Contacto) { let confirm = this.alertCtrl.create({ title: 'Eliminar contacto', message: '¿Realmente desea eliminar el contacto?', buttons: [ { text: 'Cancelar' }, { text: 'Eliminar', handler: () => { this.eliminarContacto(contacto); } } ] }); confirm.present(); } eliminarContacto(contacto: Contacto) { let loader = this.loadingCtrl.create(); loader.present(); this._contactoService.deleteContacto(contacto.id) .subscribe( result => { this.status = result.status; if (this.status != "success") { console.log("Error en el servidor"); } this.getContactos();

112

Capítulo 3: Servidor de desarrollo

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

loader.dismiss(); }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } loader.dismiss(); } ); } mostrarAgregarContacto() { let modal = this.modalCtrl.create(AddContactoModal); modal.present(); modal.onDidDismiss(data => { if (data) { this.getContactos(); } }); } }

Gist⁵¹ Página de detalles de un contacto contacto.html Del libro: 1 2 3 4 5 6 7 8 9



⁵¹https://gist.github.com/vihugarcia/b7320c840ccb5c60e1d2e803cdd551c3

113

114

Capítulo 3: Servidor de desarrollo

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

Contacto





{{contacto.nombre}}

{{contacto.direccion}}

{{contacto.telefono}}

{{contacto.email}}





Gist⁵² contacto.ts Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; import { Contacto } from '../../model/contacto'; /* Generated class for the Contacto page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-contacto', templateUrl: 'contacto.html' }) ⁵²https://gist.github.com/vihugarcia/e7f0a994c701ad7bd1baca907ac73608

Capítulo 3: Servidor de desarrollo

16 17 18 19 20 21 22 23 24 25 26 27

export class ContactoPage { public contacto: Contacto; constructor(public navCtrl: NavController, public navParams: NavParams) { this.contacto = this.navParams.get('contacto'); } ionViewDidLoad() { console.log('ionViewDidLoad ContactoPage'); } }

Gist⁵³ Ventana modal Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23



{{titulo}}

Cancelar





⁵³https://gist.github.com/vihugarcia/dcf7ab7bf60af7f907dc55969bdebb03

115

Capítulo 3: Servidor de desarrollo

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65



El nombre es obligatorio



La dirección es obligatoria



El teléfono es obligatorio





Enviar



116

Capítulo 3: Servidor de desarrollo

66

Gist⁵⁴ add-contacto-modal.ts Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

import import import import

{ { { {

Component } from '@angular/core'; NavController, NavParams, ViewController } from 'ionic-angular'; Contacto } from '../../model/contacto'; ContactoService} from '../../services/contacto.service';

/* Generated class for the AddContactoModal page. See http://ionicframework.com/docs/v2/components/#navigation for more info on Ionic pages and navigation. */ @Component({ selector: 'page-add-contacto-modal', templateUrl: 'add-contacto-modal.html' }) export class AddContactoModal { public contacto = new Contacto(0, '', '', '', ''); public titulo : String = 'Agregar Contacto'; public accion : String; public status: String; public errorMessage: String; public id; constructor( public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController, private _contactoService: ContactoService) { if (this.navParams.get('contacto')) { this.contacto = Contacto.clone(this.navParams.get('contacto')); this.titulo = 'Editar Contacto'; this.accion = 'editar'; this.id = this.contacto.id; ⁵⁴https://gist.github.com/vihugarcia/b0a10a9f10942bb73b494efbb6f033eb

117

Capítulo 3: Servidor de desarrollo

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

} } ionViewDidLoad() { console.log('ionViewDidLoad AddContactoModalPage'); } onSubmit() { let observable; if (this.accion == "editar") { observable = this._contactoService.editContacto(this.id, this.contacto); } else { observable = this._contactoService.addContacto(this.contacto); } observable.subscribe( response => { this.status = response.status; if (this.status != "success") { console.log("Error en el servidor"); } }, error => { this.errorMessage = Error; if (this.errorMessage != null) { console.log(this.errorMessage); } } ); this.viewCtrl.dismiss(this.contacto); } }

Gist⁵⁵ Modelo contacto contacto.ts Del libro: ⁵⁵https://gist.github.com/vihugarcia/821f02189b29fcb21f239e8b8cc7ea9a

118

119

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7 8 9 10 11 12 13 14

export class Contacto{ constructor( public public public public public ){}

id: number, nombre: string, direccion: string, telefono: string, email: string

static clone(contacto: Contacto) { return new Contacto(contacto.id, contacto.nombre, contacto.direccion, contacto.telefono, contacto.email); } }

Gist⁵⁶ Archivo de configuración de la dirección de la api Del libro: 1 2 3 4 5

export class AppSettings { public static get API_ENDPOINT() { return 'http://localhost/api-cmgr/contactos-api.php'; } }

Gist⁵⁷ Servicio contacto.service.ts Del libro:

⁵⁶https://gist.github.com/vihugarcia/e53821bb37529027c2a219e11b6628b7 ⁵⁷https://gist.github.com/vihugarcia/6693e3bbe3e76e6a76bedca91034c299

Capítulo 3: Servidor de desarrollo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

import import import import import import

{Injectable} from "@angular/core"; {Http, Response, Headers} from "@angular/http"; "rxjs/add/operator/map"; {Observable} from "rxjs/Observable"; {Contacto} from "../model/contacto"; {AppSettings} from "../shared/app.settings";

@Injectable() export class ContactoService { constructor(private _http:Http) { } getContactos() { return this._http.get(`${AppSettings.API_ENDPOINT}/contactos`) .map(res => res.json()); } addContacto(contacto: Contacto) { let json = JSON.stringify(contacto); let params = "json="+json; let headers = new Headers({"Content-Type":"application/x-www-form-urlencoded"}); return this._http.post( `${AppSettings.API_ENDPOINT}/contactos`, params, {headers: headers}) .map(res => res.json()); } editContacto(id, contacto: Contacto) { let json = JSON.stringify(contacto); let params = "json="+json; let headers = new Headers({"Content-Type":"application/x-www-form-urlencoded"}); return this._http.put( `${AppSettings.API_ENDPOINT}/update-contacto/`+id, params, {headers: headers}) .map(res => res.json()); }

120

Capítulo 3: Servidor de desarrollo

43 44 45 46 47 48 49 50

deleteContacto(id) { return this._http.delete( `${AppSettings.API_ENDPOINT}/delete-contacto/`+id) .map(res => res.json()); } }

Gist⁵⁸ Módulo principal Del libro: 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

import import import import import import import from import

{ NgModule, ErrorHandler } from '@angular/core'; { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; { MyApp } from './app.component'; { HomePage } from '../pages/home/home'; { ContactosPage } from '../pages/contactos/contactos'; { ContactoPage } from '../pages/contacto/contacto'; { AddContactoModal } '../pages/add-contacto-modal/add-contacto-modal'; { ContactoService } from '../services/contacto.service';

@NgModule({ declarations: [ MyApp, HomePage, ContactosPage, ContactoPage, AddContactoModal ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp, HomePage, ContactosPage, ContactoPage, ⁵⁸https://gist.github.com/vihugarcia/62e1448ddd5c38fafc2fa37c6f58b56f

121

Capítulo 3: Servidor de desarrollo

28 29 30 31 32 33 34 35

AddContactoModal ], providers: [ {provide: ErrorHandler, useClass: IonicErrorHandler}, ContactoService ] }) export class AppModule {}

Gist⁵⁹ Componente principal de la aplicación Del libro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

import { Component } from '@angular/core'; import { Platform } from 'ionic-angular'; import { StatusBar, Splashscreen } from 'ionic-native'; import { HomePage } from '../pages/home/home'; import { ContactosPage } from '../pages/contactos/contactos';

@Component({ templateUrl: 'app.html' }) export class MyApp { rootPage = ContactosPage; constructor(platform: Platform) { platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. StatusBar.styleDefault(); Splashscreen.hide(); }); } }

Gist⁶⁰ [1] https://es.wikipedia.org/wiki/Servicio_web ⁵⁹https://gist.github.com/vihugarcia/32cf5530d3d7f1c398738f85e419596f ⁶⁰https://gist.github.com/vihugarcia/1b27b634d7088a58e24208929e74799d

122

Capítulo 4: Búsquedas y filtros A lo largo de los capítulos anteriores, hemos desarrollado una aplicación totalmente funcional. Esta aplicación realiza las operaciones CRUD básicas sobre un conjunto de recursos, contactos en este caso, que son puestos a disposición por medio de un servicio web. Sin embargo, siempre debemos procurar lograr una interface lo más usable posible, por lo que, como habíamos mencionado en el capítulo anterior, vamos a agregar nueva funcionalidad y mejorar cuestiones estéticas que elevarán grandemente el valor de nuestra aplicación. Una característica que será de enorme utilidad para los usuarios, sobre todo si el número de contactos es grande, es la posibilidad de buscar y/o filtrar contactos. Vamos a ver como implementar dicha característica. En el archivo contactos.html, cuando explicamos el concepto de data binding, ubicamos un cuadro de texto con las siguientes líneas: 1 2 3 4



Vamos a reemplazar dichas líneas por la siguiente: 1

Estamos utilizando un nuevo componente, una barra de búsqueda. Debemos agregar en contactos.ts, la variable que se está enlazando al modelo. 1

public terminoBusqueda: String = '';

Si ejecutamos ionic serve, el resultado será el siguiente:

124

Capítulo 4: Búsquedas y filtros

Barra de búsqueda

Muy bien. Ahora debemos construir la funcionalidad de búsqueda. Para ello vamos a introducir un nuevo concepto.

Pipes Las pipes son clases que permiten formatear datos para ser presentados en una interface. Una pipe toma un dato de entrada, lo transforma según se necesita, y devuelve el dato transformado para su visualización. Angular, el framework detrás de IONIC, viene con una serie de pipes predefinidas que son de utilidad. Veamos algunos ejemplos. Supongamos que en alguna de las clases que corresponden a nuestras páginas definimos una variable denominada fechaNac: 1

public fechaNac = new Date(1982, 4, 12);

Y luego en la correspondiente plantilla incluimos: 1

{{fechaNac}}



La salida será similar a esta:

125

Capítulo 4: Búsquedas y filtros

Fecha sin formatear

Podemos utilizar una de las pipes de IONIC, denominada Date, para formatear la salida: 1

{{fechaNac | date}}



Noten la sintaxis. El dato que se quiere formatear, es seguido del operador | y de la pipe. Ahora la salida es mucho más agradable:

Fecha formateada

Pero las pipes son aún más potentes, ya que pueden recibir parámetros. Por ejemplo: 1

{{fechaNac | date:"dd/MM/yyyy"}}



La salida ahora es:

Pipe con parámetro

Ahora bien. No estamos limitados a las pipes predefinidas. Podemos construir las nuestras, y eso es precisamente lo que haremos para implementar la función de búsqueda. Creemos un directorio denominado pipes a la misma altura que el directorio pages. Dentro de pipes, añadamos un archivo llamado contact-name-pipe.ts con el siguiente contenido. Del libro:

Capítulo 4: Búsquedas y filtros

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

126

import { PipeTransform, Pipe } from '@angular/core'; @Pipe({ name: 'contactNamePipe' }) export class ContactNamePipe implements PipeTransform { transform(contacts: Array, searchTerm: string) : Array { if (contacts == null) { return []; } if (searchTerm == null) { return contacts; } return contacts.filter( contact => { return `${contact.nombre}`.toLowerCase().indexOf(searchTerm) > -1; }); } }

Gist⁶¹ Lo primero que tenemos es el import de los componentes necesarios para trabajar con pipes: 1

import { PipeTransform, Pipe } from '@angular/core';

Luego viene la sección: 1 2 3

@Pipe({ name: 'contactNamePipe' })

Allí estamos definiendo el nombre de la pipe, que es el que se utilizará al momento de transformar datos. En este caso, para llamar a la pipe utilizaremos: 1

{{dato | contactNamePipe}}

Ya que ese es el nombre que definimos. Luego viene el export de la clase: ⁶¹https://gist.github.com/vihugarcia/138e12432f94fb0618b674f78ac0bb3d

Capítulo 4: Búsquedas y filtros

1

127

export class ContactNamePipe implements PipeTransform

ContactNamePipe es el nombre de la clase que se deberá usar en los imports. Como nuestra pipe implementa la interface PipeTransform, debe forzosamente definir un método transform. En este caso nuestra pipe actúa sobre un arreglo (nuestra lista de contactos) y recibe como parámetro una cadena (nuestro término de búsqueda), y devuelve un arreglo (la lista filtrada) transform(contacts: Array, searchTerm: string) : Array Si la lista recibida es nula, se devuelve un arreglo vacío: 1 2 3

if (contacts == null) { return []; }

Si el término de búsqueda es vacío, se devuelve la lista completa: 1 2 3

if (searchTerm == null) { return contacts; }

Si tenemos una lista y un término de búsqueda, entonces debemos aplicar el filtrado. 1 2 3

return contacts.filter( contact => { return `${contact.nombre}`.toLowerCase().indexOf(searchTerm) > -1; });

El método filter en un arreglo, es un método muy útil que recibe una función como parámetro, y devuelve los elementos para los cuales la función evalúe a verdadero. La función recibe como parámetro cada uno de los elemento del arreglo. Dentro de la función, el nombre se pasa a minúsulas, y se busca mediante el método indexOf la ocurrencia de la cadena correspondiente al término de búsqueda. indexOf, si encuentra la cadena, devolverá la posición a partir de la cual se encuentra dicha cadena dentro del nombre. En caso de no encontrarse, devuelve -1. Al preguntar si el valor de indexOf es mayor que -1 entonces, obtendremos verdadero cuando la cadena se encuentre, y por lo tanto el contacto en cuestión será devuelto como parte del arreglo filtrado. La pipe está definida ya, pero aún no podemos utilizarla. Necesitamos declararle en app.module.ts de lo contrario obtendremos un error. Primero agregamos el import:

128

Capítulo 4: Búsquedas y filtros

1

import { ContactNamePipe } from '../pipes/contact-name-pipe';

Luego debemos incluir la clase en la sección declarations. No es necesario incluirla en entryComponents porque no se trata de un elemento de visualización. Ahora si, podemos ir a contactos.html y modificar la sección donde se itera a través de los elementos: 1 2

Ahora podemos poner en funcionamiento nuestra opción de búsqueda:

Búsqueda

Búsqueda

¡Magnífico! Hemos agregado una pieza de funcionalidad que sin dudas los usuarios apreciarán. Como un detalle final, si les desagrada el texto que aparece como placeholder (Search) este puede ser fácilmente modificado de la siguiente manera:

129

Capítulo 4: Búsquedas y filtros

1 2



Con lo que verán:

Placeholder

Gravatar Vamos a ocuparnos ahora de una cuestión estética. Si recuerdan del comienzo del capítulo 2, cuando diseñamos la pantalla inicial llegamos a algo así:

130

Capítulo 4: Búsquedas y filtros

Diseño de pantalla

Tenemos una imagen que se muestra para cada contacto. Las imágenes que vamos a mostrar para cada contacto es un gravatar. Para los que no están al tanto de lo que es un gravatar, pueden visitar el sitio http://en.gravatar.com/ Para darnos una rápida idea de lo que queremos lograr, vamos a ubicar una placeholder como imagen. En la página de detalle de contacto, justo encima del nombre, coloquemos lo siguiente: 1

El sitio placehold.it sirve imágenes del tamaño solicitado para que actúen como placeholders. Si vamos al detalle de un contacto, veremos:

131

Capítulo 4: Búsquedas y filtros

Placeholder

Muy bien. Ahora debemos incorporar a nuestro proyecto el paquete npm crypto-md5. Desde la raíz de nuestro proyecto ejecutemos: npm install crypto-md5 –save El proceso puede demorar un par de minutos. Ahora podemos usar el nuevo paquete desde cualquier lugar que lo necesitemos con el siguiente import: import md5 from ‘crypto-md5’; Vamos a agregar dicho import en el archivo contacto.ts. A continuación, definamos una variable que contendrá la imagen. 1

profilePicture: any;

Ahora modifiquemos la definición del constructor de la siguiente manera:

132

Capítulo 4: Búsquedas y filtros

1 2 3 4 5 6

constructor(public navCtrl: NavController, public navParams: NavParams) { this.contacto = this.navParams.get('contacto'); this.profilePicture = "https://www.gravatar.com/avatar/" + md5(this.contacto.email.toLowerCase(), 'hex'); }

En el archivo contacto.html, cambiemos la disposición de la ion-card como sigue: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16





{{contacto.nombre}}

{{contacto.direccion}}

{{contacto.telefono}}

{{contacto.email}}



Si ahora vamos a ver un contacto, y en caso de que este tenga un gravatar, veremos algo así:

Gravatar

133

Capítulo 4: Búsquedas y filtros

En caso de que el contacto no tenga un gravatar (al menos asociado a la dirección de email que se introdujo) se verá una imagen genérica:

Sin gravatar

Para finalizar, vamos a darle un poco de estilo a la imagen. Editemos el archivo contacto.scss de la siguiente manera: 1 2 3 4 5 6 7

page-contacto { ion-avatar > img { margin: auto; width: 100px !important; height: auto !important; } }

El resultado es el siguiente:

134

Capítulo 4: Búsquedas y filtros

Gravatar con estilo

Capítulo 5: Corriendo la aplicación desde el emulador Hasta ahora hemos venido corriendo nuestra aplicación desde el navegador. En este capítulo, veremos cómo ejecutar nuestra aplicación en un emulador, para poder apreciar cómo se vería en un ambiente real. Puntos importantes para señalar: • Para poder realizar la emulación (y posteriormente para generar la aplicación que será subida a las app store), debemos tener en cuenta la plataforma de destino. • Para poder compilar el proyecto para IOS, necesitaremos una MAC. Alternativamente podremos utilizar algún ambiente de desarrollo en la nube. Vamos a trabajar con Android. Para esto debemos tener instalado el SDK de Android. Desde la raíz de nuestro proyecto, ejecutemos el siguiente comando: ionic run android Después de unos minutos podremos ver el emulador en acción. Y…la aplicación devuelve un error.

Capítulo 5: Corriendo la aplicación desde el emulador

136

Error emulador

Es un excelente momento para introducir el proceso de depuración de aplicaciones en ejecución. ¿Cómo podemos depurar una aplicación que se está ejecutando en el emulador? Aquí es donde vemos la ventaja de utilizar Google Chrome. Si abrimos una nueva pestaña y escribimos en la barra de direcciones: chrome://inspect podremos ver la siguiente pantalla:

Capítulo 5: Corriendo la aplicación desde el emulador

Chrome Inspect

Si hacemos clic en inspect, se abrirá una nueva ventana de herramienta de desarrollador:

Herramientas de desarrollador

Si nos dirigimos a la pestaña Console, podemos apreciar el siguiente error:

137

Capítulo 5: Corriendo la aplicación desde el emulador

138

Error de consola

Vemos que se ha rechazado la conexión a: http://localhost/api-cmgr/contactos-api.php/contactos Y aquí debemos señalar un detalle muy importante. En este caso, estamos consumiendo datos de un web service que montamos en nuestra propia máquina. Esto funciona perfectamente cuando corremos la aplicación en el navegador utilizando ionic serve, pero al correr desde un emulador, este no tiene idea de quién es localhost. Debemos utilizar una dirección ip que pueda ser encontrada. Por lo tanto, para poder correr nuestra aplicación desde el emulador debemos hacer un cambio en app.settings.ts. Como primera medida, vamos a obtener la dirección ip local de nuestra máquina. Esto dependerá del sistema operativo que tengamos. En mi caso es 10.147.64.246 Editemos app.settings.ts de la siguiente manera: 1 2 3 4 5 6

export class AppSettings { public static get API_ENDPOINT() { //return 'http://localhost/api-cmgr/contactos-api.php'; return 'http://10.147.64.246/api-cmgr/contactos-api.php'; } }

Ejecutemos nuevamente ionic run android. Deberíamos ver la aplicación funcionando:

Capítulo 5: Corriendo la aplicación desde el emulador

App corriendo en el emulador

139

Capítulo 5: Corriendo la aplicación desde el emulador

140

App corriendo en el emulador

Fantástico.

APK Observemos el contenido del directorio de nuestro proyecto. Como resultado de haber ejecutado el comando ionic run android, vemos que hay presente un directorio platforms, y dentro de este un directorio android. Este directorio android contiene una carpeta denominada build, y dentro de ella hay otra llamada outputs.

Capítulo 5: Corriendo la aplicación desde el emulador

141

Finalmente, dentro de esta carpeta outputs tenemos un directorio llamado apk. Hay dos archivos con extensión apk dentro de este directorio: android-debug.apk y android-debug-unaligned.apk Aunque se trata de archivos .apk, no es ninguno de ellos un archivo apto para poder subirlo a una playstore. Tenemos que realizar un proceso para poder publicar nuestra aplicación.

Publicando una aplicación para Android Como primera parte del proceso, vamos a editar el archivo config.xml que se encuentra en la raíz de nuestro proyecto. La información que se encuentre en este archivo será tenida en cuenta por el proceso de creación de la versión release, por eso debemos ajustarlo. En el encontraremos estas líneas: 1 2 3

Debemos cambiar el valor de id a algo único que esté relacionado con nosotros. Podría ser por ejemplo algo basado en el nombre de nuestra compañía o nuestro sitio personal. Por ejemplo: com.misuperempresa.contactmgrapp El valor de versión puede quedar como está, pero hay que tener en cuenta que cada vez que subamos una actualización de nuestra aplicación deberemos incrementar dicho valor. Después tenemos las siguientes líneas: 1 2 3

An awesome Ionic/Cordova app.

Ionic Framework Team

Que también deberemos ajustar. Muy bien. Hecho esto podemos comenzar a trabajar en la línea de comandos. Primero ejecutamos desde la raíz de nuestro proyecto: ionic build android –release Esto generará una versión release en el directorio outputs. El proceso puede demorar varios minutos. Cuando concluya verá algo similar a esto:

Capítulo 5: Corriendo la aplicación desde el emulador

142

Build de la aplicación

Y podrán encontrar un archivo denominado android-release-unsigned.apk Esta es la versión release de nuestra apk, pero como su nombre lo indica, no está firmada aún. Para firmar una apk, debemos generar una key privada. Esta generación sólo tendremos que realizarla una vez. Luego podremos usar esa misma key para firmar nuevamente cada actualización de nuestra app. De esto se deduce que si por alguna razón llegamos a perder el archivo .keystore que se generará, no seremos ya capaces de enviar actualización a la app store. El comando para generar la key tiene la forma: Del libro: 1 2

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

Gist⁶² Debemos sustituir my-release-key.keystore por el nombre que deseamos tenga nuestro archivo, por ejemplo contactmgrapp.keystore y alias_name por un nombre que haga referencia a nuestra aplicación, por ejemplo contactmgrapp. Se inciará un asistente que nos irá pidiendo datos. En primer lugar una contraseña para el almacén de claves. Una vez finalizado el asistente, tendremos el archivo .keystore generado. ⁶²https://gist.github.com/vihugarcia/bec8b2483521886a85e953d26f081fa8

Capítulo 5: Corriendo la aplicación desde el emulador

143

Archivo de claves generados

Ahora debemos firmar nuestra aplicación utilizando el archivo .keystore generado. Para simplificar el proceso, vamos a copiar el archivo android-release-unsigned.apk del directorio apk al mismo lugar donde está el archivo .keystore. Luego ejecutamos: 1 2 3

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore HelloWorld-release-unsigned.apk alias_name

Necesitamos reemplazar my-release-key.keystore por el nombre de nuestro archivo .keystore, por ejemplo contactmgrapp.keystore y HelloWorld-release-unsigned.apk por el nombre de nuestro archivo unsigned, en este caso android-release-unsigned.apk. También debemos cambiar alias_name por el nombre de nuestra app, en este caso contactmgrapp. El último paso, es correr la herramienta de zip align, que tiene como función optimizar la aplicación para que pueda ser subida a las app store. 1 2

ruta/a/android/sdk/build-tools/version/zipalign -v 4 HelloWorld-release-unsigned.apk HelloWorld.apk

Lógicamente debemos reemplazar ruta/a/android/sdk/build-tools/versión/zipalign por la ruta correcta para nuestra configuración. HelloWorld-release-unsigned.apk por android-release-unsigned.apk y HelloWorld.apk por el nombre de la aplicación. En mi caso el comando queda como: c:/Android/sdk/build-tools/23.0.2/zipalign –v 4 android-release-unsigned.apk contactmgrapp.apk Si todo ha ido bien, debemos encontrar ahora un archivo contactmgrapp.apk en nuestro directorio:

apk lista para subir

¡Magnífico! Ya tenemos nuestra app firmada y alineada, lista para ser enviada a las app store. El proceso para subir nuestra app es sencillo. Para el caso de la app store de Google, primero debemos visitar la Consola de Desarrolladores de Google. Si no contamos con una cuenta, deberemos crear una. El registro no es gratuito, tiene un costo de 25 dólares. El costo del registro en Apple es de 99 dólares. ¡Ya están listos para sacudir el mundo!

Capítulo 5: Corriendo la aplicación desde el emulador

144

Palabras de despedida Eso es todo por ahora en lo que respecta a este libro. Tal vez no se hayan percatado aún, pero han adquirido una gran cantidad de conocimientos que les permitirán desarrollar aplicaciones móviles de gran calidad. Y remarco el por ahora, porque este libro seguirá creciendo y actualizándose con nuevo material para que ustedes puedan obtener el máximo provecho. Espero por sobre todo haber despertado su curiosidad y sus ansias de aprender. No hay límites para lo que puedan lograr. ¡Hasta pronto!

Capítulo 6: Apéndices ## Instalando xampp en Linux En primer lugar, debemos dirigirnos a la página de descargas de xampp, donde tendremos que elegir la versión correcta para nuestro sistema operativo: [página de descargas de xampp] (https://www.apachefriends.org/es/download.html) Cuando hablo de la versión correcta para el sistema operativo, me refiero a que debemos saber si el mismo es de 32 o 64 bits, de lo contrario nos encontraremos con un error que puede resultar difícil de resolver. Permítame explicarle a qué me refiero. Una vez descargada la versión, y siguiendo lo establecido en las [FAQs de xampp para Linux] (https://www.apachefriends.org/es/faq_linux.html), debemos ejecutar desde la consola en primer lugar:

chmod

Lo que estamos haciendo aquí es asignar los permisos correctos (755) al archivo ejecutable, de lo contrario la instalación no podrá llevarse a cabo. Podemos ver que el ejecutable corresponde a la versión para 64 bits (como podemos ver por la partícula x64 en el nombre). Luego, debemos iniciar la ejecución del archivo con:

sudo

Deberá introducir la contraseña de administrador, y presionar enter…y aquí es dónde puede presentarse un problema: Si observa el siguiente error: 1

./xampp-linux-x64-5.6.3-0-installer.run: Syntax error: “(” unexpected

146

Capítulo 6: Apéndices

Que como podemos ver no es para nada descriptivo, entonces probablemente sea debido a que está tratando de instalar la versión de 64 bits sobre un sistema operativo de 32 bits. Esto puede ocurrir si hemos SUPUESTO erróneamente que nuestra arquitectura es de 64 bits. Para comprobarlo, podemos escribir: 1

uname -m

uname

Si, como observamos arriba, obtenemos como respuesta i686, entonces eso nos mostrará que hemos descargado e intentado ejecutar la versión equivocada. En ese caso debemos dirigirnos nuevamente a la página de descargas y a obtener la versión correcta, luego procederemos nuevamente a asignar los permisos adecuados:

chmod

A continuación ejecutamos 1

sudo ./xampp-linux-5.6.3-0-installer.run

Si todo ha ido bien aparecerá el asistente de instalación:

147

Capítulo 6: Apéndices

Instalación

Muy bien, después de presionar Next unas cuantas veces, llegaremos al final:

Fin de instalación

Al presionar Finish, aparecerá la interfaz gráfica de xampp:

148

Capítulo 6: Apéndices

Interface gráfica

Desde la pestaña Manage Servers podremos acceder a los Servicios:

Servicios

149

Capítulo 6: Apéndices

Luego de verificar que Apache y MySQL están activos, podrá dirigirse al navegador e ingresar la dirección localhost/phpmyadmin

PhpMyAdmin

¡Perfecto! ya tenemos un servidor con Apache y MySQL funcionando.