Injetando $ scope em uma função de serviço angular ()

107

Eu tenho um serviço:

angular.module('cfd')
  .service('StudentService', [ '$http',
    function ($http) {
    // get some data via the $http
    var path = 'data/people/students.json';
    var students = $http.get(path).then(function (resp) {
      return resp.data;
    });     
    //save method create a new student if not already exists
    //else update the existing object
    this.save = function (student) {
      if (student.id == null) {
        //if this is new student, add it in students array
        $scope.students.push(student);
      } else {
        //for existing student, find this student using id
        //and update it.
        for (i in students) {
          if (students[i].id == student.id) {
            students[i] = student;
          }
        }
      }
    };

Mas quando ligo save(), não tenho acesso ao $scopee recebo ReferenceError: $scope is not defined. Portanto, a etapa lógica (para mim) é fornecer save () com o $scopee, portanto, também devo fornecer / injetá-lo no service. Então, se eu fizer assim:

  .service('StudentService', [ '$http', '$scope',
                      function ($http, $scope) {

Estou tendo o erro a seguir:

Erro: [$ injector: despr] Provedor desconhecido: $ scopeProvider <- $ scope <- StudentService

O link do erro (uau que legal!) Me informa que é relacionado ao injetor, e pode ter a ver com a ordem de declaração dos arquivos js. Tentei reordená-los no index.html, mas acho que é algo mais simples, como a forma como estou injetando.

Usando Angular-UI e Angular-UI-Router

Chris Frisina
fonte

Respostas:

183

O $scopeque você vê sendo injetado nos controladores não é um serviço (como o resto das coisas injetáveis), mas é um objeto Scope. Muitos objetos de escopo podem ser criados (geralmente herdando prototipicamente de um escopo pai). A raiz de todos os escopos é o $rootScopee você pode criar um novo escopo filho usando o $new()método de qualquer escopo (incluindo o $rootScope).

O objetivo de um Escopo é "unir" a apresentação e a lógica de negócios de seu aplicativo. Não faz muito sentido passar um $scopepara um serviço.

Serviços são objetos singleton usados ​​(entre outras coisas) para compartilhar dados (por exemplo, entre vários controladores) e geralmente encapsulam pedaços de código reutilizáveis ​​(uma vez que podem ser injetados e oferecer seus "serviços" em qualquer parte de seu aplicativo que precise deles: controladores, diretivas, filtros, outros serviços, etc.).

Tenho certeza de que várias abordagens funcionariam para você. Uma é a seguinte:
como o StudentServiceé responsável por lidar com os dados dos alunos, você pode StudentServicemanter uma matriz de alunos e deixá-la "compartilhá-la" com quem estiver interessado (por exemplo, o seu $scope). Isso faz ainda mais sentido, se houver outras visualizações / controladores / filtros / serviços que precisam ter acesso a essas informações (se não houver nenhuma agora, não se surpreenda se elas começarem a aparecer logo).
Cada vez que um novo aluno é adicionado (usando o save()método do serviço ), a própria matriz de alunos do serviço será atualizada e todos os outros compartilhamentos de objetos dessa matriz também serão atualizados automaticamente.

Com base na abordagem descrita acima, seu código pode ser assim:

angular.
  module('cfd', []).

  factory('StudentService', ['$http', '$q', function ($http, $q) {
    var path = 'data/people/students.json';
    var students = [];

    // In the real app, instead of just updating the students array
    // (which will be probably already done from the controller)
    // this method should send the student data to the server and
    // wait for a response.
    // This method returns a promise to emulate what would happen 
    // when actually communicating with the server.
    var save = function (student) {
      if (student.id === null) {
        students.push(student);
      } else {
        for (var i = 0; i < students.length; i++) {
          if (students[i].id === student.id) {
            students[i] = student;
            break;
          }
        }
      }

      return $q.resolve(student);
    };

    // Populate the students array with students from the server.
    $http.get(path).then(function (response) {
      response.data.forEach(function (student) {
        students.push(student);
      });
    });

    return {
      students: students,
      save: save
    };     
  }]).

  controller('someCtrl', ['$scope', 'StudentService', 
    function ($scope, StudentService) {
      $scope.students = StudentService.students;
      $scope.saveStudent = function (student) {
        // Do some $scope-specific stuff...

        // Do the actual saving using the StudentService.
        // Once the operation is completed, the $scope's `students`
        // array will be automatically updated, since it references
        // the StudentService's `students` array.
        StudentService.save(student).then(function () {
          // Do some more $scope-specific stuff, 
          // e.g. show a notification.
        }, function (err) {
          // Handle the error.
        });
      };
    }
]);

Uma coisa que você deve ter cuidado ao usar essa abordagem é nunca reatribuir o array do serviço, porque então quaisquer outros componentes (por exemplo, escopos) ainda farão referência ao array original e seu aplicativo será interrompido.
Por exemplo, para limpar a matriz em StudentService:

/* DON'T DO THAT   */  
var clear = function () { students = []; }

/* DO THIS INSTEAD */  
var clear = function () { students.splice(0, students.length); }

Veja, também, esta pequena demonstração .


LITTLE UPDATE:

Algumas palavras para evitar a confusão que pode surgir ao falar sobre o uso de um serviço, mas não criá-lo com a service()função.

Citando os documentos em$provide :

Um serviço Angular é um objeto singleton criado por uma fábrica de serviços . Essas fábricas de serviços são funções que, por sua vez, são criadas por um provedor de serviços . Os provedores de serviços são funções construtoras. Quando instanciados, eles devem conter uma propriedade chamada $get, que contém a função de fábrica de serviço .
[...]
... o $provideserviço tem métodos auxiliares adicionais para registrar serviços sem especificar um provedor:

  • provedor (provedor) - registra um provedor de serviços com o $ injetor
  • constante (obj) - registra um valor / objeto que pode ser acessado por provedores e serviços.
  • valor (obj) - registra um valor / objeto que só pode ser acessado por serviços, não por provedores.
  • factory (fn) - registra uma função de fábrica de serviço, fn, que será empacotada em um objeto de provedor de serviço, cuja propriedade $ get conterá a função de fábrica fornecida.
  • serviço (classe) - registra uma função construtora, classe que será envolvida em um objeto de provedor de serviço, cuja propriedade $ get instanciará um novo objeto usando a função construtora fornecida.

Basicamente, o que ele diz é que todo serviço Angular é registrado usando $provide.provider(), mas existem métodos de "atalho" para serviços mais simples (dois dos quais são service()e factory()).
Tudo se resume a um serviço, então não faz muita diferença o método que você usa (contanto que os requisitos para seu serviço possam ser cobertos por esse método).

BTW, providervs servicevs factoryé um dos conceitos mais confusos para os novatos do Angular, mas felizmente existem muitos recursos (aqui no SO) para tornar as coisas mais fáceis. (Basta pesquisar.)

(Espero que isso esclareça - me avise se não resolver).

gkalpak
fonte
1
Uma questão. Você diz serviço, mas seu exemplo de código usa a fábrica. Estou apenas começando a entender a diferença entre fábricas, serviços e fornecedores, só quero ter certeza de que ir com uma fábrica é a melhor opção, já que estava usando um serviço. Aprendi muito com seu exemplo. Obrigado pelo violino e explicação MUITO clara.
Chris Frisina,
3
@chrisFrisina: Atualizou a resposta com uma pequena explicação. Basicamente, não faz muita diferença se você usar serviceou factory- você terminará com um serviço Angular . Apenas certifique-se de entender como cada um funciona e se ele atende às suas necessidades.
gkalpak de
Bela postagem! Isso me ajuda bastante !
Oni1 de
Obrigado mano! aqui está um bom artigo sobre assunto semelhante stsc3000.github.io/blog/2013/10/26/…
Terafor
@ExpertSystem $scope.studentsficará vazio se a chamada ajax não for concluída? Ou $scope.studentsserá parcialmente preenchido, se este bloco de código estiver funcionando em andamento? students.push(student);
Yc Zhang
18

Em vez de tentar modificar o $scopedentro do serviço, você pode implementar um $watchem seu controlador para observar uma propriedade em seu serviço quanto a alterações e, em seguida, atualizar uma propriedade no $scope. Aqui está um exemplo que você pode experimentar em um controlador:

angular.module('cfd')
    .controller('MyController', ['$scope', 'StudentService', function ($scope, StudentService) {

        $scope.students = null;

        (function () {
            $scope.$watch(function () {
                return StudentService.students;
            }, function (newVal, oldVal) {
                if ( newValue !== oldValue ) {
                    $scope.students = newVal;
                }
            });
        }());
    }]);

Uma coisa a observar é que dentro do seu serviço, para que a studentspropriedade seja visível, ela precisa estar no objeto Serviço ou algo thisassim:

this.students = $http.get(path).then(function (resp) {
  return resp.data;
});
Keith Morris
fonte
12

Bem (longa) ... se você insiste em ter $scopeacesso dentro de um serviço, você pode:

Crie um serviço getter / setter

ngapp.factory('Scopes', function (){
  var mem = {};
  return {
    store: function (key, value) { mem[key] = value; },
    get: function (key) { return mem[key]; }
  };
});

Injete-o e armazene o escopo do controlador nele

ngapp.controller('myCtrl', ['$scope', 'Scopes', function($scope, Scopes) {
  Scopes.store('myCtrl', $scope);
}]);

Agora, pegue o escopo dentro de outro serviço

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);
Jonatas Walker
fonte
Como os escopos estão sendo destruídos?
JK.
9

Os serviços são singletons e não é lógico que um escopo seja injetado no serviço (o que é o caso, você não pode injetar o escopo no serviço). Você pode passar o escopo como um parâmetro, mas isso também é uma escolha de design ruim, porque você teria o escopo sendo editado em vários lugares, dificultando a depuração. O código para lidar com variáveis ​​de escopo deve ir para o controlador e as chamadas de serviço vão para o serviço.

Ermin Dedovic
fonte
Eu entendo o que você está dizendo. No entanto, no meu caso, tenho muitos controladores e gostaria de configurar seus escopos com um conjunto muito semelhante de relógios $. Como / onde você faria isso? Atualmente, eu realmente passo o escopo como um parâmetro para um serviço que define os $ relógios.
moritz
@moritz talvez implemente uma diretiva secundária (uma que tenha escopo: false, para que use o escopo definido por outras diretivas) e que faça as ligações do watchess, bem como qualquer outra coisa que você precise. Dessa forma, você pode usar essa outra diretiva em qualquer lugar que precise para definir esses relógios. Porque passar o escopo para um serviço é realmente horrível :) (acredite, eu estive lá, fiz isso, bati minha cabeça contra a parede no final)
tfrascaroli
@TIMINeutron que soa muito melhor do que passar ao redor do osciloscópio, vou tentar isso da próxima vez que o cenário surgir! Obrigado!
moritz
Certo. Ainda estou aprendendo sozinho, e este problema específico é um que recentemente enfrentei dessa maneira particular, e funcionou como um encanto para mim.
tfrascaroli
3

Você pode tornar seu serviço completamente inconsciente do escopo, mas em seu controlador permitir que o escopo seja atualizado de forma assíncrona.

O problema que você está tendo é porque você não sabe que as chamadas http são feitas de forma assíncrona, o que significa que você não obtém um valor imediatamente como deveria. Por exemplo,

var students = $http.get(path).then(function (resp) {
  return resp.data;
}); // then() returns a promise object, not resp.data

Existe uma maneira simples de contornar isso e é fornecer uma função de retorno de chamada.

.service('StudentService', [ '$http',
    function ($http) {
    // get some data via the $http
    var path = '/students';

    //save method create a new student if not already exists
    //else update the existing object
    this.save = function (student, doneCallback) {
      $http.post(
        path, 
        {
          params: {
            student: student
          }
        }
      )
      .then(function (resp) {
        doneCallback(resp.data); // when the async http call is done, execute the callback
      });  
    }
.controller('StudentSaveController', ['$scope', 'StudentService', function ($scope, StudentService) {
  $scope.saveUser = function (user) {
    StudentService.save(user, function (data) {
      $scope.message = data; // I'm assuming data is a string error returned from your REST API
    })
  }
}]);

A forma:

<div class="form-message">{{message}}</div>

<div ng-controller="StudentSaveController">
  <form novalidate class="simple-form">
    Name: <input type="text" ng-model="user.name" /><br />
    E-mail: <input type="email" ng-model="user.email" /><br />
    Gender: <input type="radio" ng-model="user.gender" value="male" />male
    <input type="radio" ng-model="user.gender" value="female" />female<br />
    <input type="button" ng-click="reset()" value="Reset" />
    <input type="submit" ng-click="saveUser(user)" value="Save" />
  </form>
</div>

Isso removeu parte da sua lógica de negócios por brevidade e, na verdade, não testei o código, mas algo como isso funcionaria. O conceito principal é passar um retorno de chamada do controlador para o serviço que será chamado posteriormente no futuro. Se você estiver familiarizado com o NodeJS, este é o mesmo conceito.

2upmedia
fonte
Esta abordagem não é recomendada. Consulte Por que os retornos de chamada de .thenmétodos de promessa são um antipadrão .
georgeawg
0

Enfrentou a mesma situação. Acabei com o seguinte. Portanto, aqui não estou injetando o objeto de escopo na fábrica, mas configurando o $ escopo no próprio controlador usando o conceito de promessa retornado pelo serviço $ http .

(function () {
    getDataFactory = function ($http)
    {
        return {
            callWebApi: function (reqData)
            {
                var dataTemp = {
                    Page: 1, Take: 10,
                    PropName: 'Id', SortOrder: 'Asc'
                };

                return $http({
                    method: 'GET',
                    url: '/api/PatientCategoryApi/PatCat',
                    params: dataTemp, // Parameters to pass to external service
                    headers: { 'Content-Type': 'application/Json' }
                })                
            }
        }
    }
    patientCategoryController = function ($scope, getDataFactory) {
        alert('Hare');
        var promise = getDataFactory.callWebApi('someDataToPass');
        promise.then(
            function successCallback(response) {
                alert(JSON.stringify(response.data));
                // Set this response data to scope to use it in UI
                $scope.gridOptions.data = response.data.Collection;
            }, function errorCallback(response) {
                alert('Some problem while fetching data!!');
            });
    }
    patientCategoryController.$inject = ['$scope', 'getDataFactory'];
    getDataFactory.$inject = ['$http'];
    angular.module('demoApp', []);
    angular.module('demoApp').controller('patientCategoryController', patientCategoryController);
    angular.module('demoApp').factory('getDataFactory', getDataFactory);    
}());
VivekDev
fonte
0

O código para lidar com variáveis ​​de escopo deve ir para o controlador e as chamadas de serviço vão para o serviço.

Você pode injetar $rootScopecom o propósito de usar $rootScope.$broadcaste $rootScope.$on.

Caso contrário, evite injetar $rootScope. Vejo

georgeawg
fonte