Inyectando $ scope en una función de servicio angular ()

108

Tengo un servicio:

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

Pero cuando llamo save(), no tengo acceso al $scopey obtengo ReferenceError: $scope is not defined. Entonces, el paso lógico (para mí) es proporcionar save () con el $scope, y por lo tanto también debo proporcionarlo / inyectarlo al service. Entonces, si hago eso así:

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

Obtuve el siguiente error:

Error: [$ injector: unpr] Proveedor desconocido: $ scopeProvider <- $ scope <- StudentService

El enlace en el error (¡guau, eso es genial!) Me permite saber que está relacionado con el inyector y que podría tener que ver con el orden de declaración de los archivos js. He intentado reordenarlos en el index.html, pero creo que es algo más simple, como la forma en que los estoy inyectando.

Uso de Angular-UI y Angular-UI-Router

chris frisina
fuente

Respuestas:

183

Lo $scopeque ves que se inyecta en los controladores no es un servicio (como el resto de las cosas inyectables), sino un objeto Scope. Se pueden crear muchos objetos de alcance (generalmente heredando prototípicamente de un alcance principal). La raíz de todos los ámbitos es $rootScopey puede crear un nuevo ámbito secundario utilizando el $new()método de cualquier ámbito (incluido $rootScope).

El propósito de un alcance es "unir" la presentación y la lógica empresarial de su aplicación. No tiene mucho sentido $scopeconvertir un en un servicio.

Los servicios son objetos singleton que se utilizan (entre otras cosas) para compartir datos (por ejemplo, entre varios controladores) y generalmente encapsulan piezas de código reutilizables (ya que pueden inyectarse y ofrecer sus "servicios" en cualquier parte de su aplicación que los necesite: controladores, directivas, filtros, otros servicios, etc.).

Estoy seguro de que varios enfoques funcionarían para usted. Una es la siguiente:
dado que StudentServiceestá a cargo de manejar los datos de los estudiantes, puede StudentServicemantener una matriz de estudiantes y dejar que la "comparta" con quien pueda estar interesado (por ejemplo, su $scope). Esto tiene aún más sentido, si hay otras vistas / controladores / filtros / servicios que necesitan tener acceso a esa información (si no hay ninguna en este momento, no se sorprenda si comienzan a aparecer pronto).
Cada vez que se agrega un nuevo estudiante (usando el save()método del servicio ), la propia matriz de estudiantes del servicio se actualizará y todos los demás objetos que compartan esa matriz también se actualizarán automáticamente.

Según el enfoque descrito anteriormente, su código podría verse así:

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

Una cosa con la que debe tener cuidado al usar este enfoque es nunca reasignar la matriz del servicio, porque entonces cualquier otro componente (por ejemplo, ámbitos) seguirá haciendo referencia a la matriz original y su aplicación se romperá.
Por ejemplo, para borrar la matriz en StudentService:

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

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

Vea también esta breve demostración .


PEQUEÑA ACTUALIZACIÓN:

Unas palabras para evitar la confusión que pueda surgir al hablar de utilizar un servicio, pero no de crearlo con la service()función.

Citando los documentos en$provide :

Un servicio angular es un objeto único creado por una fábrica de servicios . Estas fábricas de servicios son funciones que, a su vez, son creadas por un proveedor de servicios . Los proveedores de servicios son funciones de constructor. Cuando se crean instancias, deben contener una propiedad llamada $get, que contiene la función de fábrica de servicios .
[...]
... el $provideservicio tiene métodos auxiliares adicionales para registrar servicios sin especificar un proveedor:

  • proveedor (proveedor) : registra un proveedor de servicios con $ injector
  • constante (obj) : registra un valor / objeto al que pueden acceder los proveedores y servicios.
  • valor (obj) : registra un valor / objeto al que solo pueden acceder los servicios, no los proveedores.
  • factory (fn) : registra una función de fábrica de servicios, fn, que se incluirá en un objeto de proveedor de servicios, cuya propiedad $ get contendrá la función de fábrica dada.
  • service (class) : registra una función de constructor, clase que se incluirá en un objeto de proveedor de servicios, cuya propiedad $ get instanciará un nuevo objeto utilizando la función de constructor dada.

Básicamente, lo que dice es que todos los servicios de Angular se registran usando $provide.provider(), pero hay métodos de "atajos" para servicios más simples (dos de los cuales son service()y factory()).
Todo "se reduce" a un servicio, por lo que no importa mucho el método que utilice (siempre que los requisitos de su servicio puedan ser cubiertos por ese método).

Por cierto, providervs servicevs factoryes uno de los conceptos más confusos para los recién llegados de Angular, pero afortunadamente hay muchos recursos (aquí en SO) para facilitar las cosas. (Solo busca alrededor).

(Espero que eso lo aclare, avíseme si no es así).

gkalpak
fuente
1
Una pregunta. Dice servicio, pero su ejemplo de código usa la fábrica. Apenas estoy comenzando a comprender la diferencia entre fábricas, servicios y proveedores, solo quiero estar seguro de que ir con una fábrica es la mejor opción, ya que estaba usando un servicio. Aprendí mucho de tu ejemplo. Gracias por el violín y una explicación MUY clara.
chris Frisina
3
@chrisFrisina: actualizó la respuesta con una pequeña explicación. Básicamente, no hace mucha diferencia si usa serviceo factory- terminará con un servicio Angular . Solo asegúrese de comprender cómo funciona cada uno y si se adapta a sus necesidades.
gkalpak
¡Buen post! Me ayuda mucho !
1 de
¡Gracias hermano! aquí hay un buen artículo sobre un asunto similar stsc3000.github.io/blog/2013/10/26/…
Terafor
@ExpertSystem ¿ $scope.studentsVa a estar vacío, si la llamada ajax no finaliza? ¿O se $scope.studentsllenará parcialmente, si este bloque de código está trabajando en progreso? students.push(student);
Yc Zhang
18

En lugar de intentar modificar el $scopedentro del servicio, puede implementar un $watchdentro de su controlador para observar una propiedad en su servicio en busca de cambios y luego actualizar una propiedad en el $scope. Aquí hay un ejemplo que puede probar en un 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;
                }
            });
        }());
    }]);

Una cosa a tener en cuenta es que dentro de su servicio, para que la studentspropiedad sea visible, debe estar en el objeto Servicio o algo thisasí:

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

Bueno (uno largo) ... si insistes en tener $scopeacceso dentro de un servicio, puedes:

Crear un servicio getter / setter

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

Inyectarlo y almacenar el alcance del controlador en él

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

Ahora, obtenga el alcance dentro de otro servicio

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);
Jonatas Walker
fuente
¿Cómo se destruyen los visores?
JK.
9

Los servicios son singleton, y no es lógico que se inyecte un alcance en el servicio (que es el caso, de hecho, no se puede inyectar alcance en el servicio). Puede pasar el alcance como parámetro, pero esa también es una mala elección de diseño, porque tendría que editar el alcance en varios lugares, lo que dificultaría la depuración. El código para tratar las variables de alcance debe ir al controlador y las llamadas de servicio al servicio.

Ermin Dedovic
fuente
Entiendo lo que dices. Sin embargo, en mi caso, tengo muchos controladores y me gustaría configurar sus osciloscopios con un conjunto de $ watches muy similar. ¿Cómo / dónde harías eso? Actualmente, de hecho paso el alcance como parámetro a un servicio que configura los $ relojes.
moritz
@moritz tal vez implemente una directiva secundaria (una que tiene alcance: falso, por lo que usa el alcance definido por otras directivas) y que hace los enlaces de los relojes, así como cualquier otra cosa que necesite. De esa manera, podría usar esa otra directiva en cualquier lugar que necesite para definir dichos relojes. Porque pasar el alcance a un servicio es bastante horrible :) (créanme, estuve allí, hice eso, me golpeé la cabeza contra la pared al final)
tfrascaroli
@TIMINeutron eso suena mucho mejor que pasar por el alcance, ¡lo intentaré la próxima vez que surja el escenario! ¡Gracias!
moritz
Por supuesto. Todavía estoy aprendiendo por mí mismo, y este problema en particular es uno que abordé recientemente de esta manera en particular, y funcionó como un encanto para mí.
tfrascaroli
3

Puede hacer que su servicio desconozca por completo el alcance, pero en su controlador permita que el alcance se actualice de forma asincrónica.

El problema que tiene es porque no sabe que las llamadas http se realizan de forma asincrónica, lo que significa que no obtiene un valor de inmediato como podría. Por ejemplo,

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

Hay una forma sencilla de solucionar este problema y es proporcionar una función de devolución de llamada.

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

Esto eliminó parte de la lógica de su negocio por brevedad y en realidad no he probado el código, pero algo como esto funcionaría. El concepto principal es pasar una devolución de llamada del controlador al servicio que se llama más adelante en el futuro. Si está familiarizado con NodeJS, este es el mismo concepto.

2upmedia
fuente
0

Me metí en la misma situación. Terminé con lo siguiente. Entonces, aquí no estoy inyectando el objeto scope en la fábrica, sino configurando el $ scope en el controlador mismo usando el concepto de promesa devuelto por $ 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);    
}());
VivekDev
fuente