web-dev-qa-db-fra.com

AngularJS: Comment surveiller les variables de service?

J'ai un service, disons:

factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
  var service = {
    foo: []
  };

  return service;
}]);

Et j'aimerais utiliser foo pour contrôler une liste rendue en HTML:

<div ng-controller="FooCtrl">
  <div ng-repeat="item in foo">{{ item }}</div>
</div>

Pour que l’automate détecte quand aService.foo est mis à jour, j’ai bricolé ce motif, j’ajoute un service à l’automate $scope, puis j’utilise $scope.$watch():

function FooCtrl($scope, aService) {                                                                                                                              
  $scope.aService = aService;
  $scope.foo = aService.foo;

  $scope.$watch('aService.foo', function (newVal, oldVal, scope) {
    if(newVal) { 
      scope.foo = newVal;
    }
  });
}

Cela semble long et je le répète dans tous les contrôleurs qui utilisent les variables du service. Existe-t-il une meilleure façon de regarder les variables partagées?

404
berto

Vous pouvez toujours utiliser le bon vieux modèle d'observateur si vous voulez éviter la tyrannie et les frais généraux de $watch.

Dans le service:

factory('aService', function() {
  var observerCallbacks = [];

  //register an observer
  this.registerObserverCallback = function(callback){
    observerCallbacks.Push(callback);
  };

  //call this when you know 'foo' has been changed
  var notifyObservers = function(){
    angular.forEach(observerCallbacks, function(callback){
      callback();
    });
  };

  //example of when you may want to notify observers
  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

Et dans le contrôleur:

function FooCtrl($scope, aService){
  var updateFoo = function(){
    $scope.foo = aService.foo;
  };

  aService.registerObserverCallback(updateFoo);
  //service now in control of updating foo
};
275
dtheodor

Dans un scénario comme celui-ci, où plusieurs objets inconnus pourraient être intéressés par les modifications, utilisez $rootScope.$broadcast à partir de l'élément en cours de modification.

Plutôt que de créer votre propre registre d'écouteurs (qui doivent être nettoyés à l'aide de plusieurs $ destroys), vous devriez pouvoir utiliser $broadcast à partir du service en question.

Vous devez toujours coder les gestionnaires $on dans chaque écouteur, mais le modèle est découplé de plusieurs appels à $digest et évite ainsi le risque d'observateurs de longue durée.

De cette façon, les auditeurs peuvent également aller et venir à partir de DOM et/ou de différents périmètres enfants sans que le service ne modifie son comportement.

** mise à jour: exemples **

Les émissions "globales" auraient le plus de sens et pourraient avoir un impact sur d'innombrables autres éléments de votre application. Un bon exemple est un service d’utilisateur où plusieurs événements peuvent se produire, tels que la connexion, la déconnexion, la mise à jour, l’inactivité, etc. Je pense que c’est là que les émissions ont le plus de sens car toute portée peut écouter un événement sans même en injectant le service, et il n'est pas nécessaire d'évaluer les expressions ou les résultats du cache pour rechercher les modifications. Il suffit de tirer et d'oublier (alors assurez-vous que c'est une notification feu et oublie, et non quelque chose qui nécessite une action)

.factory('UserService', [ '$rootScope', function($rootScope) {
   var service = <whatever you do for the object>

   service.save = function(data) {
     .. validate data and update model ..
     // notify listeners and provide the data that changed [optional]
     $rootScope.$broadcast('user:updated',data);
   }

   // alternatively, create a callback function and $broadcast from there if making an ajax call

   return service;
}]);

Le service ci-dessus diffuserait un message à chaque portée lorsque la fonction save () serait terminée et que les données étaient valides. Sinon, s'il s'agit d'une soumission $ resource ou ajax, déplacez l'appel de diffusion dans le rappel afin qu'il se déclenche lorsque le serveur a répondu. Les émissions conviennent particulièrement à ce modèle, car chaque auditeur n’attend que l’événement sans avoir à inspecter la portée de chaque $ digest. L'auditeur ressemblerait à ceci:

.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {

  var user = UserService.getUser();

  // if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
   $scope.name = user.firstname + ' ' +user.lastname;

   $scope.$on('user:updated', function(event,data) {
     // you could inspect the data to see if what you care about changed, or just update your own scope
     $scope.name = user.firstname + ' ' + user.lastname;
   });

   // different event names let you group your code and logic by what happened
   $scope.$on('user:logout', function(event,data) {
     .. do something differently entirely ..
   });

 }]);

L'un des avantages de ceci est l'élimination de plusieurs montres. Si vous combinez des champs ou dérivez des valeurs comme dans l'exemple ci-dessus, vous devez surveiller les propriétés firstname et lastname. Regarder la fonction getUser () ne fonctionnerait que si l'objet utilisateur était remplacé lors des mises à jour, il ne se déclencherait pas si les propriétés de l'objet utilisateur étaient simplement mises à jour. Dans ce cas, vous devrez faire une surveillance approfondie, ce qui est plus intensif.

$ broadcast envoie le message de la portée à laquelle elle a été appelée dans toutes les portées enfants. Donc, l'appeler depuis $ rootScope se déclenchera sur toutes les portées. Si vous diffusiez $ à partir de l'étendue de votre contrôleur, par exemple, elle ne se déclencherait que dans les étendues héritées de l'étendue de votre contrôleur. $ emit va dans le sens opposé et se comporte de la même manière qu'un événement DOM en ce sens qu'il bouillonne dans la chaîne d'étendue.

Gardez à l'esprit qu'il existe des scénarios dans lesquels $ broadcast a beaucoup de sens et qu'il existe des scénarios dans lesquels $ watch est une meilleure option - en particulier si elle se trouve dans une étendue isolée avec une expression de veille très spécifique.

224
Matt Pileggi

J'utilise l'approche similaire à @dtheodot mais j'utilise angular promise au lieu de passer des rappels

app.service('myService', function($q) {
    var self = this,
        defer = $q.defer();

    this.foo = 0;

    this.observeFoo = function() {
        return defer.promise;
    }

    this.setFoo = function(foo) {
        self.foo = foo;
        defer.notify(self.foo);
    }
})

Ensuite, n’importe où utilisez simplement la méthode myService.setFoo(foo) pour mettre à jour foo sur le service. Dans votre contrôleur, vous pouvez l'utiliser comme:

myService.observeFoo().then(null, null, function(foo){
    $scope.foo = foo;
})

Les deux premiers arguments de then sont les rappels de succès et d'erreur, le troisième est le rappel de notification.

référence pour $ q.

44
Krym

Sans surveillance ni rappel de l'observateur ( http://jsfiddle.net/zymotik/853wvv7s/ ):

JavaScript:

angular.module("Demo", [])
    .factory("DemoService", function($timeout) {

        function DemoService() {
            var self = this;
            self.name = "Demo Service";

            self.count = 0;

            self.counter = function(){
                self.count++;
                $timeout(self.counter, 1000);
            }

            self.addOneHundred = function(){
                self.count+=100;
            }

            self.counter();
        }

        return new DemoService();

    })
    .controller("DemoController", function($scope, DemoService) {

        $scope.service = DemoService;

        $scope.minusOneHundred = function() {
            DemoService.count -= 100;
        }

    });

HTML

<div ng-app="Demo" ng-controller="DemoController">
    <div>
        <h4>{{service.name}}</h4>
        <p>Count: {{service.count}}</p>
    </div>
</div>

Ce JavaScript fonctionne lorsque nous renvoyons un objet du service plutôt qu'une valeur. Lorsqu'un objet JavaScript est renvoyé d'un service, Angular ajoute des surveillances à toutes ses propriétés.

Notez également que j'utilise 'var self = this' car je dois conserver une référence à l'objet d'origine lors de l'exécution de $ timeout, sinon 'this' fera référence à l'objet window.

39
Zymotik

Je suis tombé par hasard sur cette question à la recherche de quelque chose de similaire, mais je pense qu'elle mérite une explication détaillée de ce qui se passe, ainsi que des solutions supplémentaires.

Lorsqu'une expression angular telle que celle que vous avez utilisée est présente dans le code HTML, Angular configure automatiquement un _$watch_ pour _$scope.foo_ et met à jour le code HTML. chaque fois que _$scope.foo_ change.

_<div ng-controller="FooCtrl">
  <div ng-repeat="item in foo">{{ item }}</div>
</div>
_

La question non résolue ici est que l’un des deux problèmes qui affectent _aService.foo_ est tel que les modifications ne sont pas détectées. Ces deux possibilités sont:

  1. _aService.foo_ est défini à chaque fois sur un nouveau tableau, ce qui rend la référence obsolète.
  2. _aService.foo_ est mis à jour de manière à ce qu'un cycle _$digest_ ne soit pas déclenché lors de la mise à jour.

Problème 1: références obsolètes

En considérant la première possibilité, en supposant que _$digest_ est appliqué, si _aService.foo_ était toujours le même tableau, le __$watch_ automatiquement défini détecterait les modifications, comme indiqué dans l'extrait de code ci-dessous.

Solution 1-a: Assurez-vous que le tableau ou l'objet est le même objet à chaque mise à jour.

_angular.module('myApp', [])
  .factory('aService', [
    '$interval',
    function($interval) {
      var service = {
        foo: []
      };

      // Create a new array on each update, appending the previous items and 
      // adding one new item each time
      $interval(function() {
        if (service.foo.length < 10) {
          var newArray = []
          Array.prototype.Push.apply(newArray, service.foo);
          newArray.Push(Math.random());
          service.foo = newArray;
        }
      }, 1000);

      return service;
    }
  ])
  .factory('aService2', [
    '$interval',
    function($interval) {
      var service = {
        foo: []
      };

      // Keep the same array, just add new items on each update
      $interval(function() {
        if (service.foo.length < 10) {
          service.foo.Push(Math.random());
        }
      }, 1000);

      return service;
    }
  ])
  .controller('FooCtrl', [
    '$scope',
    'aService',
    'aService2',
    function FooCtrl($scope, aService, aService2) {
      $scope.foo = aService.foo;
      $scope.foo2 = aService2.foo;
    }
  ]);_
_<!DOCTYPE html>
<html>

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp">
  <div ng-controller="FooCtrl">
    <h1>Array changes on each update</h1>
    <div ng-repeat="item in foo">{{ item }}</div>
    <h1>Array is the same on each udpate</h1>
    <div ng-repeat="item in foo2">{{ item }}</div>
  </div>
</body>

</html>_

Comme vous pouvez le constater, la répétition ng supposément attachée à _aService.foo_ ne se met pas à jour lorsque _aService.foo_ change, mais la répétition ng attachée à _aService2.foo_ . En effet, notre référence à _aService.foo_ est obsolète, mais notre référence à _aService2.foo_ ne l’est pas. Nous avons créé une référence au tableau initial avec _$scope.foo = aService.foo;_, qui a ensuite été ignorée par le service lors de sa prochaine mise à jour, ce qui signifie que _$scope.foo_ ne faisait plus référence au tableau que nous voulions.

Cependant, bien qu'il existe plusieurs façons de s'assurer que la référence initiale est maintenue intacte, il peut parfois être nécessaire de modifier l'objet ou le tableau. Ou si la propriété de service fait référence à une primitive telle que String ou Number? Dans ces cas, nous ne pouvons pas simplement compter sur une référence. Alors, que pouvons-nous ?

Plusieurs des réponses données précédemment apportent déjà des solutions à ce problème. Cependant, je suis personnellement favorable à l'utilisation de la méthode simple suggérée par Jin et thetallweeks dans les commentaires:

référencez simplement aService.foo dans le balisage HTML

Solution 1-b: attachez le service à l'étendue et référence _{service}.{property}_ au format HTML.

Signification, faites ceci:

HTML:

_<div ng-controller="FooCtrl">
  <div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
_

JS:

_function FooCtrl($scope, aService) {
    $scope.aService = aService;
}
_
_angular.module('myApp', [])
  .factory('aService', [
    '$interval',
    function($interval) {
      var service = {
        foo: []
      };

      // Create a new array on each update, appending the previous items and 
      // adding one new item each time
      $interval(function() {
        if (service.foo.length < 10) {
          var newArray = []
          Array.prototype.Push.apply(newArray, service.foo);
          newArray.Push(Math.random());
          service.foo = newArray;
        }
      }, 1000);

      return service;
    }
  ])
  .controller('FooCtrl', [
    '$scope',
    'aService',
    function FooCtrl($scope, aService) {
      $scope.aService = aService;
    }
  ]);_
_<!DOCTYPE html>
<html>

<head>
  <script data-require="[email protected]" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp">
  <div ng-controller="FooCtrl">
    <h1>Array changes on each update</h1>
    <div ng-repeat="item in aService.foo">{{ item }}</div>
  </div>
</body>

</html>_

De cette façon, le _$watch_ résoudra _aService.foo_ sur chaque _$digest_, ce qui donnera la valeur correctement mise à jour.

C’est un peu ce que vous essayiez de faire avec votre solution de contournement, mais de façon beaucoup moins harmonieuse. Vous avez ajouté un _$watch_ inutile __ au contrôleur qui place explicitement foo sur le _$scope_ chaque fois que cela change. Vous n'avez pas besoin de ce _$watch_ supplémentaire lorsque vous attachez aService au lieu de _aService.foo_ au _$scope_, et vous liez explicitement à _aService.foo_ dans le balisage.


C’est bien beau en supposant qu’un cycle _$digest_ soit appliqué. Dans les exemples ci-dessus, j'ai utilisé le service $interval d'Angular pour mettre à jour les tableaux, qui déclenche automatiquement une boucle _$digest_ après chaque mise à jour. Mais que se passe-t-il si les variables de service (pour une raison quelconque) ne sont pas mises à jour dans le "monde angulaire"? En d'autres termes, nous n'avons pas un cycle _$digest_ activé automatiquement chaque fois que la propriété de service change?


Problème 2: manquant $digest

Beaucoup des solutions ici vont résoudre ce problème, mais je suis d'accord avec Code Whisperer :

La raison pour laquelle nous utilisons un cadre tel que Angular est de ne pas créer nos propres modèles d'observateur

Par conséquent, je préférerais continuer à utiliser la référence _aService.foo_ dans le balisage HTML, comme indiqué dans le deuxième exemple ci-dessus, sans avoir à enregistrer un rappel supplémentaire dans le contrôleur.

Solution 2: utilisez un passeur et un getter avec $rootScope.$apply()

J'ai été surpris que personne n'ait encore suggéré l'utilisation de setter et getter . Cette fonctionnalité a été introduite dans ECMAScript5 et existe donc depuis des années. Bien sûr, cela signifie que si, pour une raison quelconque, vous devez prendre en charge de très vieux navigateurs, cette méthode ne fonctionnera pas, mais j’ai l’impression que les getters et les setters sont largement sous-utilisés en JavaScript. Dans ce cas particulier, ils pourraient être très utiles:

_factory('aService', [
  '$rootScope',
  function($rootScope) {
    var realFoo = [];

    var service = {
      set foo(a) {
        realFoo = a;
        $rootScope.$apply();
      },
      get foo() {
        return realFoo;
      }
    };
  // ...
}
_
_angular.module('myApp', [])
  .factory('aService', [
    '$rootScope',
    function($rootScope) {
      var realFoo = [];

      var service = {
        set foo(a) {
          realFoo = a;
          $rootScope.$apply();
        },
        get foo() {
          return realFoo;
        }
      };

      // Create a new array on each update, appending the previous items and 
      // adding one new item each time
      setInterval(function() {
        if (service.foo.length < 10) {
          var newArray = [];
          Array.prototype.Push.apply(newArray, service.foo);
          newArray.Push(Math.random());
          service.foo = newArray;
        }
      }, 1000);

      return service;
    }
  ])
  .controller('FooCtrl', [
    '$scope',
    'aService',
    function FooCtrl($scope, aService) {
      $scope.aService = aService;
    }
  ]);_
_<!DOCTYPE html>
<html>

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp">
  <div ng-controller="FooCtrl">
    <h1>Using a Getter/Setter</h1>
    <div ng-repeat="item in aService.foo">{{ item }}</div>
  </div>
</body>

</html>_

Ici, j'ai ajouté une variable 'privée' dans la fonction de service: realFoo. Cet objet est mis à jour et récupéré à l'aide des fonctions get foo() et set foo() respectivement sur l'objet service.

Notez l'utilisation de $rootScope.$apply() dans la fonction set. Cela garantit que Angular sera informé de tout changement apporté à _service.foo_. Si vous obtenez des erreurs 'inprog', voir cette page de référence utile , ou si vous utilisez Angular> = 1.3, vous pouvez simplement utiliser $rootScope.$applyAsync().

Méfiez-vous également de cela si _aService.foo_ est mis à jour très fréquemment car cela pourrait avoir une incidence importante sur les performances. Si les performances posent problème, vous pouvez configurer un modèle d'observateur similaire aux autres réponses fournies ici à l'aide du paramètre de définition.

28
NanoWizard

Autant que je sache, vous n'avez pas à faire quelque chose d'aussi élaboré. Vous avez déjà affecté foo du service à votre étendue et foo étant un tableau (et à son tour un objet, il est assigné par référence!). Donc, tout ce que vous devez faire est quelque chose comme ceci:

function FooCtrl($scope, aService) {                                                                                                                              
  $scope.foo = aService.foo;

 }

Si certaines, d'autres variables dans cette même Ctrl dépendent de foo, alors oui, vous aurez besoin d'une surveillance pour observer foo et apporter des modifications à cette variable. Mais tant que c'est une simple référence, l'observation est inutile. J'espère que cela t'aides.

28
ganaraj

Vous pouvez insérer le service dans $ rootScope et regarder:

myApp.run(function($rootScope, aService){
    $rootScope.aService = aService;
    $rootScope.$watch('aService', function(){
        alert('Watch');
    }, true);
});

Dans votre contrôleur:

myApp.controller('main', function($scope){
    $scope.aService.foo = 'change';
});

Une autre option consiste à utiliser une bibliothèque externe telle que: https://github.com/melanke/Watch.JS

Fonctionne avec: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Vous pouvez observer les modifications d'un, de plusieurs ou de tous les attributs d'objet.

Exemple:

var ex3 = {
    attr1: 0,
    attr2: "initial value of attr2",
    attr3: ["a", 3, null]
};   
watch(ex3, function(){
    alert("some attribute of ex3 changes!");
});
ex3.attr3.Push("new value");​

Vous pouvez regarder les changements au sein même de l’usine, puis diffuser un changement.

angular.module('MyApp').factory('aFactory', function ($rootScope) {
    // Define your factory content
    var result = {
        'key': value
    };

    // add a listener on a key        
    $rootScope.$watch(function () {
        return result.key;
    }, function (newValue, oldValue, scope) {
        // This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
        $rootScope.$broadcast('aFactory:keyChanged', newValue);
    }, true);

    return result;
});

Puis dans votre contrôleur:

angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {

    $rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
        // do something
    });
}]);

De cette manière, vous mettez tout le code d’usine associé dans sa description, vous ne pouvez compter que sur la diffusion de l’extérieur.

6
Flavien Volken

== UPDATED ==

Très simple maintenant dans $ watch.

Stylo ici .

HTML:

<div class="container" data-ng-app="app">

  <div class="well" data-ng-controller="FooCtrl">
    <p><strong>FooController</strong></p>
    <div class="row">
      <div class="col-sm-6">
        <p><a href="" ng-click="setItems([ { name: 'I am single item' } ])">Send one item</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 2' }, { name: 'Item 2 of 2' } ])">Send two items</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 3' }, { name: 'Item 2 of 3' }, { name: 'Item 3 of 3' } ])">Send three items</a></p>
      </div>
      <div class="col-sm-6">
        <p><a href="" ng-click="setName('Sheldon')">Send name: Sheldon</a></p>
        <p><a href="" ng-click="setName('Leonard')">Send name: Leonard</a></p>
        <p><a href="" ng-click="setName('Penny')">Send name: Penny</a></p>
      </div>
    </div>
  </div>

  <div class="well" data-ng-controller="BarCtrl">
    <p><strong>BarController</strong></p>
    <p ng-if="name">Name is: {{ name }}</p>
    <div ng-repeat="item in items">{{ item.name }}</div>
  </div>

</div>

JavaScript:

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

app.factory('PostmanService', function() {
  var Postman = {};
  Postman.set = function(key, val) {
    Postman[key] = val;
  };
  Postman.get = function(key) {
    return Postman[key];
  };
  Postman.watch = function($scope, key, onChange) {
    return $scope.$watch(
      // This function returns the value being watched. It is called for each turn of the $digest loop
      function() {
        return Postman.get(key);
      },
      // This is the change listener, called when the value returned from the above function changes
      function(newValue, oldValue) {
        if (newValue !== oldValue) {
          // Only update if the value changed
          $scope[key] = newValue;
          // Run onChange if it is function
          if (angular.isFunction(onChange)) {
            onChange(newValue, oldValue);
          }
        }
      }
    );
  };
  return Postman;
});

app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.setItems = function(items) {
    PostmanService.set('items', items);
  };
  $scope.setName = function(name) {
    PostmanService.set('name', name);
  };
}]);

app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.items = [];
  $scope.name = '';
  PostmanService.watch($scope, 'items');
  PostmanService.watch($scope, 'name', function(newVal, oldVal) {
    alert('Hi, ' + newVal + '!');
  });
}]);
6
hayatbiralem

En vous appuyant sur dtheodor, vous pouvez utiliser une méthode similaire à celle décrite ci-dessous pour ne pas oublier de désenregistrer le rappel ... Certains peuvent toutefois s'opposer à ce que le $scope soit transmis à un service.

factory('aService', function() {
  var observerCallbacks = [];

  /**
   * Registers a function that will be called when
   * any modifications are made.
   *
   * For convenience the callback is called immediately after registering
   * which can be prevented with `preventImmediate` param.
   *
   * Will also automatically unregister the callback upon scope destory.
   */
  this.registerObserver = function($scope, cb, preventImmediate){
    observerCallbacks.Push(cb);

    if (preventImmediate !== true) {
      cb();
    }

    $scope.$on('$destroy', function () {
      observerCallbacks.remove(cb);
    });
  };

  function notifyObservers() {
    observerCallbacks.forEach(function (cb) {
      cb();
    });
  };

  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

Array.remove est une méthode d'extension qui ressemble à ceci:

/**
 * Removes the given item the current array.
 *
 * @param  {Object}  item   The item to remove.
 * @return {Boolean}        True if the item is removed.
 */
Array.prototype.remove = function (item /*, thisp */) {
    var idx = this.indexOf(item);

    if (idx > -1) {
        this.splice(idx, 1);

        return true;
    }
    return false;
};
4
Jamie

alors que je faisais face à un problème très similaire, j'ai regardé une fonction dans la portée et la fonction a renvoyé la variable de service. J'ai créé un js fiddle . vous pouvez trouver le code ci-dessous.

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

myApp.factory("randomService", function($timeout){
    var retValue = {};
    var data = 0;

    retValue.startService = function(){
        updateData();
    }

    retValue.getData = function(){
        return data;
    }

    function updateData(){
        $timeout(function(){
            data = Math.floor(Math.random() * 100);
            updateData()
        }, 500);
    }

    return retValue;
});

myApp.controller("myController", function($scope, randomService){
    $scope.data = 0;
    $scope.dataUpdated = 0;
    $scope.watchCalled = 0;
    randomService.startService();

    $scope.getRandomData = function(){
        return randomService.getData();    
    }

    $scope.$watch("getRandomData()", function(newValue, oldValue){
        if(oldValue != newValue){
            $scope.data = newValue;
            $scope.dataUpdated++;
        }
            $scope.watchCalled++;
    });
});
2
chandings

Voici mon approche générique.

mainApp.service('aService',[function(){
        var self = this;
        var callbacks = {};

        this.foo = '';

        this.watch = function(variable, callback) {
            if (typeof(self[variable]) !== 'undefined') {
                if (!callbacks[variable]) {
                    callbacks[variable] = [];
                }
                callbacks[variable].Push(callback);
            }
        }

        this.notifyWatchersOn = function(variable) {
            if (!self[variable]) return;
            if (!callbacks[variable]) return;

            angular.forEach(callbacks[variable], function(callback, key){
                callback(self[variable]);
            });
        }

        this.changeFoo = function(newValue) {
            self.foo = newValue;
            self.notifyWatchersOn('foo');
        }

    }]);

Dans votre contrôleur

function FooCtrl($scope, aService) {
    $scope.foo;

    $scope._initWatchers = function() {
        aService.watch('foo', $scope._onFooChange);
    }

    $scope._onFooChange = function(newValue) {
        $scope.foo = newValue;
    }

    $scope._initWatchers();

}

FooCtrl.$inject = ['$scope', 'aService'];
2
elitechance

J'ai trouvé une très bonne solution sur l'autre fil avec un problème similaire mais une approche totalement différente. Source: AngularJS: La surveillance $ de la directive ne fonctionne pas lorsque la valeur de $ rootScope est modifiée

En gros , la solution indique NOT TO utilisez $watch comme c'est une solution très lourde. Au lieu de cela , ils proposent d'utiliser $emit et $on.

Mon problème était de regarder une variable dans mon service et réagir dans directive . Et avec la méthode ci-dessus, c'est très facile!

Mon exemple de module/service:

angular.module('xxx').factory('example', function ($rootScope) {
    var user;

    return {
        setUser: function (aUser) {
            user = aUser;
            $rootScope.$emit('user:change');
        },
        getUser: function () {
            return (user) ? user : false;
        },
        ...
    };
});

Donc, fondamentalement, je surveille ma user - chaque fois qu’elle est définie sur la nouvelle valeur I $emit un user:change statut.

Maintenant dans mon cas, dans la directive , j'ai utilisé:

angular.module('xxx').directive('directive', function (Auth, $rootScope) {
    return {
        ...
        link: function (scope, element, attrs) {
            ...
            $rootScope.$on('user:change', update);
        }
    };
});

Maintenant, dans la directive , j'écoute sur le $rootScope et sur le changement donné - I réagissent respectivement. Très facile et élégant!

2
Atais

Pour ceux qui, comme moi, cherchent simplement une solution simple, cela correspond presque exactement à ce que vous attendez de l’utilisation normale de $ watch dans les contrôleurs. La seule différence est qu'il évalue la chaîne dans son contexte javascript et non sur une étendue spécifique. Vous devrez injecter $ rootScope dans votre service, bien qu'il ne soit utilisé que pour se connecter correctement aux cycles de résumé.

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};
2
Justus Wingert

Je suis venu à cette question mais il s'est avéré que mon problème était que j'utilisais setInterval alors que j'aurais dû utiliser le fournisseur angular $ interval. C'est également le cas pour setTimeout (utilisez plutôt $ timeout). Je sais que ce n'est pas la réponse à la question du PO, mais cela pourrait aider certains, comme cela m'a été utile.

2
honkskillet

Un petit peu moche, mais j'ai ajouté l'enregistrement des variables de portée à mon service pour une bascule:

myApp.service('myService', function() {
    var self = this;
    self.value = false;
    self.c2 = function(){};
    self.callback = function(){
        self.value = !self.value; 
       self.c2();
    };

    self.on = function(){
        return self.value;
    };

    self.register = function(obj, key){ 
        self.c2 = function(){
            obj[key] = self.value; 
            obj.$apply();
        } 
    };

    return this;
});

Et puis dans le contrôleur:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}
1
nclu

Jetez un oeil à ce plunker :: c'est l'exemple le plus simple que je puisse penser

http://jsfiddle.net/HEdJF/

<div ng-app="myApp">
    <div ng-controller="FirstCtrl">
        <input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
        <br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
    </div>
    <hr>
    <div ng-controller="SecondCtrl">
        Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
    </div>
</div>



// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
   return { FirstName: '' };
});

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

myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.Data = Data;
});
1
anandharshan

// service: (rien de spécial ici)

myApp.service('myService', function() {
  return { someVariable:'abc123' };
});

// ctrl:

myApp.controller('MyCtrl', function($scope, myService) {

  $scope.someVariable = myService.someVariable;

  // watch the service and update this ctrl...
  $scope.$watch(function(){
    return myService.someVariable;
  }, function(newValue){
    $scope.someVariable = newValue;
  });
});
1
Nawlbergs

Je suis en retard mais j'ai trouvé un moyen plus agréable de le faire que la réponse ci-dessus. Au lieu d'affecter une variable pour contenir la valeur de la variable de service, j'ai créé une fonction attachée à l'étendue, qui renvoie la variable de service.

contrôleur

$scope.foo = function(){
 return aService.foo;
}

Je pense que cela fera ce que vous voulez. Mon contrôleur continue de vérifier la valeur de mon service avec cette implémentation. Honnêtement, c'est beaucoup plus simple que la réponse choisie.

0
alaboudi

J'ai écrit deux services publics simples qui m'aident à suivre les modifications apportées aux propriétés du service.

Si vous voulez ignorer la longue explication, vous pouvez aller dans le détroit jsfiddle

  1. WatchObj
mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // returns watch function
  // obj: the object to watch for
  // fields: the array of fields to watch
  // target: where to assign changes (usually it's $scope or controller instance)
  // $scope: optional, if not provided $rootScope is use
  return function watch_obj(obj, fields, target, $scope) {
    $scope = $scope || $rootScope;
    //initialize watches and create an array of "unwatch functions"
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    //unregister function will unregister all our watches
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    //automatically unregister when scope is destroyed
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}

Ce service est utilisé dans le contrôleur de la manière suivante: Supposons que vous ayez un service "testService" avec les propriétés 'prop1', 'prop2', 'prop3'. Vous voulez regarder et attribuer à la portée "prop1" et "prop2". Avec le service de surveillance, cela ressemblera à ça:

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
}
  1. appliquer Watch obj est génial, mais cela ne suffit pas si vous avez du code asynchrone dans votre service. Pour ce cas, j'utilise un deuxième utilitaire qui ressemble à cela:
mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

Je le déclencherais à la fin de mon code asynchrone pour déclencher la boucle $ digest. Comme ça:

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply(); //trigger $digest loop
  }.bind(this));
}

Donc, tout cela ensemble ressemblera à ça (vous pouvez le lancer ou violon ouvert ):

// TEST app code

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

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
  $scope.test1 = function() {
    testService.test1();
  };
  $scope.test2 = function() {
    testService.test2();
  };
  $scope.test3 = function() {
    testService.test3();
  };
}

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
  this.reset();
}
TestService.prototype.reset = function() {
  this.prop1 = 'unchenged';
  this.prop2 = 'unchenged2';
  this.prop3 = 'unchenged3';
}
TestService.prototype.test1 = function() {
  this.prop1 = 'changed_test_1';
  this.prop2 = 'changed2_test_1';
  this.prop3 = 'changed3_test_1';
}
TestService.prototype.test2 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
  }.bind(this));
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply();
  }.bind(this));
}
//END TEST APP CODE

//WATCH UTILS
var mod = angular.module('watch_utils', []);

mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // target not always equals $scope, for example when using bindToController syntax in 
  //directives
  return function watch_obj(obj, fields, target, $scope) {
    // if $scope is not provided, $rootScope is used
    $scope = $scope || $rootScope;
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class='test' ng-app="app" ng-controller="TestWatch">
  prop1: {{prop1}}
  <br>prop2: {{prop2}}
  <br>prop3 (unwatched): {{prop3}}
  <br>
  <button ng-click="test1()">
    Simple props change
  </button>
  <button ng-click="test2()">
    Async props change
  </button>
  <button ng-click="test3()">
    Async props change with apply
  </button>
</div>
0
Max

J'ai vu des modèles d'observateurs terribles qui provoquent des fuites de mémoire dans les applications volumineuses.

Je suis peut-être un peu en retard mais c'est aussi simple que cela.

La fonction de surveillance surveille les changements de référence (types primitifs) si vous voulez regarder quelque chose comme array push, utilisez simplement:

someArray.Push(someObj); someArray = someArray.splice(0);

Cela mettra à jour la référence et mettra à jour la montre de n'importe où. Y compris une méthode de getter de services. Tout ce qui est une primitive sera mis à jour automatiquement.

0
Oliver Dixon