Ejercicio donde tenemos en marcha dos controladores ejecutándose en paralelo en dos partes de la página, así como varias vistas. Conocemos también $location de AngularJS.
Es común usar más de un controlador en una aplicación AngularJS. Hasta ahora no lo habíamos necesitado pero para separar nuestro código en controladores puede aportar diversos beneficios, en el caso que tenga algún sentido esa separación.
En el ejemplo que os traemos queda bien claro que tiene sentido separar el código en dos controladores distintos, porque están pensados para hacer cosas muy específicas y distintas entre si. Observarás que separar la complejidad del programa en diversas partes, provoca que cada una sea más simple y fácil de entender. Todo ello redundará en un mantenimiento más sencillo, lo que al final se traduce en menos trabajo y mayor rentabilidad.
Para hacer este ejercicio hemos seguido el prototipo realizado en el artículo anterior dedicado al ngRoute, por lo que si no lo has leído ya, es importante que lo hagas. En aquel ejemplo vimos cómo implementar enlaces "profundos", en una barra de navegación, que nos llevan a distintas vistas dentro de la misma página.
Ahora vamos a mejorar el comportamiento de nuestra barra de navegación. Aislaremos ese comportamiento en un nuevo controlador independiente. Con ello tendremos dos controladores bien separados:
- Un controlador para la barra de navegación de secciones
- Un controlador para las vistas de mi aplicación
Como puedes ver, son dos áreas distintas de la aplicación y por lo tanto tiene sentido usar controladores distintos. El controlador de la navegación sirve simplemente para implementar un comportamiento en mi barra de navegación, mientras que el controlador de las vistas independientes serviría para mantener la funcionalidad de esas vistas.
Echemos un vistazo en el código HTML de este ejercicio
<body ng-app="app">
<nav ng-controller="navCtrl as nav">
<ul>
<li ng-class="{marcado: nav.estoy('/')}">
<a href="#/">Datos personales</a>
</li>
<li ng-class="{marcado: nav.estoy('/descargas')}">
<a href="#/descargas">Descargas</a>
</li>
<li ng-class="{marcado: nav.estoy('/opciones')}">
<a href="#/opciones">Opciones de cuenta</a>
</li>
</ul>
</nav>
<hr />
<div ng-view></div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.24/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.24/angular-route.js"></script>
<script src="app.js"></script>
</body>
De momento observarás que en el HTML anterior solo parece que estemos usando un controlador, en la etiqueta NAV. No nos hemos olvidado del otro, debes recordar que estaba declarado para cada una de las vistas, que habíamos definido con el $routeProvider.
$routeProvider
.when("/", {
controller: "appCtrl",
controllerAs: "vm",
templateUrl: "home.html"
})
// …..
Controlador para la barra de navegación
El controlador navCtrl, declarado en la etiqueta NAV, tiene un scope conocido como "nav" ( ng-controller="navCtrl as nav"). En realidad este controlador es muy sencillo, luego veremos su código. De momento lo usamos simplemente para definir si debe aparecer una clase (class de CSS) en el enlace de la sección tenemos abierta en nuestra aplicación. Ya sabes que cuando se trata de asignar o no una clase se utiliza la directiva ng-class, en la que usamos un objeto para definir la clase y la expresión que debe de cumplirse para que esté presente en la etiqueta.
<li ng-class="{marcado: nav.estoy('/')}">
Si te fijas, se trata de una clase llamada "marcado" que estará presente en la etiqueta LI en caso que el método estoy() que está dentro del scope "nav" nos devuelva un valor evaluado como verdadero.
<li ng-class="{marcado: nav.estoy('/opciones')}">
Este otro elemento de la lista tendrá la clase "marcado" en caso que el método estoy() de nav devuelva true cuando le pasamos el parámetro con valor "/opciones".
Ahora veamos la implementación de ese controlador.
.controller("navCtrl", function($location){
var map = this;
map.estoy = function(ruta){
return $location.path() == ruta;
}
})
Como puedes comprobar en el controlador se definió el método estoy(), pero hay una cosa nueva que te llamará la atención seguramente. Se trata de la variable $location que está inyectada en la función que implementa el controlador.
Conociendo $location
$location es un servicio ("service", tal como los llaman en AngularJS) que nos sirve para mantener y trasladar información de la ruta actual del navegador. $location implementa una interfaz para el acceso a la propiedad nativa de Javascript window.location, y nos sirve para acceder a elementos de la ruta actual, conocer su estado y modificarlo. Por tanto, podemos saber en todo momento qué ruta tenemos y conocer cuando el usuario ha navegado a otro lugar, ya sea con los botones de atrás o adelante o pulsando un enlace.
Pero no solo eso, existe un un enlace entre $location y window.location en las dos direcciones. Todo cambio que realicemos en $location también tendrá una respuesta en lo que se está mostrando en la barra de direcciones del navegador. Así como todo cambio de URL en el navegador tiene un efecto en el service $location.
$location tiene una serie de métodos bastante interesante a través de los cuales podemos acceder a partes de la URL que está en la barra de direcciones, desde el dominio, el puerto, protocolo, hasta la ruta en sí de la aplicación.
En el ejemplo de antes estábamos usando $location.path() que nos devuelve el camino actual. Este método acepta dos juegos de parámetros.
- Si llamamos a $location.path() sin parámetros, nos devuelve la ruta actual.
- Si llamamos a $localtion.path() indicando un parámetro, entonces se cambia la ruta actual a aquello que le hayamos indicado. La ruta debe comenzar por "/" el método es lo suficientemente listo para que, si no tiene la barra, se la pone automáticamente.
Así que, volviendo a nuestra función estoy(), apreciarás que devolverá true cuando la ruta enviada como parámetro coincida con la ruta de nuestra aplicación en ese instante, recuperada con $location.path().
map.estoy = function(ruta){
return $location.path() == ruta;
}
Controlador de las vistas
De nuevo llegamos al controlador de las vistas independientes. Ahora hemos colocado un poco de código.
controller("appCtrl", function(){
var vm = this;
vm.colores = ["green", "blue", "orange"];
});
Realmente este código no es más que una inicialización de valores en un array llamado "vm.colores".
Ahora veamos el código de las vistas independientes y veamos cómo usamos ese array de colores. Observarás que las vistas no hace nada muy espectacular, solo hemos colocado alguna cosa con fines didácticos.
Vista "home.html": un simple recorrido a los colores del array para mostrarlos en cajas DIV.
<h1>Home</h1>
<div ng-repeat="color in vm.colores">{{ color }}</div>
Vista "opciones.html": un campo de texto donde escribir un color nuevo. Un botón donde enviamos el contenido de ese campo de texto dentro del array de colores.
<h1>Opciones</h1>
Tienes {{ vm.colores }}
<p>
Agrega <input type="text" ng-model="vm.nuevocolor"> <input type="button" value="ok" ng-click="vm.colores.push(vm.nuevocolor)">
Vista "descargas.html": Otro botón para vaciar el array de colores.
<h1>Descargas</h1>
{{ vm.colores }}
<input type="button" value="Vaciar" ng-click="vm.colores = []">
Controladores siempre inicializan sus valores en cada "uso"
Ahora queremos mencionar de nuevo la problemática de los controladores comentada por encima en el artículo anterior. Ahora que tenemos algo de código en nuestro controlador de las vistas y somos capaces de ejecutarlo para darnos cuenta de un detalle.
Si realizas una navegación por las distintas secciones de la aplicación fabricada hasta el momento verás que todas las vistas operan de alguna forma sobre el array de los colores. Podemos en una vista agregarle elementos, en otra podemos vaciarlo. Sin embargo, te habrás fijado que cada vez que se accede a una vista el array de colores vuelve a su estado inicial, independientemente que antes lo hayamos manipulado. ¿Cómo es posible? ¿Entonces no hay manera de memorizar los estados del scope entre las diferentes vistas de una aplicación? Como verás más tarde, sí la hay, pero no lo haremos con los controladores.
Este comportamiento ocurre porque, cada vez que se accede a una vista independiente se está volviendo a cargar el controlador, ejecutándose la función del controlador. Como en el controlador se inicializa el array de colores, observarás que cada vez que se usa en una vista ese array se inicializa de nuevo. En fin, cuando pasas de una vista a otra el array de colores se vuelve a poblar con los colores que se habían configurado de manera predeterminada en el controller.
La solución a este problema pasa por usar factorías o servicios, pero es algo que veremos en el siguiente artículo. Hasta ahora es importante que te quedes con esto: las funciones que implementan los controladores son como constructores, que se llaman cada vez que el compilador del HTML de Angular pasa por la vista.
Alberto Basalo
Alberto Basalo es experto en Angular y otras tecnologías basadas en Javascript,...