web-dev-qa-db-fra.com

Injection de $ scope dans une fonction de service angular ()

J'ai un service:

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

Mais lorsque j'appelle save(), je n'ai pas accès au $scope et j'obtiens ReferenceError: $scope is not defined. Ainsi, l’étape logique (pour moi) consiste à fournir à save () le $scope, et je dois donc également le fournir/l’injecter dans le service. Donc si je le fais comme ça:

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

Je reçois l'erreur suivante:

Erreur: [$ injector: unpr] Fournisseur inconnu: $ scopeProvider <- $ scope <- StudentService

Le lien dans l’erreur (wow, c’est génial!) Me permet de savoir qu’il s’agit d’un injecteur et pourrait avoir à voir avec l’ordre de déclaration des fichiers js. J'ai essayé de les réorganiser dans le index.html, mais je pense que c'est quelque chose de plus simple, comme la façon dont je les injecte.

Utiliser Angular-UI et Angular-UI-Router

105
chris Frisina

Le $scope que vous voyez en train d'être injecté dans les contrôleurs n'est pas un service (comme le reste du contenu injectable), mais un objet Scope. De nombreux objets de portée peuvent être créés (généralement, hériter de manière prototypique d’une portée parent). La racine de toutes les portées est le $rootScope et vous pouvez créer une nouvelle portée enfant à l'aide de la méthode $new() de n'importe quelle portée (y compris le $rootScope).

Le but d'un Scope est de "coller ensemble" la présentation et la logique commerciale de votre application. Cela n'a pas beaucoup de sens de passer un $scope dans un service.

Les services sont des objets singleton utilisés (entre autres) pour partager des données (par exemple entre plusieurs contrôleurs) et encapsulent généralement des morceaux de code réutilisables (car ils peuvent être injectés et offrent leurs "services" dans n’importe quelle partie de votre application qui en a besoin: contrôleurs, directives, filtres, autres services, etc.).

Je suis sûr que différentes approches fonctionneraient pour vous. L'un est ceci:
Étant donné que StudentService est chargé de traiter les données des élèves, vous pouvez demander à StudentService de conserver un éventail d’étudiants et de le laisser "partager" avec ceux qui pourraient être intéressés (par exemple, votre $scope). Cela est d'autant plus logique si d'autres vues/contrôleurs/filtres/services doivent avoir accès à ces informations (s'il n'y en a pas pour le moment, ne soyez pas surpris s'ils apparaissent bientôt).
Chaque fois qu'un nouvel étudiant est ajouté (à l'aide de la méthode save() du service), le groupe d'étudiants du service est mis à jour et tous les autres objets partageant ce tableau sont automatiquement mis à jour.

Sur la base de l'approche décrite ci-dessus, votre code pourrait ressembler à ceci:

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

Une chose à laquelle vous devriez faire attention lorsque vous utilisez cette approche est de ne jamais réaffecter le tableau du service, car tous les autres composants (par exemple, les portées) référenceront toujours le tableau d'origine et votre application se cassera.
Par exemple. pour effacer le tableau dans StudentService:

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

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

Voir aussi ceci brève démonstration.


LITTLE UPDATE:

Quelques mots pour éviter la confusion qui peut survenir lorsqu’on parle d’utiliser un service, mais pas de le créer avec la fonction service().

Citant le docs sur $provide:

Un service Angular est un objet singleton créé par une fabrique de services . Ces fabriques de services sont des fonctions créées à leur tour par un fournisseur de services . Les fournisseurs de services sont des fonctions constructeurs. Lorsqu'elles sont instanciées, elles doivent contenir une propriété appelée $get, qui contient la fonction du service factory .
[...]
... le service $provide dispose de méthodes d'assistance supplémentaires pour enregistrer des services sans spécifier de fournisseur:

  • fournisseur (fournisseur) - enregistre un fournisseur de services auprès de l'injecteur $
  • constant (obj) - enregistre une valeur/un objet accessible aux fournisseurs et aux services.
  • value (obj) - enregistre une valeur/un objet accessible uniquement aux services, pas aux fournisseurs.
  • factory (fn) - enregistre une fonction de fabrique de services, fn, qui sera encapsulée dans un objet fournisseur de services, dont la propriété $ get contiendra la fabrique donnée. une fonction.
  • service (classe) - enregistre une fonction constructeur, classe qui sera encapsulée dans un objet fournisseur de service, dont la propriété $ get instanciera un nouvel objet à l'aide de la fonction constructeur donnée.

Fondamentalement, il est écrit que chaque Angular service est enregistré en utilisant $provide.provider(), mais il existe des méthodes de "raccourci" pour des services plus simples (dont deux sont service() et factory()).
Tout se résume à un service, donc peu importe la méthode que vous utilisez (tant que les exigences de votre service peuvent être couvertes par cette méthode).

BTW, provider vs service vs factory est l’un des concepts les plus déroutants pour les nouveaux arrivants Angular, mais heureusement, il existe de nombreuses ressources (ici, sur SO). pour faciliter les choses. (Il suffit de chercher autour.)

(J'espère que cela s'éclaircit - laissez-moi savoir si ce n'est pas le cas.)

182
gkalpak

Au lieu d'essayer de modifier le $scope au sein du service, vous pouvez implémenter un $watch dans votre contrôleur pour surveiller les modifications apportées à une propriété de votre service, puis mettre à jour une propriété sur le $scope. Voici un exemple que vous pourriez essayer dans un contrôleur:

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

Une chose à noter est que dans votre service, pour que la propriété students soit visible, elle doit être sur l'objet Service ou this comme ceci:

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

Bien (un long) ... si vous insistez pour avoir $scope accéder à l'intérieur d'un service, vous pouvez:

Créer un service getter/setter

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

Injectez-le et stockez-y la portée du contrôleur

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

Maintenant, obtenez la portée dans un autre service

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);
12
Jonatas Walker

Les services sont des singletons et il n'est pas logique d'injecter une étendue dans le service (ce qui est le cas, vous ne pouvez pas injecter une étendue dans le service). Vous pouvez passer le paramètre scope en tant que paramètre, mais cela constitue également un mauvais choix de conception, car le périmètre aurait été modifié à plusieurs endroits, ce qui compliquerait le débogage. Le code permettant de traiter les variables d’étendue doit être placé dans le contrôleur et les appels de service envoyés au service.

8
Ermin Dedovic

Vous pouvez rendre votre service complètement inconscient de l'étendue, mais dans votre contrôleur, autorisez la mise à jour de l'étendue de manière asynchrone.

Le problème que vous rencontrez est dû au fait que vous ignorez que les appels http sont effectués de manière asynchrone, ce qui signifie que vous n'obtenez pas une valeur aussi rapidement que vous le pourriez. Par exemple,

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

Il existe un moyen simple de contourner ce problème et de fournir une fonction de rappel.

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

La forme:

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

Cela a supprimé une partie de votre logique métier par souci de brièveté et je n’ai pas réellement testé le code, mais quelque chose comme cela fonctionnerait. Le concept principal passe un rappel du contrôleur au service qui sera appelé ultérieurement. Si vous connaissez NodeJS, c'est le même concept.

3
2upmedia

Entré dans la même situation. J'ai fini avec le suivant. Donc ici je n'injecte pas l'objet scope dans la fabrique, mais je place le $ scope dans le contrôleur lui-même en utilisant le concept de promesse renvoyé par $ http service.

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

Le code permettant de traiter les variables d’étendue doit être placé dans le contrôleur et les appels de service envoyés au service.

Vous pouvez injecter $rootScope dans le but d'utiliser $rootScope.$broadcast et $rootScope.$on.

Sinon, évitez d'injecter $rootScope. Voir

0
georgeawg