6. Routing con ngRoute

Al principio de la sesión introductoria, vimos que todo el código estaba en la página principal y no había navegación hasta que se introdujo el módulo ngRoute, que nos permitió convertir nuestra aplicación en una SPA.

Desde la versión 1.2 de AngularJS, el módulo ngRoute no forma parte del core con lo que habrá que importarlo y añadirlo como dependencia de nuestra aplicación.

6.1. Gestión de la navegación

Para gestionar la navegación, AngularJS utiliza un truco, que consiste en que modifica partes de la barra de direcciones de una URL. ¿Cuáles? aquellas que van detrás del carácter #, también llamado hashbang. Todo lo que modifiquemos por detrás de este carácter se llama URL fragment. La especificación dice que, si cambiamos este fragmento sin alterar nada de lo que va por delante, el navegador no recargará la página. Sin embargo, este cambio sí que se guarda en el histórico del navegador, con lo cual los botones de atrás y adelante de nuestro navegador sí que funcionan. Será cosa nuestra gestionarlo correctamente para que la aplicación funcione como se desea al hacer uso de ellos.

Supongamos una serie de URLS de tipo CRUD. Idealmente, tendremos una URL para una lista de ítems, un formulario de edición, a lo mejor otro de creación, etc. Así, podrían ser:

  • /admin/users/list – Para mostrar un listado de usuarios

  • /admin/users/new – Formulario para crear un nuevo usuario

  • /admin/users/[userId]+ – Formulario para editar un usuario, cuyo ID es igual a +userId.

Podríamos traducir estas URLs parciales a URLs con fragmentos para una SPA, usando el truco del hashbang que hemos comentado:

6.2. La directiva ngView

La directiva ngView+ es esencial para el uso de rutas, y complementa al servicio +$route, incluyendo la plantilla de la ruta actual en nuestro layout principal, que debe estar situado en nuestro fichero index.html. Cada vez que la ruta cambia, todo el contenido de esta etiqueta cambiará, en función de lo que hayamos configurado en nuestro servicio de routing.

La directiva ng-view se puede usar a modo de elemento, o bien como atributo de un elemento div. Sin embargo, por cuestiones de compatibiliad con IE7 se recomienda su uso como atributo.

En el siguiente ejemplo se muestran las dos variantes, habiendo dejado comentado su uso como elemento.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/> </head> <body ng-app="routing"> <div class="container"> <div ng-include="'templates/common/header.html'"></div> <div ng-view></div> <!-- <ng-view></ng-view> --> <div ng-include="'templates/common/footer.html'"></div> </div> <script src="https://code.angularjs.org/1.2.22/angular.js"></script> <script src="https://code.angularjs.org/1.2.22/angular-route.js"></script> <script src="app.js"></script> </body> </html>

6.2.1. ngInclude

En el fragmento de código anterior, hemos visto dos bloques que hacen uso de la directiva ngInclude. Ésta se encarga de obtener un fragmento de HTML, compilarlo e introducirlo en nuestra aplicación.

Es muy útil para insertar elementos parciales que se van a repetir a lo largo de nuestra aplicación. Por ejemplo, en el bloque anterior se ha utilizado para añadir la cabecera y el pie de la aplicación. Son dos elementos que van a estar siempre ahí, y no queremos "ensuciar" nuestro fichero index.html con su código.

Aquí tenemos un enlace de una aplicación en ese estado.

Fijáos que la directiva recibe como parámetro una expresión, por eso su valor está entre comillas simples.

6.3. Definición de rutas

En AngularJS, las rutas se definen en la fase de configuración de la aplicación, haciendo uso del servicio $routeProvider. Éste proporciona una API sencilla donde podemos encadenar métodos para definir rutas (método when), y establecer una ruta por defecto (otherwise).

El método when recibe como entrada dos parámetros:

  • path (string). Ej: /user/list

  • ruta (objeto). Puede tener varios atributos [1], pero lo normal es que usemos:

    • controller. Será el nombre de un controlador que hayamos creado en nuestra aplicación. Al definir aquí el controlador, ya nos ahorramos el tener que emplear la directiva `ng-controller en nuestro código.

    • templateUrl. Ruta hacia la plantilla HTML con el contenido de nuestro parcial.

Por su parte, el método otherwise suele recibir un parámetro, consistente en un objeto con un atributo redirectTo, indicando la URL a la que redirigir cuando no se encuentra ninguna ruta concordante.

Supongamos una aplicación de dos páginas. Consiste en una aplicación de pedidos, donde en la primera página tenemos un listado de pedidos, y en la segunda un formulario para realizar nuevos pedidos. La página por defecto de nuestra aplicación será el listado de pedidos.

  • Un índice, que contiene un listado de todos los pedidos realizados.

  • Un formulario de introducción de nuevos pedidos.

Para no incrementar la complejidad de nuestra aplicación, los pedidos consistirán en una cadena de texto. Aquí tenemos el código completo de esta aplicación. Veamos cómo hemos configurado las rutas:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
angular .module('ordersapp', ['ngRoute']) .config(function($routeProvider){ $routeProvider .when('/orders', { templateUrl:'orders/tpl/list.html', controller: 'OrdersCtrl' }) .when('/orders/new', { templateUrl: 'new-order/tpl/new.html', controller: 'NewOrderCtrl' }) .otherwise({ redirectTo : '/orders'}); });

Si nos vamos a una de las vistas, por ejemplo orders/tpl/list.html vemos que no se ha introducido la directiva ng-controller al haber definido el controlador en la definición de rutas.

1 2 3 4 5 6 7 8 9 10 11 12
<h2>Order list</h2> <div class="row" ng-repeat="order in orders"> <div class="col col-xs-12"> <p>{{ order }}</p> </div> </div> <div class="row"> <div class="col col-xs-12"> <a href="#/orders/new" class="btn btn-default">New order</a> </div> </div>

6.4. Rutas parametrizadas

En la aplicación que hemos hecho, utilizamos un sistema de rutas bastante sencillo, que no utiliza ninguna parte variable en la URL. Sin embargo, hoy en día estamos hartos de ver URLs con partes variables. Los antiguos search parameters

/users/edit/id?=1
/users/edit/id?=2
/users/edit/id?=114

Ha pasado de moda, y ahora lo habitual es construir URLs de la forma

/users/edit/1
/users/edit/2
/users/edit/114

Hacer esto con el sistema de routing de AngularJS es bastante sencillo: podemos declarar elementos variables simplemente poniendo el símbolo de los dos puntos (:) delante de éste. Así, en nuestra aplicación de pedidos, introduciremos una ruta nueva para ver el detalle de un pedido.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
angular .module('ordersapp', ['ngRoute']) .config(function($routeProvider){ $routeProvider .when('/orders', { templateUrl:'orders/tpl/list.html', controller: 'OrdersCtrl' }) .when('/orders/new', { templateUrl: 'new-order/tpl/new.html', controller: 'NewOrderCtrl' }) .when('/orders/edit/:idx', { //Ruta parametrizada templateUrl: 'view-order/tpl/view.html', controller: 'ViewOrderCtrl' }) .otherwise({ redirectTo : '/orders'}); });

Para recibir los parámetros en nuestro controlador, deberemos hacer uso del servicio $routeParams. Éste nos permite recibir todos los parámetros que hayamos pasado a la URL.

1 2 3 4 5
angular .module('ordersapp') .controller('ViewOrderCtrl', function ($scope, OrdersService, $routeParams){ $scope.order = OrdersService.getOrder($routeParams.idx); });

Lo bueno del servicio $routeParams es que combina los parámetros que llegan tanto por URL como por search parameters. Con lo cual, hubiera funcionado exactamente igual, y sin tocar nada, de haber utilizado una url de la forma orders/edit?idx=3

Aquí tenemos el código completo de la aplicación de pedidos.

6.5. Redirección

En el código de ejemplo ya hay bloques que muestran cómo realizar una redirección de una página a otra. No obstante lo mencionaremos más detenidamente, con un par de recomendaciones.

6.5.1. Desde una vista

Para navegar de una ruta a otra desde una vista, lo haremos de manera exactamente igual a como lo haríamos en HTML: con la etiqueta <a>. No obstante, los destinos irán todos precedidos por el hashbang. Por ejemplo:

1
<a href="#/users/list">Listado de usuarios</a>

Para referenciar una ruta variable, podemos hacerlo de igual manera, haciendo uso de las variables de AngularJS:

1
<a href="#/users/edit/{{user.id}}">Editar usuario</a>

Sin embargo, cuando utilizamos código dinámico (variables, funciones, etc.) en un enlace, la manera más correcta de hacerlo es utilizando la directiva ngHref.

En algunas ocasiones al generar el enlace dinámico anterior, podría darse el caso de que el usuario hace clic antes de que AngularJS haya tenido la oportunidad de establecer el valor dinámico. Al usar la directiva ngHref, AngularJS no establece el valor del atributo href hasta que ha podido interpretar el valor completo de la cadena. Así, lo que hace es traducir esto:

1
<a ng-href="#/users/edit/{{user.id}}">Editar usuario</a>

a esto otro:

1
<a href="#/users/edit/1">Editar usuario</a>

en cuanto tiene la mínima oportunidad.

6.5.2. Desde un controlador. El objeto $location

En no pocas ocasiones querremos que la redirección se haga en función de que cierta lógica de negocio se haya aplicado correctamente o no. Esto implica que dicha redirección tenga que hacerse desde un controlador u otro servicio. Para hacerlo desde aquí, recurriremos al servicio $location.

El servicio $location+ [2] parsea la URL de la barra de direcciones del navegador (según los valores de +window.location), y hace que la URL esté accesible para nuestra aplicación. Todo cambio que se haga en la URL se verá reflejado en el servicio $location y viceversa.

El servicio $location:

  • Expone la URL actual de la barra de direcciones del navegador, para que podamos:

    • Observar la URL

    • Modificar la URL

  • Sincroniza la URL de la barra de direcciones del navegador cuando el usuario:

    • Modifica la barra de direcciones

    • Hace clic en los botones forward o back del navegador (o hace click en un enlace histórico)

    • Hace clic en un enlace.

  • Representa el objeto URL como un conjunto de métodos:

    • protocol: http, https,…​

    • host: localhost, dccia.ua.es

    • port: 80, 8080, 63342,…​

    • path: /orders

    • search

    • hash

servicio location

Estos métodos actúan a la vez como getters y setters, en función de si tienen o no parámetros. Así hemos podido ver cómo en el controlador NewOrderCtrl, redirigíamos a la página principal una vez insertado un pedido mediante la orden:

1
$location.path('/orders');

6.6. Ejercicio (1 punto)

Modifica el ejemplo de los pedidos para adaptar el carrito de la compra que hemos estado haciendo en las últimas sesiones.

Tendremos de igual manera dos pantallas: una de listado (ruta: /list) y otra de detalle de producto (ruta: /detail/:id_producto). La ruta por defecto será la del listado.

Como vemos en los siguientes mockups, la cabecera tendrá un título genérico, o bien el nombre del producto. Además, tendrá la cesta de la compra indicando el número de productos y el importe total.

listado
detalle

Aplica el tag ngRoute a la versión que quieres que se corrija.


1. Podemos ver toda la configuración en https://docs.angularjs.org/api/ngRoute/provider/$routeProvider
2. https://docs.angularjs.org/api/ng/service/$location