7. Routing con ui-router

Una cosa que puede no parecer muy obvia, es que el routing de URLs puede considerarse como una máquina de estados finitos. Cuando configuramos las rutas, estamos definiendo los distintos estados por los que atraviesa nuestra aplicación, e informando a la aplicación qué debe mostrarse cuando estamos en una ruta determinada.

Hemos visto que AngularJS nos proporciona un mecanismo de routing que, pese a ser totalmente válido, tiene ciertas limitaciones. Entre ellas, en la clase anterior hemos visto la necesidad de incluir, en cada una de las vistas, la cabecera y el pie con directivas ngInclude. Además, las redirecciones se hacían directamente contra la ruta de manera que, si esta cambia, debemos ir a cada etiqueta a y cada llamada a $location.path() a modificarla.

El módulo ui-rouoter se adapta perfectamente al concepto de routing como máquina de estados finita. Permite definir estados, y transiciones de un estado a otro. Además, nos permite desacoplar estados anidados, y gestionar layouts más complejos de una manera sencilla y elegante.

El concepto de routing es un poco distinto, pero a la larga acaba gustando más que el de ngRoute

En este capítulo, vamos a modificar la aplicación de pedidos realizada en el capítulo anterior y adaptarla a ui-router.

En nuestra aplicación, identificamos un layout con tres componentes:

  • Cabecera

  • Cuerpo

  • Pie

7.1. Primeros cambios en la aplicación

Lo primero que haremos será deshacernos del módulo ngRoute, e incluir el módulo de ui-router, para ello, eliminaremos la línea

1
<script src="https://code.angularjs.org/1.2.22/angular-route.js"></script>

Y en su lugar introduciremos la siguiente:

1
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.js"></script>

Deberemos inyectar, además, el módulo ui.router en nuestra aplicación:

1 2
angular .module('ordersapp', ['ui.router'])

7.2. La directiva uiView

Al igual que con ngRoute era imprescindible el uso de la directiva ngView para declarar dónde iba el contenido de cada ruta en la vista, aquí haremos uso de la directiva uiView.

Sin embargo, una de las ventajas de ui-router es que nos permite definir más de un bloque de este tipo, por lo que vamos a definir tres de ellos:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
<!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="ordersapp"> <div class="container"> <div ui-view name="header"></div> <div ui-view name="content"></div> <div ui-view name="footer"></div> </div> <script src="https://code.angularjs.org/1.2.22/angular.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.js"></script> <script src="app.js"></script> <script src="components/services/OrdersService.js"></script> <script src="orders/controllers/OrdersCtrl.js"></script> <script src="new-order/controllers/NewOrderCtrl.js"></script> <script src="view-order/controllers/ViewOrderCtrl.js"></script> </body> </html>

Al realizar este cambio, vamos a olvidarnos de tener que realizar un ng-include en cada una de las vistas.

Al igual que teníamos una plantilla llamada header y otra llamada footer, crearemos ahora una tercera plantilla, content.html, cuyo contenido será:

1 2 3 4 5
<div class="row"> <div class="col col-xs-12" ui-view> <h4>Welcome to the orders app</h4> </div> </div>

Vemos que éste también tiene una directiva ui-view. En seguida veremos por qué.

7.3. Definiendo nuestro primer estado

Nuestro primer estado se corresponderá con la ruta /orders.

Al igual que en el capítulo anterior hacíamos uso del servicio $routeProvider para la definición de rutas, aquí utilizaremos el servicio $stateProvider, ya que hemos dicho que consideraremos nuestro sistema de routing como una máquina de estados.

Para ello, el servicio $stateProvider dispone de un método llamado state, que recibe como primer parámetro un nombre de estado (el que nosotros queramos), y como segundo parámetro un objeto con los atributos:

  • url: url del estado que estamos definiendo

  • views: objeto que tendrá tantos atributos como directivas ui-view hayamos definido. En nuestro caso habrá tres (header, content, footer). Al igual que en el caso de ngRoute, aquí podremos definir el templateUrl para la vista a cargar, y un controller para definir el controlador que gestionará dicha vista. Como de momento no vamos a querer un controlador, no lo definimos para ninguna de ellas.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
angular .module('ordersapp', ['ui.router']) .config(function ($stateProvider) { $stateProvider .state('orders', { url: '/orders', views: { header: { templateUrl: 'components/templates/common/header.html' }, content: { templateUrl: 'components/templates/common/content.html' }, footer: { templateUrl: 'components/templates/common/footer.html' } } }); });

Si vamos a http://localhost:63342/angularjs-routing-examples/index.html#/orders, veremos que el layout de nuestra aplicación ya está conformado.

ngroute orders

7.4. Estados anidados

Hasta ahora, hemos creado el esqueleto de nuestra aplicación. No vemos dónde está el listado de pedidos. Para ello, definiremos un nuevos estado, llamado orders.list

1 2 3 4 5
.state('orders.list', { url: '/list', controller: 'OrdersCtrl', templateUrl: 'orders/tpl/list.html' })

Si nos vamos ahora a la URL http://localhost:63342/angularjs-routing-examples/index.html#/orders/list, veremos que ya tenemos el listado de productos de igual manera que teníamos en el capítulo anterior.

A priori, nos llamará la atención varias cosas. La primera de ellas, es que en nuestro estado hemos definido la url como list, y en nuestra aplicación aparece /orders/list como URL.

Esto se debe a que, por definición, todo estado que tenga un nombre dado, precedido por el nombre de otro estado y un punto (orders.list) se considera un estado anidado (nested state).

Un estado anidado hereda todo lo definido en el estado padre. Su URL, además, será composición de la URL del padre, más la URL que en el estado definamos. Es por ello que la URl es /orders/list. Una gran ventaja que aporta es que, si queremos renombrar la URL a order (por poner un ejemplo), únicamente debemos hacerlo en un punto. Además, todo el layout del padre se hereda, por eso no hemos tenido que definir la cabecera ni el pie.

¿Pero cómo sabe ui-router dónde colocar la vista? Muy sencillo. Si volvemos a ver el código de la plantilla content.html veremos que ahí se había definido un objeto div con un atributo ui-view. Éste es el punto que aprovecha ui-router para introducir la nueva plantilla, dejando el resto intacto.

Además, como a este nivel ya disponemos sólo de un ui-view, no es necesario jugar con el objeto views como habíammos hecho en la definición del estado anterior: podemos definir el controller (aquí sí que necesitamos ya uno) y el templateUrl a nivel de raíz del objeto.

De igual manera, definiremos los estados de creación y detalle:

1 2 3 4 5 6 7 8 9 10
.state('orders.new', { url: '/new', templateUrl: 'new-order/tpl/new.html', controller: 'NewOrderCtrl' }) .state('orders.edit', { url: '/edit/:idx', templateUrl: 'view-order/tpl/view.html', controller: 'ViewOrderCtrl' })

7.5. Definiendo una ruta por defecto

Al igual que con el servicio ngRoute podíamos definir un estado por defecto en caso de no encontrar ninguna ruta, aquí también lo podemos hacer. Para ello, necesitamos inyectar el servicio $urlRouterProvider en nuestra función de configuración. Este servicio dispone de un método otherwise, que recibe como parámetro la URL destino a la que redirigir en caso de no haber resuelto ninguna.

1 2 3 4 5 6 7 8
angular .module('ordersapp', ['ui.router']) .config(function ($stateProvider, $urlRouterProvider) { // DEFINICIÓN DE ESTADOS $urlRouterProvider.otherwise('/orders/list'); });

7.6. Estados abstractos

Puede que os hayáis preguntado si realmente es necesario tener una ruta /orders que sea accesible desde el navegador. Efectivamente, esta ruta nos ha valido para conformar el layout inicial y no la necesitamos para nada más, ya que no aporta nada en absoluto. El módulo ui-router contempla este caso, y nos permite definir el estado orders como un estado abstracto. Al igual que una clase java, un estado abstracto no puede generarse por sí sólo, sino a través de alguna de las clases que lo extienden. Podemos declarar un estado como abstracto añadiéndole el atributo abstract:true:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
.state('orders', { abstract: true, url: '/orders', views: { header: { templateUrl: 'components/templates/common/header.html' }, content: { templateUrl: 'components/templates/common/content.html' }, footer: { templateUrl: 'components/templates/common/footer.html' } } })

Si intentamos ir ahora a http://localhost:63342/angularjs-routing-examples/index.html#/orders, veremos que la URL no se resuelve correctamente, con lo que seremos redirigidos al estado definido en $urlRouterProvider.otherwise.

7.7. Recepción de parámetros en el controlador

Para la recepción de parámetros en un controlador utilizaremos el servicio $stateParams, que funciona de igual manera que el servicio routeParams. Así, nuestro cambios en el controlador ViewOrderCtrl serán m��nimos.

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

7.8. Redirección

A nivel de redirección, tendremos que hacer unos cambios mayores. En ui-router, en lugar de la ruta, indicaremos al estado al que queremos realizar la transición. Es muy fácil querer cambiar el nombre de una ruta. Sin embargo, los estados tienen una nomenclatura con un significado semántico, que no querremos cambiar. Ahora, si decidimos traducir nuestras URLs a español, sólo tendremos que hacerlo a nivel de configuración.

7.8.1. Desde una vista

Desde una vista, cambiaremos nuestros ng-href="route" por ui-sref="state".

En caso de incluir parámetros, añadiremos un objeto con el nombre de el(los) parámetro(s).

Por ejemplo, la plantilla orders/tpl/list.html quedará:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
<div> <h2>Order list</h2> <div class="row" ng-repeat="order in orders"> <div class="col col-xs-12"> <p>{{ order }} <a ui-sref="orders.edit({idx:$index})">[Edit]</a></p> </div> </div> <div class="row"> <div class="col col-xs-12"> <a ui-sref="orders.new" class="btn btn-default">New order</a> </div> </div> </div>

7.8.2. Desde un controlador

Desde un controlador, haremos uso del servicio state, que dispone del método go(stateName). La variación a realizar sobre el controlador NewOrderCtrl sería:

1 2 3 4 5 6 7 8 9 10
angular .module('ordersapp') .controller('NewOrderCtrl', function ($scope, OrdersService, $state) { $scope.order = null; $scope.saveOrder = function(){ OrdersService.addOrder($scope.order); $state.go('orders.list'); }; });

En caso de querer redirigir a una ruta con parámetros, los pasaremos en un objeto JSON:

1
$state.go('orders.list', {idx: 0});

Aquí tenéis acceso al código de la aplicación de pedidos modificada y adaptada a ui-router.

7.9. Ejercicio (1 punto)

Adapta el ejemplo de la sesión anterior para utilizar ui-router.

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