¿Puede un controlador AngularJS llamar a otro?

581

¿Es posible que un controlador use otro?

Por ejemplo:

Este documento HTML simplemente imprime un mensaje entregado por el MessageCtrlcontrolador en el messageCtrl.jsarchivo.

<html xmlns:ng="http://angularjs.org/">
<head>
    <meta charset="utf-8" />
    <title>Inter Controller Communication</title>
</head>
<body>
    <div ng:controller="MessageCtrl">
        <p>{{message}}</p>
    </div>

    <!-- Angular Scripts -->
    <script src="http://code.angularjs.org/angular-0.9.19.js" ng:autobind></script>
    <script src="js/messageCtrl.js" type="text/javascript"></script>
</body>
</html>

El archivo del controlador contiene el siguiente código:

function MessageCtrl()
{
    this.message = function() { 
        return "The current date is: " + new Date().toString(); 
    };
}

Que simplemente imprime la fecha actual;

Si tuviera que agregar otro controlador, DateCtrlque devolvió la fecha en un formato específico MessageCtrl, ¿cómo podría uno hacer esto? El marco DI parece estar preocupado XmlHttpRequestsy tener acceso a los servicios.

BanksySan
fuente
44
Este hilo de grupo de google, groups.google.com/d/topic/angular/m_mn-8gnNt4/discussion , analiza 5 formas en que los controladores pueden comunicarse entre sí.
Mark Rajcok
Ya hay buenas respuestas aquí, así que me gustaría señalar que para el caso de uso particular mencionado, ¿quizás un filtro AngularJS sería una mejor solución? Solo pensé en mencionarlo :)
Joe Dyndale

Respuestas:

705

Hay varias formas de comunicarse entre los controladores.

El mejor probablemente sea compartir un servicio:

function FirstController(someDataService) 
{
  // use the data service, bind to template...
  // or call methods on someDataService to send a request to server
}

function SecondController(someDataService) 
{
  // has a reference to the same instance of the service
  // so if the service updates state for example, this controller knows about it
}

Otra forma es emitir un evento en el alcance:

function FirstController($scope) 
{
  $scope.$on('someEvent', function(event, args) {});
  // another controller or even directive
}

function SecondController($scope) 
{
  $scope.$emit('someEvent', args);
}

En ambos casos, también puede comunicarse con cualquier directiva.

Vojta
fuente
44
Hia, el primer ejemplo requeriría que la página web conozca todos los servicios en la pila. Que se siente como un mal olor (?). Al igual que con el segundo, ¿no necesitaría la página web proporcionar el argumento $ scope?
BanksySan
54
¿Qué? ¿Por qué? Todos los controladores son inyectados por el DI de Angular.
Vojta
77
@JoshNoe en 1 / tiene dos controladores (o más) y ambos obtienen un servicio idéntico / compartido. Luego, tiene varias formas de comunicarse, algunas de las cuales mencionó. Decidiría según su caso de uso específico. Puede poner la lógica / estado compartido en el servicio y ambos controladores solo delegan a ese servicio o incluso exportan el servicio a la plantilla. Por supuesto, el servicio también puede disparar eventos ...
Vojta
137
Llegando a esta tarde: ustedes saben que están discutiendo con THE Vojta de Google que trabaja en AngularJS, ¿verdad? :)
Suman
16
Para mí no era obvio que en mi HTML el controlador emisor de eventos tiene que ser un nodo secundario del controlador de escucha para que funcione.
djangonaut
122

Vea este violín: http://jsfiddle.net/simpulton/XqDxG/

También vea el siguiente video: Comunicación entre controladores

HTML:

<div ng-controller="ControllerZero">
  <input ng-model="message" >
  <button ng-click="handleClick(message);">LOG</button>
</div>

<div ng-controller="ControllerOne">
  <input ng-model="message" >
</div>

<div ng-controller="ControllerTwo">
  <input ng-model="message" >
</div>

javascript:

var myModule = angular.module('myModule', []);
myModule.factory('mySharedService', function($rootScope) {
  var sharedService = {};

  sharedService.message = '';

  sharedService.prepForBroadcast = function(msg) {
    this.message = msg;
    this.broadcastItem();
  };

  sharedService.broadcastItem = function() {
    $rootScope.$broadcast('handleBroadcast');
  };

  return sharedService;
});

function ControllerZero($scope, sharedService) {
  $scope.handleClick = function(msg) {
    sharedService.prepForBroadcast(msg);
  };

  $scope.$on('handleBroadcast', function() {
    $scope.message = sharedService.message;
  });        
}

function ControllerOne($scope, sharedService) {
  $scope.$on('handleBroadcast', function() {
    $scope.message = 'ONE: ' + sharedService.message;
  });        
}

function ControllerTwo($scope, sharedService) {
  $scope.$on('handleBroadcast', function() {
    $scope.message = 'TWO: ' + sharedService.message;
  });
}

ControllerZero.$inject = ['$scope', 'mySharedService'];        

ControllerOne.$inject = ['$scope', 'mySharedService'];

ControllerTwo.$inject = ['$scope', 'mySharedService'];
adardesign
fuente
12
El violín y el video anteriores comparten un servicio. Aquí hay un violín que usa $ scope. $ Emit
Mark Rajcok
1
@adardesign: ME ENCANTARÍA leer el mismo ejemplo sucinto y significativo para las directivas (¡gracias por esta respuesta también!)
sscarduzio
Gran respuesta, uso myModule.service ('mySharedService', function ($ rootScope) {}) en lugar de myModule.factory, pero no funciona.
TacoEater
Excelente. Sin embargo, tengo una pregunta: ¿por qué agregaste un controlador dentro de ControllerZero? $ scope. $ on ('handleBroadcast', function () {$ scope.message = sharedService.message;});
ZooZ
¡El video proporcionado es realmente increíble! Parece que esto es lo que necesito para consultar el estado de otro controlador desde otro controlador. Sin embargo, esto no funciona con la función "invocar". Funciona utilizando la acción "disparador". Entonces, efectivamente, si un controlador lleva a cabo una acción y tiene un nuevo estado, entonces tendrá que transmitir el estado, y depende de otros controladores escuchar esa transmisión y responder en consecuencia. O mejor, realice la acción en el servicio compartido, luego difunda el estado. Por favor, dime si mi comprensión es correcta.
tarekahf
53

Si desea llamar un controlador a otro, hay cuatro métodos disponibles

  1. $ rootScope. $ emit () y $ rootScope. $ broadcast ()
  2. Si el segundo controlador es hijo, puede utilizar la comunicación padre-hijo.
  3. Usar servicios
  4. Tipo de pirateo: con la ayuda de angular.element ()

1. $ rootScope. $ Emit () y $ rootScope. $ Broadcast ()

El controlador y su alcance pueden destruirse, pero $ rootScope permanece en toda la aplicación, por eso estamos tomando $ rootScope porque $ rootScope es el padre de todos los ámbitos.

Si está realizando una comunicación de padre a hijo e incluso el niño quiere comunicarse con sus hermanos, puede usar $ broadcast

Si está realizando una comunicación de hijo a padre, no se invocan hermanos, entonces puede usar $ rootScope. $ Emit

HTML

<body ng-app="myApp">
    <div ng-controller="ParentCtrl" class="ng-scope">
      // ParentCtrl
      <div ng-controller="Sibling1" class="ng-scope">
        // Sibling first controller
      </div>
      <div ng-controller="Sibling2" class="ng-scope">
        // Sibling Second controller
        <div ng-controller="Child" class="ng-scope">
          // Child controller
        </div>
      </div>
    </div>
</body>

Código Angularjs

 var app =  angular.module('myApp',[]);//We will use it throughout the example 
    app.controller('Child', function($rootScope) {
      $rootScope.$emit('childEmit', 'Child calling parent');
      $rootScope.$broadcast('siblingAndParent');
    });

app.controller('Sibling1', function($rootScope) {
  $rootScope.$on('childEmit', function(event, data) {
    console.log(data + ' Inside Sibling one');
  });
  $rootScope.$on('siblingAndParent', function(event, data) {
    console.log('broadcast from child in parent');
  });
});

app.controller('Sibling2', function($rootScope) {
  $rootScope.$on('childEmit', function(event, data) {
    console.log(data + ' Inside Sibling two');
  });
  $rootScope.$on('siblingAndParent', function(event, data) {
    console.log('broadcast from child in parent');
  });
});

app.controller('ParentCtrl', function($rootScope) {
  $rootScope.$on('childEmit', function(event, data) {
    console.log(data + ' Inside parent controller');
  });
  $rootScope.$on('siblingAndParent', function(event, data) {
    console.log('broadcast from child in parent');
  });
});

En la consola de código anterior de $ emit, 'childEmit' no llamará dentro de los hermanos menores y solo llamará dentro de los padres, donde $ broadcast también se llamará dentro de los hermanos y los padres. Este es el lugar donde el rendimiento entra en acción. $ Emit es preferible, si está utilizando comunicación de niño a padre porque omite algunos cheques sucios.

2. Si el segundo controlador es hijo, puede utilizar la comunicación Hijo-padre

Es uno de los mejores métodos, si desea comunicarse con los padres de los niños donde el niño quiere comunicarse con los padres inmediatos, entonces no necesitaría ningún tipo de $ broadcast o $ emit, pero si desea comunicarse de padres a hijos, entonces debe use el servicio o $ broadcast

Por ejemplo HTML: -

<div ng-controller="ParentCtrl">
 <div ng-controller="ChildCtrl">
 </div>
</div>

Angularjs

 app.controller('ParentCtrl', function($scope) {
   $scope.value='Its parent';
      });
  app.controller('ChildCtrl', function($scope) {
   console.log($scope.value);
  });

Siempre que esté utilizando la comunicación de niño a padre, Angularjs buscará una variable dentro del niño. Si no está presente en el interior, elegirá ver los valores dentro del controlador padre.

3.Utilice servicios

AngularJS admite los conceptos de "separación de preocupaciones" utilizando la arquitectura de servicios. Los servicios son funciones de JavaScript y son responsables de realizar solo tareas específicas. Esto los convierte en una entidad individual que es mantenible y comprobable servicios solían inyectarse usando el mecanismo de inyección de dependencia de Angularjs.

Código Angularjs:

app.service('communicate',function(){
  this.communicateValue='Hello';
});

app.controller('ParentCtrl',function(communicate){//Dependency Injection
  console.log(communicate.communicateValue+" Parent World");
});

app.controller('ChildCtrl',function(communicate){//Dependency Injection
  console.log(communicate.communicateValue+" Child World");
});

Dará salida Hello Child World y Hello Parent World. De acuerdo con documentos angulares de servicios Singletons: cada componente dependiente de un servicio obtiene una referencia a la instancia única generada por la fábrica de servicios .

4.Tipo de pirateo: con la ayuda de angular.element ()

Este método obtiene el alcance () del elemento por su Id / unique class.angular.element () el método devuelve el elemento y el alcance () da la variable $ scope de otra variable usando la variable $ scope de un controlador dentro de otro no es una buena práctica.

HTML: -

<div id='parent' ng-controller='ParentCtrl'>{{varParent}}
 <span ng-click='getValueFromChild()'>Click to get ValueFormChild</span>
 <div id='child' ng-controller='childCtrl'>{{varChild}}
   <span ng-click='getValueFromParent()'>Click to get ValueFormParent </span>
 </div>
</div>

Angularjs: -

app.controller('ParentCtrl',function($scope){
 $scope.varParent="Hello Parent";
  $scope.getValueFromChild=function(){
  var childScope=angular.element('#child').scope();
  console.log(childScope.varChild);
  }
});

app.controller('ChildCtrl',function($scope){
 $scope.varChild="Hello Child";
  $scope.getValueFromParent=function(){
  var parentScope=angular.element('#parent').scope();
  console.log(parentScope.varParent);
  }
}); 

En el código anterior, los controladores muestran su propio valor en Html y cuando haga clic en el texto obtendrá los valores en la consola en consecuencia. Si hace clic en el intervalo de los controladores principales, el navegador consolará el valor de child y viceversa.

Shubham Nigam
fuente
52

Aquí hay un ejemplo de una página de dos controladores que comparten datos de servicio:

<!doctype html>
<html ng-app="project">
<head>
    <title>Angular: Service example</title>
    <script src="http://code.angularjs.org/angular-1.0.1.js"></script>
    <script>
var projectModule = angular.module('project',[]);

projectModule.factory('theService', function() {  
    return {
        thing : {
            x : 100
        }
    };
});

function FirstCtrl($scope, theService) {
    $scope.thing = theService.thing;
    $scope.name = "First Controller";
}

function SecondCtrl($scope, theService) {   
    $scope.someThing = theService.thing; 
    $scope.name = "Second Controller!";
}
    </script>
</head>
<body>  
    <div ng-controller="FirstCtrl">
        <h2>{{name}}</h2>
        <input ng-model="thing.x"/>         
    </div>

    <div ng-controller="SecondCtrl">
        <h2>{{name}}</h2>
        <input ng-model="someThing.x"/>             
    </div>
</body>
</html>

También aquí: https://gist.github.com/3595424

exclsr
fuente
Y si se theServiceactualiza thing.x, entonces ese cambio se propaga automáticamente a <input> s en FirstCtrly SecondCtrl, ¿verdad? Y también se puede cambiar thing.xdirectamente a través de cualquiera de los dos <input> s (¿verdad?).
KajMagnus
44
Si. Todos los servicios de Angular son aplicaciones únicas, lo que significa que solo hay una instancia del Servicio. Referencia: docs.angularjs.org/guide/dev_guide.services.creating_services
exclsr
El enlace en mi comentario anterior es 404, así que aquí está la guía de servicios, hoy, que señala que los servicios son únicos: docs.angularjs.org/guide/services
exclsr
1
@exclsr ¡Sí! Lo siento, me perdí eso antes
CodyBugstein
3
Con mucho, el mejor ejemplo que he visto en la web hasta ahora. Gracias
Sevenearths
33

Si está buscando emitir y transmitir eventos para compartir datos o funciones de llamada entre los controladores , mire este enlace : y verifique la respuesta por zbynour(respuesta con el máximo de votos). ¡Estoy citando su respuesta!

Si el alcance de firstCtrl es padre del alcance de secondCtrl, su código debería funcionar reemplazando $ emit por $ broadcast en firstCtrl:

function firstCtrl($scope){
    $scope.$broadcast('someEvent', [1,2,3]);
}

function secondCtrl($scope){
    $scope.$on('someEvent', function(event, mass) {console.log(mass)});
}

En caso de que no haya una relación padre-hijo entre sus ámbitos, puede inyectar $ rootScope en el controlador y transmitir el evento a todos los ámbitos secundarios (es decir, también secondCtrl).

function firstCtrl($rootScope){
    $rootScope.$broadcast('someEvent', [1,2,3]);
}

Finalmente, cuando necesite despachar el evento desde el controlador secundario a los ámbitos hacia arriba, puede usar $ scope. $ Emit. Si el alcance de firstCtrl es padre del alcance de secondCtrl:

function firstCtrl($scope){
    $scope.$on('someEvent', function(event, data) { console.log(data); });
}

function secondCtrl($scope){
    $scope.$emit('someEvent', [1,2,3]);
}
SharpCoder
fuente
24

Dos violines más: (enfoque sin servicio)

1) Para el controlador $scopepadre -hijo: uso del controlador padre para emitir / transmitir eventos. http://jsfiddle.net/laan_sachin/jnj6y/

2) Uso a $rootScopetravés de controladores no relacionados. http://jsfiddle.net/VxafF/

Caballero oscuro
fuente
¿Qué razón de toda esta complejidad con los eventos? ¿Por qué no hacer algo como esto? jsfiddle.net/jnj6y/32
Dfr
Depende de qué tipo de relación entre padres e hijos sea correcta. Puede ser una jerarquía DOM, ya que los eventos de caso te permitirán desacoplar cosas.
DarkKnight
17

En realidad, el uso de emitir y emitir es ineficiente porque el evento sube y baja en la jerarquía del alcance, lo que puede degradarse fácilmente en una botella de rendimiento para una aplicación compleja.

Sugeriría usar un servicio. Así es como lo implementé recientemente en uno de mis proyectos: https://gist.github.com/3384419 .

Idea básica: registrar un bus pub-sub / event como servicio. Luego inyecte ese bus de eventos donde necesite suscribirse o publicar eventos / temas.

numan salati
fuente
5

También sé de esta manera.

angular.element($('#__userProfile')).scope().close();

Pero no lo uso demasiado, porque no me gusta usar selectores jQuery en código angular.

Andrey Korchak
fuente
La mejor respuesta. Tan simple y fácil ... =)
zVictor
3
@zVictor, este es realmente un enfoque de "último recurso". Funciona, pero está saliendo del alcance para forzar su regreso. Esto está utilizando la manipulación DOM para forzar algo a hacer en lugar de hacerlo programáticamente. Es simple, funciona, pero no es escalable.
Brian Noah
2
@BrianNoah, cierto. Está bien usar este código para prototipos o algunos experimentos, pero no para el código de producción.
Andrey Korchak
1
Eso es lo peor que se puede hacer. Manipulación DOM en servicios y acceso directo al ámbito.
Mattia Franchetto
3

Hay un método que no depende de los servicios, $broadcasto $emit. No es adecuado en todos los casos, pero si tiene 2 controladores relacionados que pueden resumirse en directivas, puede usar la requireopción en la definición de directiva. Es muy probable que así se comuniquen ngModel y ngForm. Puede usar esto para comunicarse entre controladores de directivas que están anidadas o en el mismo elemento.

Para una situación padre / hijo, el uso sería el siguiente:

<div parent-directive>
  <div inner-directive></div>
</div>

Y los puntos principales para que funcione: en la directiva principal, con los métodos que se deben llamar, debe definirlos en this(no en $scope):

controller: function($scope) {
  this.publicMethodOnParentDirective = function() {
    // Do something
  }
}

En la definición de la directiva secundaria, puede usar la requireopción para que el controlador principal pase a la función de enlace (para que luego pueda llamar a las funciones desde la scopedirectiva secundaria.

require: '^parentDirective',
template: '<span ng-click="onClick()">Click on this to call parent directive</span>',
link: function link(scope, iElement, iAttrs, parentController) {
  scope.onClick = function() {
    parentController.publicMethodOnParentDirective();
  }
}

Lo anterior se puede ver en http://plnkr.co/edit/poeq460VmQER8Gl9w8Oz?p=preview

Una directiva entre hermanos se usa de manera similar, pero ambas directivas sobre el mismo elemento:

<div directive1 directive2>
</div>

Usado al crear un método en directive1:

controller: function($scope) {
  this.publicMethod = function() {
    // Do something
  }
}

Y en directive2 esto se puede llamar mediante el uso de la requireopción que da como resultado que el siblingController se pase a la función de enlace:

require: 'directive1',
template: '<span ng-click="onClick()">Click on this to call sibling directive1</span>',
link: function link(scope, iElement, iAttrs, siblingController) {
  scope.onClick = function() {
    siblingController.publicMethod();
  }
}

Esto se puede ver en http://plnkr.co/edit/MUD2snf9zvadfnDXq85w?p=preview .

Los usos de esto?

  • Parent: Cualquier caso en el que los elementos secundarios necesitan "registrarse" con un padre. Al igual que la relación entre ngModel y ngForm. Estos pueden agregar cierto comportamiento que puede afectar a los modelos. Es posible que también tenga algo basado exclusivamente en DOM, donde un elemento padre necesita administrar las posiciones de ciertos niños, por ejemplo, administrar o reaccionar al desplazamiento.

  • Hermano: permitir que una directiva modifique su comportamiento. ngModel es el caso clásico, para agregar analizadores / validación al uso de ngModel en las entradas.

Michal Charemza
fuente
3

No sé si esto está fuera de los estándares, pero si tiene todos sus controladores en el mismo archivo, puede hacer algo como esto:

app = angular.module('dashboardBuzzAdmin', ['ngResource', 'ui.bootstrap']);

var indicatorsCtrl;
var perdiosCtrl;
var finesCtrl;

app.controller('IndicatorsCtrl', ['$scope', '$http', function ($scope, $http) {
  indicatorsCtrl = this;
  this.updateCharts = function () {
    finesCtrl.updateChart();
    periodsCtrl.updateChart();
  };
}]);

app.controller('periodsCtrl', ['$scope', '$http', function ($scope, $http) {
  periodsCtrl = this;
  this.updateChart = function() {...}
}]);

app.controller('FinesCtrl', ['$scope', '$http', function ($scope, $http) {
  finesCtrl = this;
  this.updateChart = function() {...}
}]);

Como puede ver los indicadores Ctrl está llamando a las funciones updateChart de los otros dos controladores al llamar a updateCharts.

tomascharad
fuente
2

Puede inyectar el servicio '$ controller' en su controlador principal (MessageCtrl) y luego instanciar / inyectar el controlador secundario (DateCtrl) usando:
$scope.childController = $controller('childController', { $scope: $scope.$new() });

Ahora puede acceder a los datos de su controlador secundario llamando a sus métodos, ya que es un servicio.
Avísame si hay algún problema.

Smrutiranjan Sahu
fuente
1

El siguiente es un publish-subscribeenfoque que es independiente de Angular JS.

Controlador de parámetros de búsqueda

//Note: Multiple entities publish the same event
regionButtonClicked: function () 
{
        EM.fireEvent('onSearchParamSelectedEvent', 'region');
},

plantButtonClicked: function () 
{
        EM.fireEvent('onSearchParamSelectedEvent', 'plant');
},

Controlador de opciones de búsqueda

//Note: It subscribes for the 'onSearchParamSelectedEvent' published by the Search Param Controller
localSubscribe: function () {
        EM.on('onSearchParamSelectedEvent', this.loadChoicesView, this);

});


loadChoicesView: function (e) {

        //Get the entity name from eData attribute which was set in the event manager
        var entity = $(e.target).attr('eData');

        console.log(entity);

        currentSelectedEntity = entity;
        if (entity == 'region') {
            $('.getvalue').hide();
            this.loadRegionsView();
            this.collapseEntities();
        }
        else if (entity == 'plant') {
            $('.getvalue').hide();
            this.loadPlantsView();
            this.collapseEntities();
        }


});

Administrador de evento

myBase.EventManager = {

    eventArray:new Array(),


    on: function(event, handler, exchangeId) {
        var idArray;
        if (this.eventArray[event] == null) {
            idArray = new Array();
        } else { 
            idArray = this.eventArray[event];
        }
        idArray.push(exchangeId);
        this.eventArray[event] = idArray;

        //Binding using jQuery
        $(exchangeId).bind(event, handler);
    },

    un: function(event, handler, exchangeId) {

        if (this.eventArray[event] != null) {
            var idArray = this.eventArray[event];
            idArray.pop(exchangeId);
            this.eventArray[event] = idArray;

            $(exchangeId).unbind(event, handler);
        }
    },

    fireEvent: function(event, info) {
        var ids = this.eventArray[event];

        for (idindex = 0; idindex < ids.length; idindex++) {
            if (ids[idindex]) {

                //Add attribute eData
                $(ids[idindex]).attr('eData', info);
                $(ids[idindex]).trigger(event);
            }
        }
    }
};

Global

var EM = myBase.EventManager;
LCJ
fuente
1

En angular 1.5 esto se puede lograr haciendo lo siguiente:

(function() {
  'use strict';

  angular
    .module('app')
    .component('parentComponent',{
      bindings: {},
      templateUrl: '/templates/products/product.html',
      controller: 'ProductCtrl as vm'
    });

  angular
    .module('app')
    .controller('ProductCtrl', ProductCtrl);

  function ProductCtrl() {
    var vm = this;
    vm.openAccordion = false;

    // Capture stuff from each of the product forms
    vm.productForms = [{}];

    vm.addNewForm = function() {
      vm.productForms.push({});
    }
  }

}());

Este es el componente principal. En esto, he creado una función que empuja a otro objeto en mi productFormsmatriz, nota: este es solo mi ejemplo, esta función puede ser cualquier cosa realmente.

Ahora podemos crear otro componente que haga uso de require:

(function() {
  'use strict';

  angular
    .module('app')
    .component('childComponent', {
      bindings: {},
      require: {
        parent: '^parentComponent'
      },
      templateUrl: '/templates/products/product-form.html',
      controller: 'ProductFormCtrl as vm'
    });

  angular
    .module('app')
    .controller('ProductFormCtrl', ProductFormCtrl);

  function ProductFormCtrl() {
    var vm = this;

    // Initialization - make use of the parent controllers function
    vm.$onInit = function() {
      vm.addNewForm = vm.parent.addNewForm;
    };  
  }

}());

Aquí, el componente secundario está creando una referencia a la función del componente primario addNewFormque luego puede vincularse al HTML y llamarse como cualquier otra función.

Katana24
fuente