Si ayer veíamos como funcionaba el routing con AngularJS, hoy vamos a ver como aplicar la misma idea en Phonegap con Ionic (que recordemos, trabaja sobre Angular), pero trabajando con estados.
Angular, de hecho, dispone también de directivas para gestionar estados a través del módulo ui-router, con “$urlRouterProvider”, “$stateProvider”. Ionic sencillamente trabaja encima de estas, aplicando una capa extra de funcionalidades.
Primeros pasos
Creamos un proyecto desde cero, y en nuestro caso añadiremos la plataforma Android:
$ ionic start ionic-nav-project blank
$ cd ionic-nav-project
$ ionic platform android
$ ionic build android
Directiva < ion-nav-view>
De forma similar a Angular con < ui-router>, debemos indicar en index.html donde queremos que se produzca la navegación, para lo que usaremos la directiva < ion-nav-view>
El < ion-nav-view> es nuestro container, la directiva que el router de Ionic buscará para insertar en ella los templates. Entre otras cosas, soporta herencia y vistas nominales.
Está construido sobre la directiva de Angular < ui-router>, proporcionando además animaciones, histórico, etc.
Llamaremos a nuestra aplicación “navApp”, por lo que un index.html básico quedaría:
index.html:
<!DOCTYPE html>
<html ng-app="navApp">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<title>Nav App</title>
<link href="lib/ionic/css/ionic.css" rel="stylesheet">
<link href="css/style.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/app.js"></script>
</head>
<body>
<ion-nav-view></ion-nav-view>
</body>
</html>
Definición de rutas y estados en JS
En Angular, ahora utilizaríamos $urlRouteProvider y $stateProvider para definir los templates que se incrustarían en la vista en función de las urls.
El funcionamiento con Ionic es similar, con la diferencia de que aquí el módulo que cargamos es el de Ionic, que monta su propia máquina de estados interna para poder determinar, con facilidad, en que vista estamos, de dónde venimos y a dónde podemos ir. Para ello utiliza los providers:
- $stateProvider: Nos permite declarar estados, que irán vinculados a una url, y que contendrán un template, o una url a un template.
- $urlRouteProvider: Se utiliza internamente cada vez que se especifica una url en la configuración de estados. Tiene la responsabilidad de observar $location, y en caso de que esta cambie, analiza una serie de reglas una por una hasta que encuentra una coincidencia.
Veamos como definiríamos un estado, y como usaríamos $urlRouterProvider para establecer la ruta por defecto.
app.js:
var app = angular.module('navApp', ['ionic'])
app.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/')
$stateProvider.state('home', {
url: '/',
template: '<p>Welcome to Nav App home page!</p>'
})
})
Otra opción, más habitual para el estado, es en lugar de incrustar el código del template, definir una url a un template:
$stateProvider.state('home', {
url: '/',
templateUrl: 'home.html'
})
DETALLE I: Si el estado contiene una URL a un template, Angular buscará primero en $templateCache, y en caso de no encontrar el template, hará una petición al servidor y lo guardará en la misma.
DETALLE II: Podemos utilizar la etiqueta type=”text/ng-template” y el id con el nombre de la página para que el template sea añadido a la cache, y evitemos tener que hacer el request.
Navegación con Tabs
Veamos como podemos aplicar ahora estos conceptos de navegación.
Layout
Creamos un layout con un par de Tabs:
index.html:
<!DOCTYPE html>
<html ng-app="navApp">
<head><!-- ... --></head>
<body>
<ion-nav-bar class="bar-positive">
</ion-nav-bar>
<ion-tabs class="tabs-positive">
<ion-tab icon="ion-home" ui-sref="home">
<ion-nav-view name="home"></ion-nav-view>
</ion-tab>
<ion-tab icon="ion-information" ui-sref="info">
<ion-nav-view name="info"></ion-nav-view>
</ion-tab>
</ion-tabs>
</body>
</html>
Explicando el código:
- < ion-nav-bar>: Hemos añadido una barra de navegación, donde se mostrará el título de nuestra vista.
- < ion-tabs>: Estamos utilizando la directiva ion-tabs de Ionic, que nos permite crear un conjunto de tabs a través de la directiva:
- < ion-tab>: Esta directiva crea un content, únicamente existente cuando dicho tab está activo. Además cada Tab tiene su propio histórico de navegación.
- Dado que el content solo existe para su propio tab, es dentro del mismo donde insertamos la directiva < ion-nav-view>. Al haber más de una directivas de ion-nav-view, al mismo nivel jerárquico, tenemos que darles un nombre a cada una para evitarle conflictos al router, lo que hacemos con name=”nombreDeMiVista”.
- ui-sref: esta directiva es similar a un href, indicándonos el estado al que queremos que se produzca una transición.
Javascript
Obviamente, ahora necesitamos tener 2 estados, por lo que nuestro JS quedará:
app.js:
var app = angular.module('navApp', ['ionic'])
app.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider.state('home', {
url: '/',
views: {
home: {
templateUrl: 'home.html'
}
}
});
$stateProvider.state('info', {
url: '/info',
views: {
info: {
templateUrl: 'info.html'
}
}
});
});
Cabe destacar que hemos añadido la clave views (un objeto con todas nuestras subvistas para dicho estado). Esto se debe al uso de vistas nominales en ion-nav-view.
Cuando añadimos una vista a nuestro array views, su contenido es equivalente al que habríamos tenido en el estado en caso de no utilizar vistas nominales, por lo que podremos añadir atributos como:
- template
- templateUrl
- controller
- etc
Templates
Por supuesto, tendremos que crear 2 templates, uno para home.html y otro para index.html.
Para ello, podemos añadirlas en index.html, al final del body como:
<script type="text/ng-template" id="home.html">
<ion-view title="Home">
<ion-content padding="true">
<h2>Home Page</h2>
<p>Here's the main route for the app.</p>
</ion-content>
</ion-view>
</script>
<script type="text/ng-template" id="info.html">
<ion-view title="Info">
<ion-content padding="true">
<h2>Info page</h2>
<p>Here you are some more info</p>
</ion-content>
</ion-view>
</script>
O bien, crearlas en archivos separados, como:
home.html:
<ion-view title="Home">
<ion-content padding="true">
<h2>Home Page</h2>
<p>Here's the main route for the app.</p>
</ion-content>
</ion-view>
info.html:
<ion-view title="Info">
<ion-content padding="true">
<h2>Info page</h2>
<p>Here you are some more info</p>
</ion-content>
</ion-view>
Personalmente me gusta más esta segunda aproximación por que permite separar mejor el contenido.
A continuación podemos ver el resultado actual de nuestra aplicación:
Estado de navegación en el tab
Ahora que disponemos de un par de tabs, vamos a ver como se gestiona el estado de navegación dentro de un mismo tab. Para ello, vamos a crear un nuevo tab que será un listado de películas. al hacer click en una película podremos ver su sinopsis, y tendremos un botón “atrás” en la barra de navegación para volver al listado de películas.
Un servicio para acceder a nuestras películas
Dado que no podemos compartir contenido de un controlador a otro al existir en contextos distintos (vistas distintas), compartiremos un servicio entre los controladores de la vista de listado de películas y la vista de detalle. Es decir, lo que vamos a hacer ahora es utilizar una factoría de Angular para crear una representación singleton de nuestras películas (servicio), que podrá ser leída y actualizada desde multiples puntos de acceso. Si no has trabajando anteriormente con servicios en Angular, es recomendable leer la documentación al respecto.
app.factory('FilmsService', function() {
var films = [
{ title: "Captain Phillips",
details: "In telling the story of Capt. Richard Phillips (Tom Hanks), " +
"whose cargo ship was boarded by Somali pirates in 2009, " +
"director Paul Greengrass doesn't stop at gripping docudrama. " +
"With Hanks digging deep into the origins of an everyman's courage, " +
"the film raises the bar on action thrillers."},
{ title: "American Hustle",
details: "Only that scrappy virtuoso David O. Russell could morph a film about " +
"the Abscam political scandals of the late 1970s into a rollicking, " +
"emotionally raw human drama. Russell regulars – Christian Bale and Amy Adams from The Fighter, " +
"Jennifer Lawrence and Bradley Cooper from Silver Linings Playbook – " +
"help him turn the toxic mess of life on the edge into an exhilarating gift."},
{ title: "Her",
details: "Director Spike Jonze (Being John Malkovich, Adaptation) creates movies that " +
"help us see the world in startlingly funny and touching new ways. And in Her, " +
"set in the near future, Theodore (a sublime, soulful Joaquin Phoenix) falls hard " +
"for his computer operating system (voiced with humor, heat and heart by Scarlett Johansson) " +
"and makes us believe it. This is personal filmmaking at its glorious, groundbreaking peak."},
{ title: "Before Midnight",
details: "Nothing happens as two lovers, Jesse (Ethan Hawke) and Celine (Julie Delpy), " +
"continue to climb the Mount Everest of their relationship. In this story's third part, " +
"after 1995's Before Sunrise and 2004's Before Sunset, director Richard Linklater and " +
"pitch-perfect co-writers Hawke and Delpy create the defining love story of a generation."},
{ title: "The Wolf of Wall Street",
details: "This three-hour bolt of polarizing brilliance from Martin Scorsese, with a killer script " +
"by The Sopranos' Terence Winter, details the true tale of Jordan Belfort " +
"(Leonardo DiCaprio flares like a five-alarm fire in full blaze), who lived hoggishly high on securities " +
"fraud in the 1990s. Jordan and his co-scumbags (Jonah Hill crushes it as his wingman)" +
" numb moral qualms with coke, 'ludes and hookers. Scorsese's high-wire act of bravura " +
"filmmaking is a lethally hilarious take on white-collar crime. No one dies, but Wall Street victims" +
" will scream bloody murder."}
];
return{
films: films,
getFilm: function(index) {
return films[index];
}
}
});
Básicamente lo que hemos hecho es crear en la factoría un array json con unos cuantos diccionarios {title,details} de películas, de modo que cuando se cree el singleton devuelva un objeto con dicho array, y un método getFilm que nos devolverá la película para una cierta posición del array.
Estados abstractos
Para poder disponer de vistas anidadas, o varias vistas dentro de nuestro tab, crearemos lo que se llama un controlador abstracto, por lo que reestructuraremos nuestro código de routing de la siguiente forma:
app.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider.state('home', {
url: '/',
views: {
home: {
templateUrl: 'home.html'
}
}
});
$stateProvider.state('info', {
url: '/info',
views: {
info: {
templateUrl: 'info.html'
}
}
});
$stateProvider.state('films', {
abstract:true,
url: '/films',
views: {
films: {
template: '<ion-nav-view></ion-nav-view>'
}
}
});
$stateProvider.state('films.index', {
url: '',
templateUrl: 'films.html',
controller: 'FilmsCtrl'
});
$stateProvider.state('films.detail', {
url: '/:film',
templateUrl: 'film.html',
controller: 'FilmCtrl',
resolve: {
film: function($stateParams, FilmsService) {
return FilmsService.getFilm($stateParams.film)
}
}
});
});
Explicando el código:
- Como podemos ver, home e info los hemos dejado igual.
- Vista abstracta: Hemos creado un nuevo estado films, con la peculiaridad de declarar abstract:true, que además en la única vista que contiene define directamente un template < ion-nav-view>< /ion-nav-view>
- Vistas anidadas: Hemos creado 2 nuevos estados, films.index y films.detail, que serán los que se anidarán dentro del estado abstracto anterior, cada uno con su url (relativa a la de la vista abstracta), su template, y su controlador.
- url: ‘/:film’: “/myUrl/:param” es una forma de pasar un parámetro a la url, de modo que estaríamos aceptando urls del estilo “/myUrl/45”. En el caso que nos ocupa, pasaremos el valor de la variable film como parámetro. Es equivalente a url: ‘/{film}’
- resolve: Resolve es un objeto cuyas claves mapean valores que pueden ser inyectados en el controlador de nuestro estado. Funciona con valores o elementos diferidos, por lo que es especialmente útil para cargas de datos asíncronos (podríamos imaginar que nuestro servicio de películas en lugar de contener el JSON, lo carga de un servidor).
En este caso utilizamos el método resolve para crear una variable con el contenido del método getFilm de nuestro servicio, al que le hemos pasado gracias a $stateParams el índice de la película seleccionada (tendremos que pasarlo como parámetro en el template del listado de películas). Ahora, podremos utilizar esta variable desde nuestro controlador FilmCtrl.
DETALLE: Hacemos esto del resolve en lugar de llamar directamente FilmsService.getFilm($index) en el controlador FilmCtrl, por que así podemos asegurarnos de que cuando se llama al controlador, los datos ya están disponibles.
Esta forma de trabajar es especialmente útil cuando hacemos una petición al servidor por datos al cambiar una ruta.
Controladores
Nuestros controladores serán bien sencillos, con la particularidad de que el controlador FilmCtrl tomará como parámetro la variable film que hemos creado en el punto anterior. Como el controlador es lo último que se carga en el ciclo de Angular, la variable con el contenido de la película ya existirá.
Veamos el código, también en app.js:
app.controller('FilmsCtrl', function($scope, FilmsService) {
$scope.films = FilmsService.films;
});
app.controller('FilmCtrl', function($scope, film) {
$scope.film = film;
});
Templates
films.html
<ion-view title="Films">
<ion-content>
<ion-list>
<ion-item ng-repeat="film in films" class="item item-icon-right" ui-sref="films.detail({film: $index})">
<span>{{film.title}}</span>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
DETALLE: Como en los tabs, utilizamos ui-sref para indicar el estado al que se tiene que acceder, al pulsar sobre una película. No obstante, en este caso además le estamos pasando como argumento el índice de la iteración de ng-repeat en la que nos encontramos.
Esto es lo que, en la definición de dicho estado, recuperamos mediante $stateParams.
film.html
<ion-view title="Film">
<ion-content padding="true">
<h2>{{film.title}}</h2>
<span>{{film.details}}</span>
</ion-content>
</ion-view>
Botón atrás
Para que nuestra navegación dentro de las películas tenga sentido, nos falta un botón “atrás” en la barra de navegación, que incluiremos, dentro de la barra de navegacion en index.html, con la directiva ion-nav-back-button.
Además, añadimos también, como es lógico, el tab relativo a las películas.
Nuestro index.html se modifica conforme a:
<!DOCTYPE html>
<html ng-app="navApp">
<head><!-- ... --></head>
<body>
<ion-nav-bar class="bar-positive">
<ion-nav-back-button class="button-clear">
<i class="ion-arrow-left-c"></i> Back
</ion-nav-back-button>
</ion-nav-bar>
<ion-tabs class="tabs-positive">
<ion-tab icon="ion-home" ui-sref="home">
<ion-nav-view name="home"></ion-nav-view>
</ion-tab>
<ion-tab icon="ion-film-marker" ui-sref="films">
<ion-nav-view name="films"></ion-nav-view>
</ion-tab>
<ion-tab icon="ion-information" ui-sref="info">
<ion-nav-view name="info"></ion-nav-view>
</ion-tab>
</ion-tabs>
</body>
</html>
Resultados
Veamos como afectan al diseño los cambios que hemos hecho.
Podemos observar como el botón back de la barra superior, nos devuelve de nuevo, de forma automática, a la vista de la que procedíamos, es decir, el listado de películas.
Y con esto, queda concluido el tema.
Es todo por hoy.
¡Saludos!