Может ли один контроллер AngularJS вызвать другой?

581

Возможно ли, чтобы один контроллер использовал другой?

Например:

Этот HTML-документ просто печатает сообщение, доставленное MessageCtrlконтроллером в messageCtrl.jsфайл.

<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>

Файл контроллера содержит следующий код:

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

Который просто печатает текущую дату;

Если бы мне нужно было добавить еще один контроллер, DateCtrlкоторый вернул MessageCtrlбы дату в определенном формате , как бы это сделать? Структура DI, кажется, касается XmlHttpRequestsи доступа к услугам.

BanksySan
источник
4
В этой теме группы Google, groups.google.com/d/topic/angular/m_mn-8gnNt4/discussion , обсуждаются 5 способов взаимодействия контроллеров друг с другом.
Марк Райкок
Здесь уже есть хорошие ответы, поэтому я просто хотел бы отметить, что для конкретного упомянутого случая использования, возможно, лучшим решением будет фильтр AngularJS? Просто подумал, что упомяну это :)
Джо Диндейл,

Ответы:

705

Существует несколько способов связи между контроллерами.

Лучшим, вероятно, является предоставление услуги:

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
}

Другой способ - отправка события в области видимости:

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

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

В обоих случаях вы можете общаться с любой директивой.

Войта
источник
4
Hia, первый пример потребовал бы, чтобы веб-страница знала обо всех сервисах в стеке. Что похоже на неприятный запах (?). Как и во втором, разве веб-странице не нужно предоставлять аргумент $ scope?
BanksySan
54
Какая? Почему? Все контроллеры вводятся DI Angular.
Войта
7
@JoshNoe в 1 / у вас есть два контроллера (или больше), и они оба получают один идентичный / общий сервис. Затем у вас есть несколько способов общения, некоторые из них вы упомянули. Я бы решил на основе вашего конкретного случая использования. Вы можете поместить общую логику / состояние в службу, и оба контроллера только делегируют эту службу или даже экспортируют службу в шаблон. Конечно, служба также может инициировать события ...
Войта
137
Приходите к этому поздно: вы, ребята, знаете, что спорите с THE Vojta из Google, который работает над AngularJS, верно? :)
Suman
16
Для меня не было очевидным, что в моем HTML контроллер, генерирующий события, должен быть дочерним узлом контроллера прослушивания, чтобы он работал.
Джангонаут
122

Смотрите эту скрипку: http://jsfiddle.net/simpulton/XqDxG/

Также посмотрите следующее видео: Связь между контроллерами

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
источник
12
Вышеупомянутая скрипка и видео совместно используют сервис. Вот скрипка, которая использует $ scope. $ Emit
Марк Раджкок
1
@adardesign: Я ЛЮБЛЮ читать тот краткий и содержательный пример для директив (спасибо за этот ответ тоже!)
sscarduzio
Отличный ответ, я использую myModule.service ('mySharedService', функция ($ rootScope) {}) вместо myModule.factory, но, тем не менее, работает!
TacoEater
Отлично. Однако у меня есть вопрос: почему вы добавили обработчик в ControllerZero? $ scope. $ on ('handleBroadcast', function () {$ scope.message = sharedService.message;});
ZooZ
Представленное видео действительно потрясающее! Мне кажется, это то, что мне нужно, чтобы запросить состояние другого контроллера от другого контроллера. Однако это не работает с использованием функции «invoke». Работает с использованием действия «триггер». Таким образом, если контроллер выполняет действие и имеет новое состояние, он должен будет транслировать это состояние, и другие контроллеры должны прослушивать эту трансляцию и отвечать соответствующим образом. Или лучше выполнить действие в общем сервисе, а затем передать состояние. Пожалуйста, скажите мне, если мое понимание верно.
tarekahf
53

Если вы хотите вызвать один контроллер в другой, доступны четыре метода

  1. $ rootScope. $ emit () и $ rootScope. $ broadcast ()
  2. Если Второй контроллер дочерний, вы можете использовать родительское дочернее общение.
  3. Используйте Сервисы
  4. Вид взлома - с помощью angular.element ()

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

Контроллер и его область действия могут быть разрушены, но $ rootScope остается в приложении, поэтому мы берем $ rootScope, потому что $ rootScope является родительским для всех областей.

Если вы осуществляете общение от родителя к ребенку, и даже ребенок хочет общаться со своими братьями и сестрами, вы можете использовать $ broadcast

Если вы осуществляете связь от ребенка к родителю, братьев и сестер не будет, тогда вы можете использовать $ 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>

Код 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');
  });
});

В приведенном выше коде консоль $ emit 'childEmit' не будет вызывать внутри дочерних братьев и сестер и будет вызывать только внутри родителя, где $ broadcast также вызывается внутри братьев и сестер и родителя. Это место, где производительность вступает в действие. предпочтительнее, если вы используете связь ребенка с родителем, потому что она пропускает некоторые грязные проверки.

2. Если Второй контроллер дочерний, вы можете использовать связь Родитель-ребенок

Это один из лучших способов: если вы хотите установить связь между родителем и ребенком, когда ребенок хочет общаться с непосредственным родителем, тогда он не нуждается ни в каком виде $ broadcast или $ emit, но если вы хотите установить связь от родителя к ребенку, тогда вам придется использовать либо сервис, либо $ широковещательную рассылку

Например 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);
  });

Всякий раз, когда вы используете связь потомка с родителем, Angularjs будет искать переменную внутри потомка. Если она не присутствует внутри, тогда она выберет просмотр значений внутри родительского контроллера.

3. Используйте Услуги

AngularJS поддерживает концепции «разделения проблем» с использованием сервисной архитектуры. Сервисы являются функциями javascript и отвечают только за выполнение определенных задач. Это делает их отдельной сущностью, которую можно обслуживать и тестировать. Сервисы, используемые для внедрения с помощью механизма Dependency Injection от Angularjs.

Код 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");
});

Это даст вывод Hello Child World и Hello Parent World. Согласно Angular docs of services Singles - каждый компонент, зависящий от сервиса, получает ссылку на один экземпляр, сгенерированный фабрикой сервисов .

4. Вид взлома - с помощью angular.element ()

Этот метод получает scope () из элемента по его идентификатору / уникальному методу class.angular.element () возвращает элемент, а scope () дает переменную $ scope другой переменной, используя переменную $ scope одного контроллера внутри другого, не является хорошей практикой.

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);
  }
}); 

В приведенном выше коде контроллеры показывают свое собственное значение в Html, и когда вы нажмете на текст, вы получите соответствующие значения в консоли. Если вы нажмете на родительский диапазон контроллеров, браузер отобразит значение child и наоборот.

Шубхам Нигам
источник
52

Вот одностраничный пример двух контроллеров, совместно использующих данные сервиса:

<!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>

Также здесь: https://gist.github.com/3595424

exclsr
источник
И если theServiceобновления thing.x, то это изменение автоматически распространяется на <input> в FirstCtrlи SecondCtrl, верно? И можно также изменить thing.xнапрямую через любой из <input> (правильно?).
КаджМагнус
4
Да. Все сервисы Angular являются приложениями, что означает, что существует только один экземпляр сервиса. Ссылка: docs.angularjs.org/guide/dev_guide.services.creating_services
exclsr
Ссылка в моем предыдущем комментарии - 404, так что вот руководство по услугам, которое сегодня отмечает, что службы заметок являются одиночными: docs.angularjs.org/guide/services
exclsr
1
@exclsr Да! Извините, что пропустил это раньше
CodyBugstein
3
Безусловно лучший пример, который я когда-либо видел в сети. Спасибо
Семь Земель
33

Если вы хотите отправлять и транслировать события для обмена данными или вызова функций между контроллерами , перейдите по этой ссылке : и проверьте ответ zbynour(ответ с максимальным количеством голосов). Я цитирую его ответ !!!

Если область видимости firstCtrl является родительской для области secondCtrl, ваш код должен работать, заменив $ emit на $ broadcast в firstCtrl:

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

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

Если между вашими областями нет отношения родитель-потомок, вы можете вставить $ rootScope в контроллер и передать событие всем дочерним областям (т. Е. Также secondCtrl).

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

Наконец, когда вам нужно отправить событие из дочернего контроллера в области вверх, вы можете использовать $ scope. $ Emit. Если область видимости firstCtrl является родительской областью видимости secondCtrl:

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

function secondCtrl($scope){
    $scope.$emit('someEvent', [1,2,3]);
}
SharpCoder
источник
24

Еще две скрипки: (не сервисный подход)

1) Для контроллера $scope«родитель- ребенок» - использование родительского контроллера для передачи / трансляции событий. http://jsfiddle.net/laan_sachin/jnj6y/

2) Использование $rootScopeчерез не связанные контроллеры. http://jsfiddle.net/VxafF/

Темный рыцарь
источник
В чем причина всей этой сложности с событиями? Почему бы не сделать что-то подобное? jsfiddle.net/jnj6y/32
DFR
Это зависит от того, какие отношения родитель-ребенок правильно. Это может быть иерархия DOM, если события в случае позволят вам отделить вещи.
DarkKnight
17

На самом деле использование emit и broadcast неэффективно, потому что событие всплывает вверх и вниз по иерархии областей действия, что может легко перерасти в снижение производительности для сложного приложения.

Я бы предложил использовать сервис. Вот как я недавно реализовал это в одном из моих проектов - https://gist.github.com/3384419 .

Основная идея - зарегистрировать паб-саб / шину событий как сервис. Затем добавьте эту шину событий туда, где вам нужно подписаться или публиковать события / темы.

Numan Salati
источник
5

Я тоже знаю об этом.

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

Но я не использую его слишком часто, потому что я не люблю использовать селекторы jQuery в угловом коде.

Андрей Корчак
источник
лучший ответ. Так просто и легко ... =)
zVictor
3
@zVictor, это действительно подход «последней инстанции». Это работает, но выходит за рамки возможного, чтобы заставить вас вернуться обратно. Это использует DOM-манипуляции, чтобы заставить что-то сделать, вместо того, чтобы делать это программно. Все просто, работает, но не масштабируемо.
Брайан Ноа
2
@BrianNoah, правда. Можно использовать этот код для прототипов или некоторых экспериментов, но не для производственного кода.
Андрей Корчак
1
Это худшее, что можно сделать. Манипулирование DOM в сервисах и прямой доступ к области.
Маттиа Франчетто
3

Существует метод, не зависящий от услуг, $broadcastили $emit. Это подходит не во всех случаях, но если у вас есть 2 связанных контроллера, которые можно абстрагировать в директивы, вы можете использовать эту requireопцию в определении директивы. Это наиболее вероятно, как ngModel и ngForm общаются. Вы можете использовать это для связи между контроллерами директив, которые либо вложены, либо находятся в одном и том же элементе.

Для ситуации родитель / ребенок, использование будет следующим:

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

И основные моменты, чтобы заставить его работать: в родительской директиве с вызываемыми методами вы должны определить их на this(не на $scope):

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

В определении дочерней директивы вы можете использовать эту requireопцию, чтобы родительский контроллер был передан функции связи (чтобы вы могли затем вызывать функции из нее из scopeдочерней директивы.

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();
  }
}

Выше можно увидеть на http://plnkr.co/edit/poeq460VmQER8Gl9w8Oz?p=preview

Директива «брат» используется аналогично, но обе директивы для одного и того же элемента:

<div directive1 directive2>
</div>

Используется при создании метода для directive1:

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

И в директиве 2 это можно вызвать с помощью requireопции, которая приводит к передаче siblingController в функцию link:

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();
  }
}

Это можно увидеть по адресу http://plnkr.co/edit/MUD2snf9zvadfnDXq85w?p=preview .

Использование этого?

  • Родитель: Любой случай, когда дочерние элементы должны «зарегистрироваться» вместе с родителем. Очень похоже на отношения между ngModel и ngForm. Это может добавить определенное поведение, которое может повлиять на модели. У вас также может быть что-то, основанное исключительно на DOM, где родительский элемент должен управлять позициями определенных потомков, скажем, управлять прокруткой или реагировать на нее.

  • Брат, позволяющий директиве изменять свое поведение. ngModel - классический случай добавления парсеров / проверки к использованию ngModel на входах.

Михал Чарамза
источник
3

Я не знаю, если это не соответствует стандартам, но если у вас все ваши контроллеры в одном файле, то вы можете сделать что-то вроде этого:

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() {...}
}]);

Как вы можете видеть, IndicatorsCtrl вызывает функции updateChart двух других контроллеров при вызове updateCharts.

tomascharad
источник
2

Вы можете внедрить службу '$ controller' в свой родительский контроллер (MessageCtrl), а затем создать / внедрить дочерний контроллер (DateCtrl), используя:
$scope.childController = $controller('childController', { $scope: $scope.$new() });

Теперь вы можете получить доступ к данным из вашего дочернего контроллера, вызывая его методы как сервис.
Дайте мне знать, если возникнет проблема.

Смрутиранян Саху
источник
1

Ниже приводится publish-subscribeподход, который не зависит от Angular JS.

Контроллер параметров поиска

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

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

Контроллер вариантов поиска

//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();
        }


});

Менеджер по корпоративным мероприятиям

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);
            }
        }
    }
};

Глобальный

var EM = myBase.EventManager;
LCJ
источник
1

В Angular 1.5 это можно сделать, выполнив следующие действия:

(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({});
    }
  }

}());

Это родительский компонент. В этом я создал функцию, которая помещает другой объект в мой productFormsмассив - заметка - это всего лишь мой пример, эта функция может быть чем угодно.

Теперь мы можем создать еще один компонент, который будет использовать 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;
    };  
  }

}());

Здесь дочерний компонент создает ссылку на функцию родительского компонента, addNewFormкоторая затем может быть связана с HTML и вызываться, как и любая другая функция.

Katana24
источник