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 directivasui-view
hayamos definido. En nuestro caso habrá tres (header
,content
,footer
). Al igual que en el caso dengRoute
, aquí podremos definir eltemplateUrl
para la vista a cargar, y uncontroller
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.

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.