Paso de parámetros entre controladores en AngularJS

A juzgar por el número de búsquedas en Google y preguntas en StackOverflow y listas de correo, una de las cosas que más confusas resultan al utilizar AngularJS es la manera de pasar parámetros de un controlador a otro.

No me refiero tanto a compartir información entre dos controladores que están activos simultáneamente, que es algo que se puede resolver fácilmente a través del anidamiento de scopes o mediante el uso de eventos, sino a cómo pasar información entre dos controladores asociados a rutas diferentes.

En este post vamos a ver las dos formas estándar de pasar información entre controladores que podemos utilizar si queremos sobrevivir al framework javascript de moda.

Camiseta I Survived Javascript
Camiseta original de Koalite’s Kipple

Parámetros en la URL

La primera opción que tenemos para pasar parámetros entre dos controladores que gestionan rutas diferentes es mediante parámetros en la URL:

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

app.config(function($routeProvider) {
  $routeProvider.
    when('/view1', {
      templateUrl: 'view1.html',
      controller: 'Ctrl1'
    }).
    when('/view2/:personId', {
      templateUrl: 'view2.html',
      controller: 'Ctrl2'
    });
});

app.controller('Ctrl1', function($scope, $location) {
  $scope.goTo2 = function(person) {
    $location.url('/view2/' + person.id);
  };
});

app.controller('Ctrl2', function($scope, $routeParams) {
  $scope.message = 'PersonId = ' + $routeParams.personId;
});

Al definir la ruta podemos utilizar marcadores, como :personId, y angularjs se encargará de que estén accessibles en el controlador asociado a la ruta mediante el objeto $routeParams. Ésta es una buena alternativa si queremos pasar datos simples entre controladores, pero en cuanto necesitamos pasar objetos más complejos que un número o un string, empieza a ser poco práctica.

Uso de servicios

La respuesta habitual a cómo pasar información entre controladores es usando servicios. Los servicios de angularjs son componentes singleton, por lo que si dos controladores tienen una dependencia de un mismo servicio, en ambos se inyectará la misma instancia y podremos usarla como «puente» para pasar la información:


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

app.factory("MyService", function() {
  return {
    data: {}
  };
});

// controlador asignado a la ruta /view1
app.controller("Ctrl1", function($scope, $location, MyService) {
  $scope.goTo2 = function() {
    MyService.data.name = "Paco";
    MyService.data.age = 31;
    $location.url("/view2");
  };
});

// controlador asignado a la ruta /view2
app.controller("Ctrl2", function($scope, MyService) {
  $scope.message = MyService.data.name + " tiene " + MyService.data.age + " años";
});

En el ejemplo anterior, Ctrl1 expone una función en su $scope que podría ser invocada desde la vista, y que se encarga de almacenar en MyService cierta información antes de cambiar a la ruta /view2, que será gestionada por Ctrl2, el cual podrá recuperar la información leyéndola del servicio.

Ésta es una opción perfectamente válida y hay escenarios en los que seguramente sea la mejor opción. Si el servicio es algo más que un mero contenedor de datos y tiene algo de lógica, o si mantiene el estado global de un proceso (por ejemplo, un carrito de la compra en una tienda online o el estado de un interfaz de usuario tipo wizard), ésta opción es bastante buena y mi única pega es que no me gusta llamar a eso un servicio, pero admito que es cuestión de gustos.

Sin embargo, cuando el servicio existe únicamente para pasar datos entre los controladores y actúa sólo como almacén de información temporal, me parece una solución antinatural y subóptima porque se basa en hacer global algo que debería ser local, en este caso un parámetro de invocación.

Además, el paso de parámetros resulta poco natural y obliga a mantener en ambos controladores una dependencia sobre el servicio intermedio que, realmente, no sirve más que como contenedor temporal de datos y complica el API.

Es comparable a hacer algo así en C#:

public static class MyService
{
  public string Name;
  public int Age;
}

public class Ctrl1
{
  public void GoTo2()
  {
    MyService.Name = "Paco";
    MyService.Age = 31;
    
    new Ctrl2().Execute();
  }
}

public class Ctrl2
{
  public void Execute()
  {
    Console.WriteLine(MyService.Name + " tiene " + MyService.Age + " años";
  }
}

No creo que nadie piense que esa es una opción razonable porque estamos acoplando las clases Ctrl1 y Ctrl2 a través del estado global almacenado en MyService, cuando lo más razonable sería pasar esa información de uno a otro a través de parámetros en el constructor de Ctrl2 o de su método Execute.

Resumen

Las dos alternativas que hemos visto son las formas «ortodoxas» de pasar información entre controladores (o rutas, según se mire), en angularjs. Ambas tienen su utilidad y sus limitaciones, pero ninguna de ellas resuelve satisfactoriamente el problema de pasar información compleja entre controladores sin «contaminar» el estado global de la aplicación.

En el próximo post vamos a ver otras dos alternativas más «imaginativas» que nos permitan paliar en cierto modo ese problema.

14 comentarios en “Paso de parámetros entre controladores en AngularJS

  1. Excelente explicación de lo que no hay que hacer en Angular o en cualquier otro framework o lenguaje de programación. Me hizo acordar a los global de php.

  2. Gran explicación. Yo me estoy decantando por la segunda por «eso» de «no contaminar» el rootscope que todo el mundo recomienda. Aunque también pensaba que inyectar un intermediario para solo pasar un string/id era matar moscas a cañonazos. Espero impaciente esa imaginación ;-)

  3. Hola, soy nuevo en el uso de AngularJs y quería incluirlo en mi proyecto fin de carrera pero me cuesta mucho encontrar tutoriales completos.
    Podrías poner un ejemplo de las dos HTML ( vista de Ctrl1 y vista de control Ctrl2) que interactuarian con «Uso de servicios «

  4. una consulta,

    Como haces para cuando devolves algo, pero que aún no esta resuelto, ejemplo:

    O sea, llamo a un $hhtp, traigo un valor de una base de datos, pero el return de la factory, me devuelve vacío, porque aún no esta el dato.

    Le debo agregar una promesa a la factory, mira te la paso, a ver si me podes ayudar:

    angular
    .module(‘inspinia’).factory(«ModuleConfig», function($http) {

    data = «»;

    //var whereJson = ‘{«where»: {«idModule»:’ + $rootScope.currentModule.idModule + ‘}}’
    var whereJson = ‘{«where»: {«idModule»:’ + 12 + ‘}}’

    $http({
    method: ‘GET’,
    url: «http://localhost:3000/api/ModulesConfigs?filter=» + whereJson
    }).then(function successCallback(res) {

    data = res.data;
    console.log(data);
    }, function errorCallback(res) {

    data = res.data;
    console.log(data);
    });

    return data;

    });

    el return data, cuando lo devuelve, no tiene nada, porque aun no resolvió la llamada.

    como se hace en estos casos?

    saludos
    eduardo

  5. En algún post he tenido que cerrar los comentarios porque no conseguía entender la mitad, y en los que entendía no sabía muy bien que decirles.

    De todas formas lo que peor llevo es la falta de educación de algunos, sobre todo por email. Que escriban mejor o peor… pues bueno, qué le vamos a hacer, pero un mínimo de educación es imprescindible.

    También tengo que reconocer que hay muchos comentarios muy buenos y que me ayudan mucho. Prefiero quedarme con esa parte :)

  6. Hola, Alguien sabe como retornar la respuesta de una WebApi a un controlador MVC y de este al javascript.

    El que me pueda ayudar se lo agradecería.

  7. Julio Izquierdo dijo:

    Muy buena explicación, pero que pasa si estando en la ruta 2 recargo el navegador, intente hacer esto y se pierden los datos del servicio, los cuales pases de una ruta a otra.

  8. Hola Julio,

    Si recargas el navegador, se pierde todo (uses el método que uses). La única alternativa a eso es utilizar algún mecanismo de persistencia, como SessionStorage, LocalStorage, Cookies, o similar.

    Un saludo,

    Juanma.

Comentarios cerrados.