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

108

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 quand j'appelle save(), je n'ai pas accès au $scopeet je reçois ReferenceError: $scope is not defined. Donc, l'étape logique (pour moi), est de fournir save () avec le $scope, et donc je dois aussi le fournir / l'injecter dans le service. Donc si je fais ça comme ça:

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

J'obtiens l'erreur suivante:

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

Le lien dans l'erreur (wow c'est chouette!) Me fait savoir qu'il est lié à l'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.

Utilisation de Angular-UI et Angular-UI-Router

Chris Frisina
la source

Réponses:

183

Le $scopeque vous voyez être injecté dans les contrôleurs n'est pas un service (comme le reste des éléments injectables), mais un objet Scope. De nombreux objets d'étendue peuvent être créés (généralement en héritant de manière prototypique d'une étendue parent). La racine de toutes les étendues est le $rootScopeet vous pouvez créer une nouvelle étendue enfant en utilisant la $new()méthode de n'importe quelle étendue (y compris la $rootScope).

Le but d'un Scope est de «coller» la présentation et la logique métier de votre application. Cela n'a pas beaucoup de sens de $scopetransformer un en service.

Les services sont des objets uniques 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 (puisqu'ils peuvent être injectés et offrir 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 diverses approches fonctionneraient pour vous. L'une d'entre elles est la suivante: étant
donné que le StudentServiceest chargé de traiter les données des étudiants, vous pouvez avoir le StudentServicegarder un tableau d'étudiants et le laisser "partager" avec qui pourrait être intéressé (par exemple votre $scope). Cela a encore plus de sens, s'il y a d'autres vues / contrôleurs / filtres / services qui doivent avoir accès à ces informations (s'il n'y en a pas pour le moment, ne soyez pas surpris si elles commencent à apparaître bientôt).
Chaque fois qu'un nouvel étudiant est ajouté (en utilisant la save()méthode du service ), le propre tableau d'étudiants du service sera mis à jour et tous les autres objets partageant ce tableau seront également mis à jour automatiquement.

Basé sur 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 devez faire attention lorsque vous utilisez cette approche est de ne jamais réaffecter le tableau du service, car alors tous les autres composants (par exemple les étendues) 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 cette courte démo .


PETITE MISE À JOUR:

Quelques mots pour éviter la confusion qui peut survenir en parlant d'utiliser un service, mais pas de le créer avec la service()fonction

Citant les documents sur$provide :

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

  • provider (provider) - enregistre un fournisseur de services avec l'injecteur $
  • constant (obj) - enregistre une valeur / un objet auquel les fournisseurs et les services peuvent accéder.
  • value (obj) - enregistre une valeur / un objet qui n'est accessible que par les services, pas par les fournisseurs.
  • factory (fn) - enregistre une fonction de fabrique de service, fn, qui sera encapsulée dans un objet fournisseur de services, dont la propriété $ get contiendra la fonction de fabrique donnée.
  • service (class) - enregistre une fonction constructeur, classe qui sera encapsulée dans un objet fournisseur de services, dont la propriété $ get instanciera un nouvel objet en utilisant la fonction constructeur donnée.

Fondamentalement, ce qu'il dit, c'est que chaque service Angular 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, providervs servicevs factoryest l'un des concepts les plus déroutants pour les nouveaux arrivants angulaires, mais heureusement, il existe de nombreuses ressources (ici sur SO) pour faciliter les choses. (Il suffit de chercher.)

(J'espère que cela clarifie les choses - faites-moi savoir si ce n'est pas le cas.)

gkalpak
la source
1
Une question. Vous dites service, mais votre exemple de code utilise l'usine. Je commence tout juste à comprendre la différence entre les usines, les services et les fournisseurs, je veux juste être sûr que choisir une usine est la meilleure option, car j'utilisais un service. J'ai beaucoup appris de votre exemple. Merci pour le violon et l'explication TRÈS claire.
chris Frisina
3
@chrisFrisina: Mise à jour de la réponse avec une petite explication. Fondamentalement, cela ne fait pas beaucoup de différence si vous utilisez serviceou factory- vous finirez avec un service angulaire . Assurez-vous simplement de comprendre comment chacun fonctionne et si cela répond à vos besoins.
gkalpak
Belle publication! Cela m'aide beaucoup !
Oni1
Merci mec! voici un bel article sur un sujet similaire stsc3000.github.io/blog/2013/10/26/…
Terafor
@ExpertSystem Va-t- $scope.studentsil être vide si l'appel ajax n'est pas terminé? Ou $scope.studentsva-t-il être partiellement rempli, si ce bloc de code fonctionne en cours? students.push(student);
Yc Zhang
18

Au lieu d'essayer de modifier le $scopedans le service, vous pouvez implémenter un $watchdans votre contrôleur pour surveiller une propriété de votre service pour les modifications, 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 qu'au sein de votre service, pour que la studentspropriété soit visible, elle doit être sur l'objet Service ou thissimilaire:

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

Eh bien (une longue) ... si vous insistez pour avoir $scopeaccès à 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');
}]);
Jonatas Walker
la source
Comment les scopes sont-ils détruits?
JK.
9

Les services sont des singletons, et il n'est pas logique qu'une étendue soit injectée en service (ce qui est bien le cas, vous ne pouvez pas injecter d'étendue en service). Vous pouvez passer la portée en tant que paramètre, mais c'est aussi un mauvais choix de conception, car vous auriez une portée en cours de modification à plusieurs endroits, ce qui rendrait le débogage difficile. Le code pour traiter les variables d'étendue doit aller dans le contrôleur et les appels de service vont au service.

Ermin Dedovic
la source
Je comprends ce que tu dis. Cependant, dans mon cas, j'ai de nombreux contrôleurs et j'aimerais configurer leurs oscilloscopes avec un ensemble très similaire de $ montres. Comment / où feriez-vous cela? Actuellement, je passe en effet la portée en paramètre à un service qui définit les $ montres.
moritz
@moritz peut implémenter une directive secondaire (une qui a une portée: false, donc elle utilise la portée définie par d'autres directives) et celle-ci fait les liaisons de la watchess, ainsi que tout ce dont vous avez besoin. De cette façon, vous pouvez utiliser cette autre directive partout où vous devez définir de telles montres. Parce que passer la portée à un service est en effet assez horrible :) (croyez-moi, j'ai été là, j'ai fait ça, je me suis cogné la tête contre le mur à la fin)
tfrascaroli
@TIMINeutron qui sonne beaucoup mieux que de passer autour de la lunette, je vais essayer ça la prochaine fois que le scénario se présente! Merci!
moritz
Sûr. J'apprends encore moi-même, et ce problème particulier est un problème que j'ai récemment abordé de cette manière particulière, et cela a fonctionné comme un charme pour moi.
tfrascaroli
3

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 que vous ne savez pas que les appels http sont effectués de manière asynchrone, ce qui signifie que vous n'obtenez pas une valeur immédiatement comme 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 cela 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 ça fonctionnerait. Le concept principal est de transmettre un rappel du contrôleur au service qui sera appelé plus tard dans le futur. Si vous connaissez NodeJS, c'est le même concept.

2upmédia
la source
Cette approche n'est pas recommandée. Voir Pourquoi les rappels des .thenméthodes de promesse sont-ils un anti-modèle .
georgeawg
0

Je suis dans la même situation. J'ai fini avec ce qui suit. Donc ici je n'injecte pas l'objet scope dans la fabrique, mais je règle le $ scope dans le contrôleur lui-même en utilisant le concept de promesse retourné par le service $ 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
la source