Mejorando el paso de parámetros entre controladores en AngularJS

En el post anterior explicaba dos formas de pasar parámetros entre controladores de angularjs. Una de ellas, el uso de parámetros a través de la URL, no permitía pasar datos complejos, y la otra, el uso de un servicio intermedio para almacenar los datos, no acababa de convencerme porque implicaba tener que estar creando servicios sólo para eso y resultaba en un API un tanto incómoda de manejar.

En este post vamos a ver un par de opciones que, aunque internamente se siguen basando en el uso de un servicio intermedio, por lo menos lo oculta y da lugar a un API más amigable.

Camiseta I Survived Javascript
Camiseta original de Koalite’s Kipple

Un poco de contexto

Antes de seguir leyendo, es importante dejar claro que ninguna de estas soluciones es la mejor solución. Cada una de ellas, incluyendo las que vimos en el post anterior, es una manera diferente de pasar parámetros entre controladores de angularjs, y cada una tiene sus ventajas e inconvenientes.

Las dos soluciones que vamos a ver en este post están pensadas para escenarios en que tenemos un controlador “reutilizable” que muestra una vista a la que se puede llegar desde varios sitios, pero que la información que maneja no forma realmente parte de ningún proceso mayor.

Por ejemplo, imaginad un controlador que muestra la vista previa de un informe antes de imprimirlo, o que presenta de error personalizable cuando el usuario intenta hacer una operación para la que no tiene permisos. En esos casos, pasar la información por la URL puede no ser viable, y utilizar un servicio intermedio que sólo almacena los parámetros hasta que se invoca este controlador es incómodo y feo.

Si tenemos varios controladores que colaboran en un flujo de trabajo, por ejemplo las distintas fases del checkout de un pedido online, seguramente tenga mucho más sentido encapsular ese proceso en un servicio que, además de servirnos para almacenar la información, aporte la lógica necesaria para su gestión.

Una vez aclarado este punto, veamos esas dos alternativas.

Inyectar los parámetros en el controlador de destino

Con la primera opción queremos llegar a poder escribir el siguiente código:

var app = angular.module("MyApp", []);

// Controlador asociado a /view1
app.controller("Ctrl1", function($scope, Navigator) {
  $scope.goToView2 = function() {
    Navigator.goTo('/view2', { name: "Paco", age: 31 });
  };
});

// Controlador asociado a /view2
app.controller("Ctrl2", function($scope, $args) {
  $scope.message = $args.name + " tiene " + $args.age + " años";
});

La idea es utilizar un nuevo servicio, Navigator, que funcione de una forma similar al $location de angularjs pero que, además de indicar la ruta a la que queremos navegar, nos permita pasar un objeto con los parámetros que queramos hacer llegar al controlador de la ruta de destino. El controlador de destino podrá recibir esos parámetros inyectados como una dependencia más a través del objeto $args, de forma análoga a como recibiría parámetros de la URL en el objeto $routeParams.

Para implementar esto, vamos a empezar por el servicio Navigation:

app
  .value("tempStorage", {})
  .service("Navigator", function($location, tempStorage) {
    return {
      goTo: function(url, args) {
        tempStorage.args = args;
        $location.path(url);
      }
    };
  });

El servicio Navigator lo único que hace es decorar el servicio $location de angularjs, almacenando previamente los argumentos en un “servicio” auxiliar llamado tempStorage. Sí, al final también estamos usando un servicio global para almacenar temporalmente los parámetros, pero al menos eso queda oculto del API.

Ahora sólo nos falta poder inyectar al controlador de destino los parámetros que hemos almacenado en tempStorage. Para ello, vamos a modificar dinámicamente la propiedad locals del objeto $route. La propiedad locals es un map en el que podemos definir servicios que queremos inyectar al controlador asociado a la ruta o redefinir alguno de los servicios existentes.

Suena un poco complicado, pero en la práctica es muy sencillo:

app.run(function($rootScope, tempStorage) {
  $rootScope.$on('$routeChangeSuccess', function(evt, current, prev) {
    current.locals.$args = tempStorage.args;
  });
});

Definimos un bloque run asociado a la aplicación para que se ejecute al arrancar la aplicación. En ese bloque nos enganchamos al evento $routeChangeSuccess, que se dispara cuando cambia la ruta pero antes de instanciar el controlador, y nos permite modificar el objeto locals que se usará para crear el controlador.

Añadiendo los argumentos al $scope del controlador de destino

Usando la técnica anterior podemos pasar parámetros de una forma bastante limpia entre dos controladores, pero nos obliga a hacer que el controlador de destino adquiera una dependencia sobre un objeto $args. Podemos evitar esto si hacemos que los parámetros se añadan directamente al controlador de destino, consiguiendo un API como ésta:

var app = angular.module("MyApp", []);

// Controlador asociado a /view1
app.controller("Ctrl1", function($scope, Navigator) {
  $scope.goToView2 = function() {
    Navigator.goTo('/view2', { name: "Paco", age: 31 });
  };
});

// Controlador asociado a /view2
app.controller("Ctrl2", function($scope) {
  $scope.message = $scope.$args.name + " tiene " + $scope.$args.age + " años";
});

Como véis, el código es muy parecido al anterior pero no hace falta incluir $args en la declaración de Ctrl2, sino que lo tenemos directamente accesible a través de su $scope.

Para implementar esto, el servicio Navigator es exactamente igual que antes, lo que cambia es la forma de hacer llegar los datos al controlador de destino. En lugar de engancharnos al evento $routeChangeSuccess vamos a interceptar la creación de nuevos $scopes.

Para ello, vamos a aprovecharnos de la excelente extensibilidad que tiene angularjs, gracias en gran medida al uso que hace de inyección de dependencias, lo que nos permitirá interceptar la forma en que se crea el $scope del controlador de destino:

app.config(function($provide) {
  $provide.decorator('$rootScope', function($delegate, tempStorage) {
    var _$new = $delegate.$new;
    $delegate.$new = function() {
      var scope = _$new.apply($delegate, arguments);
      scope.$args = tempStorage.args;
      return scope;
    };
    return $delegate;
  });
});

El código anterior modifica la configuración del módulo al que está asociado (en el ejemplo, el módulo que he llamado app) permitiéndonos cambiar la forma en que trabaja el sistema de inyección de dependencias de angularjs, que está representado por el objeto $provide.

La función decorator del objeto $provide permite modificar un servicio y reemplazarlo por otro o, como en nuestro caso, decorarlo de la manera que consideremos oportuna. Para ello, decorator recibe el nombre del servicio que queremos modificar, en este caso $rootScope, y una función en la que podemos referenciar al servicio a través del parámetro $delegate. Es decir, en el cuerpo de la función, podemos acceder al objeto $rootScope original usando $delegate.

Una vez que tenemos claro esto, lo demás es muy sencillo. Sólo necesitamos cambiar la función $new original que se usa para crear $scopes por otra que, después de llamar a la función original, añada al $scope creado los parámetros que habíamos almacenado temporalmente en el “servicio” tempStorage. Un ejemplo más del poder del monkey patching.

Aunque hay veces que esta opción resulta más cómoda que la anterior porque no hace falta definir la dependencia $args en el controlador de destino, tiene algunos problemas potenciales. Modificar así un servicio tan importante de angularjs como es es el $rootScope no deja de tener sus efectos colaterales.

El método $rootScope.new() no sólo se utiliza al instanciar controladores, también se puede invocar manualmente, o al crear directivas, y estaríamos añadiendo la propiedad $args a todos los objetos $scope creados. En principio no es algo demasiado preocupante (al fin y al cabo es sólo una referencia y el consumo de memoria no debería verse muy afectado), pero no deja de ser algo un poco sucio que hay que considerar si puede causarnos problemas o no en nuestra aplicación.

Una limitación importante

Las dos opciones planteadas en este post tienen una limitación importante: los parámetros no estarán disponibles para el controlador si el usuario pulsa el botón volver del navegador. En realidad, no es un caso que me haya encontrado porque en esas circunstancias normalmente se trata de un proceso más largo en el que sí que tiene sentido utilizar un servicio explícitamente para encapsular la información, pero es una limitación a tener en cuenta.

Es posible resolverla manteniendo una asociación de parámetros a ruta en el tempStorage, en lugar de mantener un único objeto con parámetros y sobreescribirlo en cada cambio de ruta. De esta forma, al activar cada ruta podríamos recuperar el último objeto con parámetros con que se navegó a ella y recuperarlo, incluso si estamos volviendo de otra ruta con un history.back() en lugar de navegando explícitamente a la ruta de destino.

La idea aplicada al primer ejempo sería algo así:

app
  .value("tempStorage", {})
  .service("Navigator", function($location, tempStorage) {
    return {
      goTo: function(url, args) {
        tempStorage.args[url] = args;
        $location.path(url);
      }
    };
  })
  .run(function($rootScope, tempStorage, $location) {
    $rootScope.$on('$routeChangeSuccess', function(evt, current, prev) {
      current.locals.$args = tempStorage.args[$location.url()];
    });
  });

Esto tendría el inconveniente del consumo de memoria, ya que no tendríamos forma de saber cuando liberar los argumentos asociados a una ruta en el tempStorage, pero dependiendo del tipo de aplicación puede no ser problemático.

Echándole un rato se podría mejorar esta solución ligándola al histórico de navegación para saber qué datos mantener y qué datos deshechar, pero sinceramente, si tienes que llegar a ese punto creo que no merece la pena y es mejor mantener los datos en un servicio creado específicamente para ello.

Conclusiones

Después de estos dos posts hemos visto unas cuantas formas de pasar información entre controladores de angularjs, empezando por las más ortodoxas y continuando con alternativas más originales.

AngularJS obliga a utilizar un servicio intermedio para pasar parámetros de un controlador a otro, pero por suerte podemos ocultar ese hecho y construir nuestra propia API de forma que resulte más cómodo y limpio el proceso.

Una de las cosas que considero más importantes a la hora de elegir un framework o una librería es que me ofrezca suficientes puntos de extensión para personalizar aquellas cosas que no me gustan o que creo que puedo mejorar.

En el caso de AngularJS, gracias al sistema de inyección de dependencias y a los eventos que se lanzan en determinados puntos del proceso es muy fácil realizar este tipo de personalizaciones. Si a eso le unimos que Javascript, como buen lenguaje dinámico, permite modificar cualquier objeto y cambiar su comportamiento, es realmente sencillo amoldarlo todo a nuestras necesidades.


11 comentarios en “Mejorando el paso de parámetros entre controladores en AngularJS

  1. El artículo está muy bien, es un buen uso de $provide.decorator. Aún así, creo que le das demasiadas vueltas al problema con tal de no usar un servicio. Los servicios son los que hacen el trabajo y los controladores no deberían de hacer nada (bueno, servir de soporte a la vista).

    Pero como bien dices, la ventaja de angular es que es flexible para todo el mundo, y mientras puedas testear tu implementación, todos contentos.

  2. Juan María Hernández dijo:

    Gracias por tu opinión Jesús.

    Mi problema no es con los servicios que, como digo en estos posts, me parece que tienen su uso, y además muy importante. Lo que no me gusta es tener un servicio que no tiene lógica más allá de almacenar temporalmente información para pasarla entre dos rutas.

    Creo que es una construcción artificial motivada por la arquitectura de angularjs que no aporta nada al diseño y acaba acoplando artificalmente dos controladores a través de un servicio del que, realmente, no dependen.

  3. Juan María Hernández dijo:

    Pensé que lo sabías. Con la foto de twitter soy más reconocible, igual debería ponerla también en el blog ;-)

  4. Hola me pareció muy interesante tu articulo, quise implementarlo pero se vuelve null al momento de navegar a la vista del controler 2, sera que es porque mi aplicación no es single page?, me gustaria pudieras compartir el proyecto completo para ver mas a detalle todo gracias y saludos

  5. Hola Roberto,

    La técnica que se expone en el artículo sólo tiene sentido si no hay una recarga completa de la página. Si se produce una navegación a otra página distinta, el browser recargará desde cero todo el código javascript, por lo que no se mantendrá el estado de los controladores. Para ese escenario, las alternativas pasan por mantener el estado en el servidor (como se hacía tradicionalmente) o almacenar el estado localmente (por ejemplo en localStorage) y comunicar las dos páginas a través de esa información.

    Un saludo,

    Juanma

  6. Rodrigo Díaz dijo:

    Hola Jorge, tengo una pregunta este pasó de parámetros según leí sería solo de manera temporal, te cuento que necesito pasar parámetros de un controlador a otro y que este se mantenga, es decir que se almacene y sea accesible y manipulable para poder moverlo o quitarlo. Si tienes algún conocimiento te agradecería que me lo compartieras.Saludos

  7. Hola Rodrigo,

    Si quieres almacenar los parámetros “para siempre” más allá del ciclo de vida de carga/descarga del controlador, te recomiendo que uses un servicio y los almacenes allí.

    Un saludo,

    Juanma.

  8. sabes que existe el $rootScope?, pues si cuentas con el puedes hacer cosas como estas.
    controladorX {
    $rootscope.unObjetoComun = “lo que sea”;
    }
    y asi el controlador que sea injectandole el $rootScope puede acceder directamente
    otroControlador{
    var elObjetoComun = $rootScope.unObjetoComun;
    }
    en el html se llama $root.unObjetoComun

    son cuatro lineas de codigo y no es necesario nada de lo que estabas haciendo

  9. Esteban Cerón dijo:

    Hola, el $rootScope me funcionó genial, muchas gracias por compartirlo. Aunque me gustaría saber las ventajas o desventajas de pasar parámetros así.

    Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>