Ionic Framework, PouchDB y Cloudant: la combinación perfecta totalmente expuesta Parte 04


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

En esta parte, haremos una introducción a lo que creo que es lo más potente que se ha incluido en Ionic, UI-Router. Hay otros componentes que hacen lo mismo, pero creo que lo más importante aquí es el concepto de estados.

Un estado es un conjunto de vista, controlador y modelo que hace una labor específica en nuestro programa. Tenemos que pensar en una labor muy simple para ver la potencia de este concepto. Por ejemplo, listar todos los correos que tenemos, es un estado, o ver un email en particular es otro estado. Mientras más simple que sea, más reutilizable será el estado.

Ahora, quizá me adelante, pero es posible que un conjunto de estados compartan una base común, por ejemplo, la lista de correos tienen que mostrarse dentro de una ventana con un menú de controles. Para eso, existe el concepto de estados abstractos.

Primero vamos por lo más simple, agregemos los estados. En nuestro caso, es simple pues tenemos solamente un estado que lista todas las noticias. Así que modificamos el archivo app.js para definir el estado.

angular.module('starter', ['ionic','controllers'])

.run(function($ionicPlatform,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})
.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('news', {
      url: "/",
      views: {
          "home":{
              templateUrl: "templates/news.html",
              controller: "newsController as news"
          }
      }
  })
  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/');
});

Desde la línea 15 se puede ver la definición del estado. En nuestro caso, hemos llamado al estado con el nombre de “news” y le hemos asignado el url “/”, también se le ha asignado una vista llamada “home” que tiene un template llamado news.html en la carpeta templates y tiene asignado el controlador newsController al que le hemos puesto un alias news. ¿Qué significa todo esto?

Traducción: Al invocarse el estado “news”, AngularJS va a tomar el archivo news.html y le va a asignar el controlador newsController para ejecutarlo. Simple.

En la línea 27 se pone que la aplicación tomará la ruta “/” como ruta por defecto, lo que llevará a la aplicación al estado “news”.

Veamos el archivo news.html.

<ion-view view-title="Noticias">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Ionic Blank Starter>/h1>
 </ion-header-bar>
 <ion-content>
 <p> Hay {{notas.length}} noticias</p>
 <div class="list"
 ng-repeat="noticia in notas">
 <div class="card">
 <div class="item item-divider">
 {{noticia.doc.fecha}}</br>{{noticia.doc.titular}}
 </div>
 <div class="item item-text-wrap">
 <b>{{noticia.doc.resumen}}</b>
 </div>
 <div class="item item-divider">
 Autor: {{noticia.doc.autor}}
 </div>
 </div>
 </div>
 </ion-content>
</ion-view>

El contenido es casi todo lo que antes había en el archivo index.html pero con las tags iniciales en lugar de las <ion-content< que habían antes. Esto le indica a AngularJS que estamos definiendo una vista.

Lo que hemos hecho es separar la vista y asignarle un controlador de forma programática lo cual es muy conveniente para poder reutilizar el código.

Ahora, para que todo esto funcione falta indicarle a AngularJS donde vamos a mostrar el resultado del estado, y para eso veamos como queda el archivo “index.html”

<!DOCTYPE html>
<html>
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
 <title></title>
 <!-- compiled css output -->
 <link href="css/ionic.app.css" rel="stylesheet">
 <!-- ionic/angularjs js -->
 <script src="lib/ionic/js/ionic.bundle.js"></script>
 <!-- cordova script (this will be a 404 during development) -->
 <script src="cordova.js"></script>
 <!-- your app's js -->
 <script src="js/services.js"></script>
 <script src="js/controllers.js"></script>
 <script src="js/app.js"></script>
 <script src="lib/pouchdb/dist/pouchdb.js"></script>
 </head>
 <body ng-app="starter">
 <ion-nav-view name="home"</ion-view>
 </body>
</html>

En la línea 20 notarán que hemos reemplazado toda la parte que mostraba las noticias por el tag . Aquí le estamos diciendo a AngularJS que queremos que se muestre la vista llamada “home”, que la hemos definido antes en el archivo app.js.

Traducción, AngularJS cargará news.html, le asignará el controlador newsController y la mostrará en el tag denominado “home”.

¿Cuál es la ventaja de todo eso? la respuesta es separación de responsabilidades. El diseñador Web podrá trabajar en la parte de la presentación con el html y el CSS y colores y demás, mientras que el programador podrá trabajar en el controlador, y si por ahí hay nuevas versiones de la vista o el controlador, pues simplemente se cambia en la definición de la vista y nuestra aplicación no sufrirá el cambio.

Ahora bien, regresemos a la pregunta inicial que recibí y que generó todos estos posts. La pregunta fue que si tenemos una lista de objetos de la base de datos, como hacemos para mostrar los detalles de estos objetos. Pues bien, en nuestro ejemplo, ya estamos recuperando todos los datos así que podríamos tener los detalles ocultos mostrando sólo las cabeceras. Para eso podemos usar la ayuda de Angular-bootstrap, así que instalemos rápidamente.

Paso 1: agregar el CSS en el index.html

<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">

Paso 2: instalar el componente angular-bootstrap con bower

bower install angular-bootstrap --save

Paso 3: agregar la referencia en el archivo index.html

<script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>

Con el angular-bootstrap listo, sólo nos queda modificar el template news.html para utilizar el componente accordion:

<ion-view view-title="Noticias">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Ionic Blank Starter</h1>
 </ion-header-bar>
 <ion-content>
 <p> Hay {{notas.length}} noticias</p>
 <accordion close-others="oneAtATime">
 <accordion-group ng-repeat="noticia in notas"
 heading='{{noticia.doc.fecha}}-{{noticia.doc.titular}}'>
 <b>{{noticia.doc.resumen}}>/b>;
 </br>
 Autor: {{noticia.doc.autor}}
 </accordion-group>
 </accordion>
 </ion-content>
</ion-view>

Y obtendremos algo como esto, donde haciendo click en los títulos se despliega el contenido de la noticia:
gen21

Todo bien, pero hay otra alternativa, sobre todo si es que queremos ver los detalles de la noticia de forma mas flexible sobre todo si hay mucho texto, pues en ese caso deberíamos tener una lista de noticias y al seleccionar un titular, ir a otra pantalla para ver solamente esa noticia en toda la pantalla.

Para esto debemos crear otro estado a nuestra app, así que hagámoslo. En el archivo app.js agregamos ese nuevo estado y nuestro nuevo app.js lucirá así:

angular.module('starter', ['ionic','controllers','ui.bootstrap'])

.run(function($ionicPlatform,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})
.config(function($stateProvider, $urlRouterProvider) {
    $stateProvider
        .state('news', {
        url: "/",
        views: {
            "home":{
                templateUrl: "templates/news.html",
                controller: "newsController as news"
            }
        }
    })
        .state('detail', {
        url: "/detail/:id",
        views: {
            "home":{
                templateUrl: "templates/detail.html",
                controller: "detailController as detail"
            }
        },
        resolve:{
            detail: function($stateParams){
                return $stateParams.id;
            }
        }
    })
    $urlRouterProvider.otherwise('/');
});

Los puntos de interés son la línea 27 donde fijamos el URL y también indicamos que el url vendrá con el parámetro ‘:id’; la línea 34, donde estamos fijando que la variable “detail” será pasada al controlador y tendrá el valor del parámetro ‘id’.
Ahora veamos como ha quedado el controlador nuevo en el archivo controller.js:

.controller('detailController',function($scope,db,detail){
	db.get(detail).then(function(doc){
        $scope.event=doc;
    })
})

Prestemos atención en la línea 11 que estamos pasando la variable ‘detail’ que definimos en la definición del estado y usamos ese valor para recuperar el objeto desde la base de datos. El valor del parámetro ‘id’ debe ser el id del documento que hemos elegido. La función db.get la estamos invocando desde el servicio que hemos definido en el archivo services.js así que tenemos que agregar este a la definición:

return {
  init: function(){
        db.replicate.from(remote,{live:true,retry:true})
        .on('paused',function(info){
            db.allDocs({startkey:'news_\uffff',endkey:'news_',descending:    true,include_docs:true})
            .then(function(result){
                  $rootScope.$broadcast('refrescar',result.rows);
            });
          });
        },
   get: function(id){
        return db.get(id);
   }
}

El documento resultante lo grabaremos en la variable de escope event para usarla en el template para mostrar.

Para verificar esto, veamos como quedan los templates.
Primero, index.html:

<!DOCTYPE html>
<html>
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
 <title></title>
 <!-- compiled css output -->
 <link href="css/ionic.app.css" rel="stylesheet">
 <link href="lib/angular-bootstrap/ui-bootstrap-csp.css" rel="stylesheet">
 <link href="css/bootstrap.css" rel="stylesheet">
 <!-- ionic/angularjs js -->
 <script src="lib/ionic/js/ionic.bundle.js"></script>
 <script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>
 <!-- cordova script (this will be a 404 during development) -->
 <script src="cordova.js"></script>
 <!-- your app's js -->
 <script src="js/services.js"></script>
 <script src="js/controllers.js"></script>
 <script src="js/app.js"></script>
 <script src="lib/pouchdb/dist/pouchdb.js"></script>
 </head>
 <body ng-app="starter">
 <ion-nav-bar class="bar-positive">
 <ion-nav-back-button class="button-clear">
 <i class="ion-arrow-left-c"></i> Regresar
 </ion-nav-back-button>
 </ion-nav-bar> 
 <ion-nav-view name="home"></ion-view>
 </body>
</html>

Aquí solamente hemos añadido una cabecera para facilitar la navegación en las líneas 23 a la 27.
Ahora veamos el archivo donde mostramos la lista de noticias: ‘news.html’

<ion-view view-title="Noticias">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Noticias</h1>
 </ion-header-bar>
 <ion-content>
 <ion-list>
 <ion-item ng-repeat="noticia in notas" 
 ui-sref='detail({id:"{{noticia.id}}"})'>
 {{noticia.doc.fecha}}-{{noticia.doc.resumen}}</ion-item>
 </ion-list>
 </ion-content>
</ion-view>

Notaremos en las líneas 6 a la 10 que hemos incluido una lista y cada item tiene una directiva nueva: ui-sref que se encarga de llevarnos al estado que se indica cuando se hace click en el item; además le estamos pasando el parámetro id con el valor del id del documento.
Y ahora, para mostrar la noticia usamos el template detail.html que lucirá así:

<ion-view view-title="Noticia">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Detalle</h1>
 </ion-header-bar>
 <ion-content>
 <div class="card">
 <div class="item-divider">
 <h2>{{event.fecha}} {{event.titular}}</h2>
 </div>
 <div class="item-body">
 {{event.resumen}}
 </div>
 <div class="item-divider">
 <h2>{{event.autor}}</h2>
 </div>
 </div>
 </ion-content>
</ion-view>

Como ven, los detalles del documento recuperado los leemos usando la variable ‘event’.

Y el resultado es una app con dos estados: news que muestra la lista de noticias disponibles y al hacer click en alguna llegamos al segundo estado llamado detail donde se muestra la noticia seleccionada completa, para lo cual recibe un parámetro que nos sirve para recuperar los datos desde la base de datos.

gen22

gen23

Seguramente notaremos que para el ejemplo que he desarrollado, hacer otra llamada a la base de datos puede parecer un desperdicio, pero lo que he tratado de hacer es mostrar como pasar parámetros y como utilizar los estados.Lo mas importante es que tenemos dos estados que podemos reutilizar en el resto de nuestra app.

NOTA: Parece que tengo un lector que ha sido responsable y ha seguido todos los pasos. Muchas gracias a Tercio Santos. Al parecer tiene un problema con el app y luego de pruebas en mi equipo, puedo decir que no hay problema con el código. Sólo por si acaso, recomiendo la instalación del plugin Cordova Whitelist:

ionic plugin add cordova-plugin-whitelist

Sucede que en las últimas versiones de Android, es necesario especificar los lugares donde el app puede ingresar y para eso se puede usar whitelist. Si es que no está, Android puede bloquear el acceso a internet de su app, por eso es mejor que esté aunque no lo utilicen todavía. Para una implementación en producción si es necesario configurar bien esos permisos. Ya llegaremos a eso.
Y para que vean que el código camina, aquí va un pantallazo. Recuerden, corre el app la primera vez con red, se muestran los datos, y listo, ya pueden ponerlo en modo avión si quieren y el app tendrá que mostrarle los datos.


  1. Reblogged this on Dinesh Ram Kali..

  2. David Mendoza

    Hola muy buen tutorial he seguido todo y ha funcionado hasta la version del detalle en donde me ha salido un error —db.get is not a function— y no me muestra el detalle de la noticia puedes ayudarme o si me puede ayudar por donde ir
    (ionic cli 1.6.4 / ionic App lib 0.3.8)

    • Pasame un zip con tu código a mi correo vpease@yahoo.com

      • David Mendoza

        Gracias ya te lo envie a tu correo.

  1. 1 Ionic Framework, PouchDB y Cloudant: la combinación perfecta totalmente expuesta | Víctor Pease

    […] Part 4 […]




Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s



%d bloggers like this: