Compartilhe dados entre controladores AngularJS

362

Estou tentando compartilhar dados entre controladores. O caso de uso é um formulário de várias etapas; os dados inseridos em uma entrada são posteriormente utilizados em vários locais de exibição fora do controlador original. Código abaixo e em jsfiddle aqui .

HTML

<div ng-controller="FirstCtrl">
    <input type="text" ng-model="FirstName"><!-- Input entered here -->
    <br>Input is : <strong>{{FirstName}}</strong><!-- Successfully updates here -->
</div>

<hr>

<div ng-controller="SecondCtrl">
    Input should also be here: {{FirstName}}<!-- How do I automatically updated it here? -->
</div>

JS

// declare the app with no dependencies
var myApp = angular.module('myApp', []);

// make a factory to share data between controllers
myApp.factory('Data', function(){
    // I know this doesn't work, but what will?
    var FirstName = '';
    return FirstName;
});

// Step 1 Controller
myApp.controller('FirstCtrl', function( $scope, Data ){

});

// Step 2 Controller
myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.FirstName = Data.FirstName;
});

Qualquer ajuda é muito apreciada.

johnkeese
fonte

Respostas:

481

Uma solução simples é fazer com que sua fábrica retorne um objeto e permita que seus controladores trabalhem com uma referência ao mesmo objeto:

JS:

// declare the app with no dependencies
var myApp = angular.module('myApp', []);

// Create the factory that share the Fact
myApp.factory('Fact', function(){
  return { Field: '' };
});

// Two controllers sharing an object that has a string in it
myApp.controller('FirstCtrl', function( $scope, Fact ){
  $scope.Alpha = Fact;
});

myApp.controller('SecondCtrl', function( $scope, Fact ){
  $scope.Beta = Fact;
});

HTML:

<div ng-controller="FirstCtrl">
    <input type="text" ng-model="Alpha.Field">
    First {{Alpha.Field}}
</div>

<div ng-controller="SecondCtrl">
<input type="text" ng-model="Beta.Field">
    Second {{Beta.Field}}
</div>

Demonstração: http://jsfiddle.net/HEdJF/

Quando os aplicativos ficam maiores, mais complexos e difíceis de testar, você pode não querer expor o objeto inteiro da fábrica dessa maneira, mas, em vez disso, conceder acesso limitado, por exemplo, através de getters e setters:

myApp.factory('Data', function () {

    var data = {
        FirstName: ''
    };

    return {
        getFirstName: function () {
            return data.FirstName;
        },
        setFirstName: function (firstName) {
            data.FirstName = firstName;
        }
    };
});

Com essa abordagem, cabe aos controladores consumidores atualizar a fábrica com novos valores e observar as alterações para obtê-los:

myApp.controller('FirstCtrl', function ($scope, Data) {

    $scope.firstName = '';

    $scope.$watch('firstName', function (newValue, oldValue) {
        if (newValue !== oldValue) Data.setFirstName(newValue);
    });
});

myApp.controller('SecondCtrl', function ($scope, Data) {

    $scope.$watch(function () { return Data.getFirstName(); }, function (newValue, oldValue) {
        if (newValue !== oldValue) $scope.firstName = newValue;
    });
});

HTML:

<div ng-controller="FirstCtrl">
  <input type="text" ng-model="firstName">
  <br>Input is : <strong>{{firstName}}</strong>
</div>
<hr>
<div ng-controller="SecondCtrl">
  Input should also be here: {{firstName}}
</div>

Demo: http://jsfiddle.net/27mk1n1o/

tasseKATT
fonte
2
A primeira solução funcionou melhor para mim - o serviço mantinha o objeto e os controladores apenas lidavam com a referência. Mucho obrigado.
21414 Johnnieese
5
. método relógio $ âmbito $ funciona lindamente para fazer uma chamada de descanso de um escopo e aplicação dos resultados para outro escopo
aclave1
Em vez de retornar um objeto como return { FirstName: '' };ou retornar um objeto contendo métodos que interagem com um objeto, não seria preferível retornar uma instância de uma classe ( function) para que, quando você o use, ele tenha um tipo específico e não seja apenas um objeto{}" ?
RPDeshaies 19/03/2015
8
Apenas um aviso, a função de ouvinte não precisa da newValue !== oldValueverificação porque o Angular não executa a função se o oldValue e o newValue forem os mesmos. A única exceção é se você se preocupa com o valor inicial. Veja: docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
Gautham C.
@tasseKATT, funciona bem, se eu não atualizar a página. Você pode me sugerir alguma solução se eu atualizar a página ???
23415 MaNn
71

Eu prefiro não usar $watchpara isso. Em vez de atribuir todo o serviço ao escopo de um controlador, você pode atribuir apenas os dados.

JS:

var myApp = angular.module('myApp', []);

myApp.factory('MyService', function(){
  return {
    data: {
      firstName: '',
      lastName: ''
    }
    // Other methods or objects can go here
  };
});

myApp.controller('FirstCtrl', function($scope, MyService){
  $scope.data = MyService.data;
});

myApp.controller('SecondCtrl', function($scope, MyService){
   $scope.data = MyService.data;
});

HTML:

<div ng-controller="FirstCtrl">
  <input type="text" ng-model="data.firstName">
  <br>Input is : <strong>{{data.firstName}}</strong>
</div>
<hr>
<div ng-controller="SecondCtrl">
  Input should also be here: {{data.firstName}}
</div>

Como alternativa, você pode atualizar os dados do serviço com um método direto.

JS:

// A new factory with an update method
myApp.factory('MyService', function(){
  return {
    data: {
      firstName: '',
      lastName: ''
    },
    update: function(first, last) {
      // Improve this method as needed
      this.data.firstName = first;
      this.data.lastName = last;
    }
  };
});

// Your controller can use the service's update method
myApp.controller('SecondCtrl', function($scope, MyService){
   $scope.data = MyService.data;

   $scope.updateData = function(first, last) {
     MyService.update(first, last);
   }
});
Bennick
fonte
Você não usa explicitamente watch (), mas internamente o AngularJS faz para atualizar a visualização com novos valores nas variáveis ​​$ scope. Em termos de desempenho, é o mesmo ou não?
Mart Dominguez
4
Não tenho certeza do desempenho de nenhum dos métodos. Pelo que entendi, o Angular já está executando um loop digest no escopo, portanto, a configuração de expressões adicionais de observação provavelmente adiciona mais trabalho ao Angular. Você precisaria executar um teste de desempenho para realmente descobrir. Eu apenas prefiro a transferência e o compartilhamento de dados através da resposta acima do que a atualização via expressões $ watch.
bennick
2
Esta não é uma alternativa ao $ watch. É uma maneira diferente de compartilhar dados entre dois objetos Angular, neste caso controladores, sem precisar usar o $ watch.
30915 bennick
11
Na IMO, esta solução é a melhor porque é mais dentro do espírito da abordagem declarativa da Angular, em vez de adicionar instruções $ watch, que parecem um pouco mais no estilo jQuery.
JamesG 22/09/2015
8
Por que essa não é a resposta mais votada? Afinal $ relógio torna o código mais complexo
Shyamal Parikh
11

Existem várias maneiras de compartilhar dados entre controladores

  1. usando serviços
  2. usando os serviços $ state.go
  3. usando stateparams
  4. usando o rootcope

Explicação de cada método:

  1. Eu não vou explicar como já foi explicado por alguém

  2. usando $state.go

      $state.go('book.name', {Name: 'XYZ'}); 
    
      // then get parameter out of URL
      $state.params.Name;
  3. $stateparamfunciona de maneira semelhante a $state.go, você o passa como objeto do controlador emissor e coleta no controlador receptor usando stateparam

  4. usando $rootscope

    (a) envio de dados do filho para o controlador pai

      $scope.Save(Obj,function(data) {
          $scope.$emit('savedata',data); 
          //pass the data as the second parameter
      });
    
      $scope.$on('savedata',function(event,data) {
          //receive the data as second parameter
      }); 

    (b) enviar dados do pai para o controlador filho

      $scope.SaveDB(Obj,function(data){
          $scope.$broadcast('savedata',data);
      });
    
      $scope.SaveDB(Obj,function(data){`enter code here`
          $rootScope.$broadcast('saveCallback',data);
      });
Ayush Mishra
fonte
6

Criei uma fábrica que controla o escopo compartilhado entre o padrão do caminho da rota, para que você possa manter os dados compartilhados apenas quando os usuários estiverem navegando no mesmo caminho pai da rota.

.controller('CadastroController', ['$scope', 'RouteSharedScope',
    function($scope, routeSharedScope) {
      var customerScope = routeSharedScope.scopeFor('/Customer');
      //var indexScope = routeSharedScope.scopeFor('/');
    }
 ])

Portanto, se o usuário for para outro caminho de rota, por exemplo '/ Support', os dados compartilhados para o caminho '/ Customer' serão automaticamente destruídos. Mas se, em vez disso, o usuário seguir para caminhos 'filhos', como '/ Customer / 1' ou '/ Customer / list', o escopo não será destruído.

Você pode ver uma amostra aqui: http://plnkr.co/edit/OL8of9

Oberdan Nunes
fonte
Use $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){ ... })se você tem ui.router
Nuno Silva
6

Existem várias maneiras de compartilhar dados entre controladores

  • Serviços angulares
  • $ broadcast, $ emit método
  • Comunicação pai para controlador filho
  • $ rootscope

Como sabemos $rootscope não é o caminho preferível para transferência ou comunicação de dados, pois é um escopo global disponível para toda a aplicação

Para compartilhamento de dados entre controladores Js angulares, serviços angulares são práticas recomendadas, por exemplo. .factory, .service
Para referência

Em caso de transferência de dados de pai para controlador de criança que você pode dados dos pais diretamente de acesso em controlador de criança através $scope
Se você estiver usando ui-router, então você pode usar $stateParmaspara passar parâmetros URL como id, name, key, etc

$broadcasttambém é uma boa maneira de transferir dados entre controladores de pai para filho e $emittransferir dados de controladores filho para pai

HTML

<div ng-controller="FirstCtrl">
   <input type="text" ng-model="FirstName">
   <br>Input is : <strong>{{FirstName}}</strong>
</div>

<hr>

<div ng-controller="SecondCtrl">
   Input should also be here: {{FirstName}}
</div>

JS

myApp.controller('FirstCtrl', function( $rootScope, Data ){
    $rootScope.$broadcast('myData', {'FirstName': 'Peter'})
});

myApp.controller('SecondCtrl', function( $rootScope, Data ){
    $rootScope.$on('myData', function(event, data) {
       $scope.FirstName = data;
       console.log(data); // Check in console how data is coming
    });
});

Consulte o link fornecido para saber mais sobre$broadcast

ojus kulkarni
fonte
4

Solução mais simples:

Eu usei um serviço AngularJS .

Etapa 1: Criei um serviço AngularJS chamado SharedDataService.

myApp.service('SharedDataService', function () {
     var Person = {
        name: ''

    };
    return Person;
});

Etapa 2: Crie dois controladores e use o serviço criado acima.

//First Controller
myApp.controller("FirstCtrl", ['$scope', 'SharedDataService',
   function ($scope, SharedDataService) {
   $scope.Person = SharedDataService;
   }]);

//Second Controller
myApp.controller("SecondCtrl", ['$scope', 'SharedDataService',
   function ($scope, SharedDataService) {
   $scope.Person = SharedDataService;
   }]);

Etapa 3: basta usar os controladores criados na exibição.

<body ng-app="myApp">

<div ng-controller="FirstCtrl">
<input type="text" ng-model="Person.name">
<br>Input is : <strong>{{Person.name}}</strong>
</div>

<hr>

<div ng-controller="SecondCtrl">
Input should also be here: {{Person.name}}
</div>

</body>

Para ver a solução funcional para esse problema, pressione o link abaixo

https://codepen.io/wins/pen/bmoYLr

arquivo .html:

<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>

<body ng-app="myApp">

  <div ng-controller="FirstCtrl">
    <input type="text" ng-model="Person.name">
    <br>Input is : <strong>{{Person.name}}</strong>
   </div>

<hr>

  <div ng-controller="SecondCtrl">
    Input should also be here: {{Person.name}}
  </div>

//Script starts from here

<script>

var myApp = angular.module("myApp",[]);
//create SharedDataService
myApp.service('SharedDataService', function () {
     var Person = {
        name: ''

    };
    return Person;
});

//First Controller
myApp.controller("FirstCtrl", ['$scope', 'SharedDataService',
    function ($scope, SharedDataService) {
    $scope.Person = SharedDataService;
    }]);

//Second Controller
myApp.controller("SecondCtrl", ['$scope', 'SharedDataService',
    function ($scope, SharedDataService) {
    $scope.Person = SharedDataService;
}]);

</script>


</body>
</html>
wins999
fonte
11
este é um exemplo muito claro para nós, gente nova, usando angularJs. Como você compartilha / passa os dados de um relacionamento pai / filho? FirstCtrlé pai e recebe uma promessa de que SecondCtrl(filho) também precisará? Não quero fazer duas chamadas de API. Obrigado.
Chris22
1

Existe outra maneira de usar $ watch, usando angular.copy:

var myApp = angular.module('myApp', []);

myApp.factory('Data', function(){

    var service = {
        FirstName: '',
        setFirstName: function(name) {
            // this is the trick to sync the data
            // so no need for a $watch function
            // call this from anywhere when you need to update FirstName
            angular.copy(name, service.FirstName); 
        }
    };
    return service;
});


// Step 1 Controller
myApp.controller('FirstCtrl', function( $scope, Data ){

});

// Step 2 Controller
myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.FirstName = Data.FirstName;
});
Hinrich
fonte
1

Existem várias maneiras de fazer isso.

  1. Eventos - já explicados bem.

  2. ui router - explicado acima.

  3. Serviço - com o método de atualização exibido acima
  4. RUIM - Observando mudanças.
  5. Outra abordagem pai-filho, em vez de emitir e brodcast -

*

<superhero flight speed strength> Superman is here! </superhero>
<superhero speed> Flash is here! </superhero>

*

app.directive('superhero', function(){
    return {
        restrict: 'E',
        scope:{}, // IMPORTANT - to make the scope isolated else we will pollute it in case of a multiple components.
        controller: function($scope){
            $scope.abilities = [];
            this.addStrength = function(){
                $scope.abilities.push("strength");
            }
            this.addSpeed = function(){
                $scope.abilities.push("speed");
            }
            this.addFlight = function(){
                $scope.abilities.push("flight");
            }
        },
        link: function(scope, element, attrs){
            element.addClass('button');
            element.on('mouseenter', function(){
               console.log(scope.abilities);
            })
        }
    }
});
app.directive('strength', function(){
    return{
        require:'superhero',
        link: function(scope, element, attrs, superHeroCtrl){
            superHeroCtrl.addStrength();
        }
    }
});
app.directive('speed', function(){
    return{
        require:'superhero',
        link: function(scope, element, attrs, superHeroCtrl){
            superHeroCtrl.addSpeed();
        }
    }
});
app.directive('flight', function(){
    return{
        require:'superhero',
        link: function(scope, element, attrs, superHeroCtrl){
            superHeroCtrl.addFlight();
        }
    }
});
user2756335
fonte
0

Não sei onde peguei esse padrão, mas para compartilhar dados entre controladores e reduzir o escopo $ rootScope e $, isso funciona muito bem. É uma reminiscência de uma replicação de dados em que você tem editores e assinantes. Espero que ajude.

O serviço:

(function(app) {
    "use strict";
    app.factory("sharedDataEventHub", sharedDataEventHub);

    sharedDataEventHub.$inject = ["$rootScope"];

    function sharedDataEventHub($rootScope) {
        var DATA_CHANGE = "DATA_CHANGE_EVENT";
        var service = {
            changeData: changeData,
            onChangeData: onChangeData
        };
        return service;

        function changeData(obj) {
            $rootScope.$broadcast(DATA_CHANGE, obj);
        }

        function onChangeData($scope, handler) {
            $scope.$on(DATA_CHANGE, function(event, obj) {
                handler(obj);
            });
        }
    }
}(app));

O Controlador que está obtendo os novos dados, que é o Publicador, faria algo assim.

var someData = yourDataService.getSomeData();

sharedDataEventHub.changeData(someData);

O Controlador que também está usando esses novos dados, chamados de Assinante, faria algo assim ...

sharedDataEventHub.onChangeData($scope, function(data) {
    vm.localData.Property1 = data.Property1;
    vm.localData.Property2 = data.Property2;
});

Isso funcionará para qualquer cenário. Portanto, quando o controlador primário é inicializado e obtém dados, ele chamaria o método changeData, que transmitia isso para todos os assinantes desses dados. Isso reduz o acoplamento de nossos controladores entre si.

ewahner
fonte
usar um 'modelo' que compartilhe o estado entre controladores parece ser a opção mais usada. Infelizmente, isso não cobre o cenário em que você atualizaria o controlador filho. Como você 'busca' os dados do servidor e recria o modelo? E como você lida com a invalidação de cache?
Andrei
Eu acho que a idéia de controladores pai e filho é um pouco perigosa e causa muitas dependências um do outro. No entanto, quando usado em combinação com o roteador da interface do usuário, você pode atualizar facilmente os dados do seu "filho", especialmente se esses dados forem injetados através da configuração do estado. Cache invalidação acontece no publisher em que eles seria responsável por recuperar os dados e chamandosharedDataEventHub.changeData(newData)
ewahner
Eu acho que há um mal-entendido. Por atualização, quero dizer que os usuários literalmente pressionam a tecla F5 no teclado e fazem com que o navegador faça uma postagem de volta. Se você estiver na exibição filho / detalhes quando isso acontecer, não haverá ninguém para chamar "var someData = yourDataService.getSomeData ()". A questão é: como você lidaria com esse cenário? quem deve atualizar o modelo?
Andrei
Dependendo de como você ativa seus controladores, isso pode ser muito simples. Se você tiver um método que é ativado, o controlador responsável por carregar os dados inicialmente, o carregaria e usaria o sharedDataEventHub.changeData(newData)filho ou o controlador que depende desses dados teria em algum lugar do corpo do controlador o seguinte: sharedDataEventHub.onChangeData($scope, function(obj) { vm.myObject = obj.data; });Se você usou a interface do usuário -Router isso pode ser injetado na configuração do estado.
ewahner
Aqui está um exemplo que ilustra o problema que estou tentando descrever: plnkr.co/edit/OcI30YX5Jx4oHyxHEkPx?p=preview . Se você soltar o botão, navegue até a página de edição e atualize o navegador, ele travará. Quem deve recriar o modelo neste caso?
Andrei
-3

Basta fazer isso de forma simples (testada na v1.3.15)

<article ng-controller="ctrl1 as c1">
    <label>Change name here:</label>
    <input ng-model="c1.sData.name" />
    <h1>Control 1: {{c1.sData.name}}, {{c1.sData.age}}</h1>
</article>
<article ng-controller="ctrl2 as c2">
    <label>Change age here:</label>
    <input ng-model="c2.sData.age" />
    <h1>Control 2: {{c2.sData.name}}, {{c2.sData.age}}</h1>
</article>

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

    var dummy = {name: "Joe", age: 25};

    app.controller("ctrl1", function () {
        this.sData = dummy;
    });

    app.controller("ctrl2", function () {
        this.sData = dummy;
    });
</script>

Lumic
fonte
5
Eu acho que isso foi rejeitado porque não é modular. dummyé global para os dois controladores, em vez de compartilhado de maneira mais modular por herança com $ scope ou controllerAs ou usando um serviço.
Chad Johnson