AngularJS: Comment regarder les variables de service?


414

J'ai un service, disons:

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

  return service;
}]);

Et je voudrais utiliser foopour contrôler une liste qui est rendue en HTML:

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

Afin que le contrôleur détecte quand aService.fooest mis à jour, j'ai bricolé ce modèle où j'ajoute un service au contrôleur $scopeet utilise ensuite $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 l'ai répété dans chaque contrôleur qui utilise les variables du service. Existe-t-il un meilleur moyen d'accomplir l'observation des variables partagées?


1
Vous pouvez passer un troisième paramètre à $ watch défini sur true pour surveiller en profondeur aService et toutes ses propriétés.
SirTophamHatt

7
$ scope.foo = aService.foo est suffisant, vous pouvez perdre la ligne ci-dessus. Et ce qu'il fait à l'intérieur de $ watch n'a pas de sens, si vous voulez attribuer une nouvelle valeur à $ scope.foo, faites-le ...
Jin

4
Pourriez-vous simplement faire référence aService.foodans le balisage html? (comme ceci: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
theetallweeks

1
J'ai ajouté un exemple sans rappels ni montres $, voir la réponse ci-dessous ( jsfiddle.net/zymotik/853wvv7s )
Zymotik

1
@MikeGledhill, vous avez raison. Je pense que c'est en raison de la nature de Javascript, vous pouvez voir ce modèle à de nombreux autres endroits (pas seulement en angulaire, mais en JS en général). D'une part, vous transférez la valeur (et elle n'est pas liée) et d'autre part vous transférez un objet (ou la valeur qui référence l'objet ...), et c'est pourquoi les propriétés sont correctement mises à jour (comme parfaitement montré dans l'exemple de Zymotik ci-dessus).
Christophe Vidal

Réponses:


277

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

21
@Moo écoutez l' $destoryévénement sur la portée et ajoutez une méthode de désenregistrement àaService
Jamie

13
Quels sont les avantages de cette solution? Il a besoin de plus de code dans un service, et à peu près la même quantité de code dans un contrôleur (car nous devons également nous désinscrire sur $ destroy). Je pourrais dire pour la vitesse d'exécution, mais dans la plupart des cas, cela n'a pas d'importance.
Alex Che

6
Je ne sais pas en quoi c'est une meilleure solution que $ watch, l'interrogateur demandait un moyen simple de partager les données, cela semble encore plus encombrant. Je préfère utiliser $ broadcast plutôt que cela
Jin

11
$watchvs modèle d'observateur consiste simplement à choisir d'interroger ou de pousser, et est fondamentalement une question de performance, alors utilisez-le lorsque la performance est importante. J'utilise un modèle d'observation alors que sinon je devrais regarder "en profondeur" des objets complexes. J'attache des services entiers à la portée $ au lieu de regarder des valeurs de service uniques. J'évite la veille angulaire comme le diable, il y en a assez dans les directives et dans la liaison de données angulaire native.
dtheodor

107
La raison pour laquelle nous utilisons un cadre comme Angular est de ne pas préparer nos propres modèles d'observateurs.
Code Whisperer

230

Dans un scénario comme celui-ci, où plusieurs objets inconnus peuvent être intéressés par des modifications, utilisez-les à $rootScope.$broadcastpartir de l'élément modifié.

Plutôt que de créer votre propre registre d'écouteurs (qui doivent être nettoyés lors de divers $ destroys), vous devriez pouvoir accéder au $broadcastservice en question.

Vous devez toujours coder les $ongestionnaires de chaque écouteur, mais le modèle est découplé de plusieurs appels vers $digestet évite ainsi le risque d'observateurs de longue durée.

De cette façon, les auditeurs peuvent également aller et venir à partir du DOM et / ou de différentes étendues enfants sans que le service ne modifie son comportement.

** mise à jour: exemples **

Les diffusions auraient le plus de sens dans les services "mondiaux" qui pourraient avoir un impact sur d'innombrables autres choses dans votre application. Un bon exemple est un service utilisateur où il y a un certain nombre d'événements qui pourraient avoir lieu tels que la connexion, la déconnexion, la mise à jour, l'inactivité, etc. même en injectant le service, et il n'a pas besoin d'évaluer les expressions ou les résultats de cache pour inspecter les changements. Il se déclenche et oublie (assurez-vous donc qu'il s'agit d'une notification de feu et d'oubli, pas 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 () était terminée et que les données étaient valides. Alternativement, s'il s'agit d'une ressource $ ou d'une soumission ajax, déplacez l'appel de diffusion dans le rappel pour qu'il se déclenche lorsque le serveur a répondu. Les diffusions conviennent particulièrement bien à ce modèle car chaque auditeur n'attend que l'événement sans avoir à inspecter la portée de chaque digest $. L'auditeur ressemblerait à:

.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 cela est l'élimination de plusieurs montres. Si vous combiniez des champs ou dériviez des valeurs comme dans l'exemple ci-dessus, vous devrez 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 l'objet utilisateur avait simplement ses propriétés mises à jour. Dans ce cas, il faudrait faire une surveillance approfondie et c'est plus intensif.

$ broadcast envoie le message de l'étendue à laquelle il est appelé dans toutes les étendues enfants. Donc, l'appeler à partir de $ rootScope se déclenchera sur chaque étendue. Si vous diffusiez $ à partir de la portée de votre contrôleur, par exemple, il se déclencherait uniquement dans les étendues héritant de la portée de votre contrôleur. $ emit va dans la direction opposée et se comporte de manière similaire à un événement DOM en ce sens qu'il bouillonne dans la chaîne de portée.

Gardez à l'esprit qu'il y a des scénarios où $ broadcast a beaucoup de sens, et il y a des scénarios où $ watch est une meilleure option - surtout si dans une portée isolée avec une expression de surveillance très spécifique.


1
Sortir du cycle $ digest est une bonne chose, surtout si les changements que vous regardez ne sont pas une valeur qui ira directement et immédiatement dans le DOM.
XML

Est-il là pour éviter la méthode .save (). Cela semble exagéré lorsque vous surveillez simplement la mise à jour d'une seule variable dans le sharedService. Peut-on regarder la variable depuis le sharedService et la diffuser lorsqu'elle change?
JerryKur

J'ai essayé plusieurs façons de partager des données entre les contrôleurs, mais c'est le seul qui a fonctionné. Bien joué, monsieur.
abettermap

Je préfère cela aux autres réponses, semble moins hacky , merci
JMK

9
Il s'agit du modèle de conception correct uniquement si votre contrôleur consommateur a plusieurs sources possibles de données; en d'autres termes, si vous avez une situation MIMO (Multiple Input / Multiple Output). Si vous utilisez simplement un modèle un-à-plusieurs, vous devez utiliser le référencement d'objet direct et laisser le cadre angulaire faire la liaison bidirectionnelle pour vous. Horkyze a lié cela ci-dessous, et c'est une bonne explication de la liaison bidirectionnelle automatique, et ses limites: stsc3000.github.io/blog/2013/10/26/…
Charles

47

J'utilise une approche similaire à @dtheodot mais en utilisant une promesse angulaire 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, utilisez la myService.setFoo(foo)méthode pour mettre à jour foole service. Dans votre contrôleur, vous pouvez l'utiliser comme:

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

Les deux premiers arguments thensont les rappels de réussite et d'erreur, le troisième est notifier le rappel.

Référence pour $ q.


Quel serait l'avantage de cette méthode sur la diffusion $ décrite ci-dessous par Matt Pileggi?
Fabio

Eh bien, les deux méthodes ont leur utilité. Les avantages de la diffusion pour moi seraient la lisibilité humaine et la possibilité d'écouter sur plus d'endroits le même événement. Je suppose que le principal inconvénient est que la diffusion émet un message vers toutes les étendues descendantes, ce qui peut donc être un problème de performances.
Krym

2
J'avais un problème où faire $scope.$watchsur une variable de service ne semblait pas fonctionner (la portée que je regardais était un modal hérité de $rootScope) - cela a fonctionné. Astuce cool, merci pour le partage!
Seiyria

4
Comment vous nettoieriez-vous avec cette approche? Est-il possible de supprimer le rappel enregistré de la promesse lorsque la portée est détruite?
Abris

Bonne question. Honnêtement, je ne sais pas. J'essaierai de faire des tests sur la façon dont vous pouvez supprimer le rappel de la promesse.
Krym

41

Sans montres ni rappels d'observateurs ( 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 montres à toutes ses propriétés.

Notez également que j'utilise 'var self = this' car j'ai besoin de garder une référence à l'objet d'origine lorsque le $ timeout s'exécute, sinon 'this' fera référence à l'objet window.


3
C'est une excellente approche! Existe-t-il un moyen de lier juste une propriété d'un service à l'étendue au lieu du service entier? Faire $scope.count = service.countne fonctionne pas.
jvannistelrooy

Vous pouvez également imbriquer la propriété à l'intérieur d'un objet (arbitraire) afin qu'elle soit transmise par référence. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross

1
Excellente approche! Bien qu'il y ait beaucoup de réponses solides et fonctionnelles sur cette page, c'est de loin a) la plus facile à implémenter, et b) la plus facile à comprendre lors de la lecture du code. Cette réponse devrait être beaucoup plus élevée qu'elle ne l'est actuellement.
CodeMoose

Merci @CodeMoose, je l'ai encore simplifié aujourd'hui pour ceux qui ne connaissent pas AngularJS / JavaScript.
Zymotik

2
Que Dieu vous bénisse. J'ai perdu des millions d'heures, je dirais. Parce que je luttais avec 1.5 et angularjs passant de 1 à 2 et que je voulais aussi partager des données
Amna

29

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

Lorsqu'une expression angulaire telle que celle que vous avez utilisée est présente dans le code HTML, Angular configure automatiquement un $watchfor $scope.fooet met à jour le code HTML à chaque $scope.foomodification.

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

Le problème non dit ici est que l'une des deux choses affecte de aService.foo telle sorte que les changements ne sont pas détectés. Ces deux possibilités sont:

  1. aService.foo est mis à un nouveau tableau à chaque fois, ce qui rend la référence obsolète.
  2. aService.fooest mis à jour de manière à ce $digestqu'aucun cycle ne se déclenche sur la mise à jour.

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

Compte tenu de la première possibilité, en supposant que a $digestest appliqué, s'il aService.fooétait toujours le même tableau, le paramètre défini automatiquement $watchdé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

Comme vous pouvez le voir, la répétition ng supposément attachée à aService.foone se met pas à jour lors des aService.foochangements, mais la répétition ng attachée à le aService2.foo fait . C'est parce que notre référence à aService.fooest dépassée, mais notre référence à aService2.foone l'est pas. Nous avons créé une référence au tableau initial avec $scope.foo = aService.foo;, qui a ensuite été supprimée par le service lors de sa prochaine mise à jour, ce qui signifie qu'il $scope.foone 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 que faire si la propriété de service fait référence à une primitive comme un Stringou Number? Dans ces cas, nous ne pouvons pas simplement nous fier à une référence. Alors, que pouvons- nous faire?

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

il suffit de référencer aService.foo dans le balisage html

Solution 1-b: Attachez le service à l'étendue et référence {service}.{property}dans le HTML.

Signification, faites juste ceci:

HTML:

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

JS:

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

De cette façon, le $watchsera résolu aService.foosur chacun $digest, ce qui obtiendra la valeur correctement mise à jour.

C'est un peu ce que vous essayez de faire avec votre solution de contournement, mais de manière beaucoup moins détournée. Vous avez ajouté un inutile $watchdans le contrôleur qui met explicitement foole $scopechaque fois qu'il change. Vous n'avez pas besoin de cet extra $watchlorsque vous attachez aServiceau lieu de aService.fooà $scope, et que vous vous liez explicitement à aService.foodans le balisage.


Maintenant, tout va bien en supposant qu'un $digestcycle est appliqué. Dans mes exemples ci-dessus, j'ai utilisé le $intervalservice d' Angular pour mettre à jour les tableaux, qui lance automatiquement une $digestboucle après chaque mise à jour. Mais que faire si les variables de service (pour une raison quelconque) ne sont pas mises à jour dans le "monde angulaire". En d' autres termes, nous DonT avons un $digestcycle étant activé automatiquement chaque fois que la propriété du service change?


Problème 2: manquant $digest

De nombreuses solutions ici permettront de résoudre ce problème, mais je suis d'accord avec Code Whisperer :

La raison pour laquelle nous utilisons un cadre comme Angular est de ne pas préparer nos propres modèles d'observateurs

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

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

J'ai été surpris que personne n'ait encore suggéré l'utilisation d'un setter et d'un getter . Cette capacité 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 anciens navigateurs, cette méthode ne fonctionnera pas, mais j'ai l'impression que les getters et 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;
      }
    };
  // ...
}

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

Notez l'utilisation de $rootScope.$apply()dans la fonction set. Cela garantit qu'Angular sera au courant de toute modification apportée à service.foo. Si vous obtenez des erreurs «inprog», consultez cette page de référence utile , ou si vous utilisez Angular> = 1.3, vous pouvez simplement l'utiliser $rootScope.$applyAsync().

Méfiez-vous également de cela si la aService.foomise à jour est très fréquente, car cela pourrait avoir un impact significatif sur les performances. Si les performances sont un problème, vous pouvez configurer un modèle d'observateur similaire aux autres réponses ici à l'aide du setter.


3
C'est la solution correcte et la plus simple. Comme le dit @NanoWizard, $ digest servicesne recherche pas les propriétés qui appartiennent au service lui-même.
Sarpdoruk Tahmaz

28

Autant que je sache, vous n'avez pas à faire quelque chose d'aussi élaboré que cela. Vous avez déjà affecté foo du service à votre portée et puisque foo est un tableau (et à son tour un objet, il est attribué 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 variables de ce même Ctrl dépendent du changement de foo, alors oui, vous auriez besoin d'une montre pour observer foo et apporter des modifications à cette variable. Mais tant qu'il s'agit d'une simple consultation de référence, il n'est pas nécessaire. J'espère que cela t'aides.


35
J'ai essayé et je n'ai pas pu $watchtravailler avec une primitive. Au lieu de cela, je définissais une méthode sur le service qui renvoie la valeur primitive: somePrimitive() = function() { return somePrimitive }Et j'attribue une propriété de portée de $ à cette méthode: $scope.somePrimitive = aService.somePrimitive;. J'ai ensuite utilisé la méthode scope dans le HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok

4
@MarkRajcok Non, n'utilisez pas de primitives. Ajoutez-les dans un objet. Les primitives ne sont pas mutables et donc la liaison de données bidirectionnelle ne fonctionnera pas
Jimmy Kane

3
@JimmyKane, oui, les primitives ne doivent pas être utilisées pour la liaison de données bidirectionnelle, mais je pense que la question portait sur l'observation des variables de service, et non sur la configuration de la liaison bidirectionnelle. Si vous avez seulement besoin de regarder une propriété / variable de service, un objet n'est pas requis - une primitive peut être utilisée.
Mark Rajcok

3
Dans cette configuration, je peux modifier les valeurs aService de la portée. Mais la portée ne change pas en réponse au changement de aService.
Ouwen Huang

4
Cela ne fonctionne pas non plus pour moi. L'affectation simple $scope.foo = aService.foone met pas automatiquement à jour la variable d'étendue.
Darwin Tech

9

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 comme: 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 changements 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");​

2
JE NE PEUX PAS CROIRE QUE CETTE RÉPONSE N'EST PAS LA PLUS MOBILE! Il s'agit de la solution la plus élégante (IMO) car elle réduit l'entropie informationnelle et atténue probablement le besoin de gestionnaires de médiation supplémentaires. Je voterais plus si je pouvais ...
Cody

L'ajout de tous vos services au $ rootScope, ses avantages et ses pièges potentiels sont détaillés un peu ici: stackoverflow.com/questions/14573023/…
Zymotik

6

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 façon, vous mettez tout le code d'usine associé dans sa description, vous ne pouvez compter que sur la diffusion de l'extérieur


6

== MISE À JOUR ==

Très simple maintenant dans $ watch.

Pen 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 + '!');
  });
}]);

1
j'aime le PostmanService mais comment dois-je changer la fonction $ watch sur le contrôleur si j'ai besoin d'écouter plus d'une variable?
jedi

Salut jedi, merci pour la tête! J'ai mis à jour le stylo et la réponse. Je recommande d'ajouter une autre fonction de montre pour cela. J'ai donc ajouté une nouvelle fonctionnalité à PostmanService. J'espère que cela aide :)
hayatbiralem

En fait, oui c'est le cas :) Si vous partagez plus de détails sur le problème, je peux peut-être vous aider.
hayatbiralem

4

En vous basant sur la réponse de dtheodor, vous pouvez utiliser quelque chose de similaire au ci-dessous pour vous assurer de ne pas oublier de désenregistrer le rappel ... Certains peuvent cependant s'opposer à la transmission $scopeà 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;
};

2

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

Pour ceux comme moi qui recherchent une solution simple, cela fait presque exactement ce que vous attendez de l'utilisation de $ watch normal dans les contrôleurs. La seule différence est qu'il évalue la chaîne dans son contexte javascript et non sur une portée 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

face à un problème très similaire, j'ai regardé une fonction dans la portée et j'ai demandé à la fonction de renvoyer la variable service J'ai créé un violon js . 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

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 angulaire $ 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 aidé.


Vous pouvez utiliser setTimeout, ou toute autre fonction non angulaire, mais n'oubliez pas d'envelopper le code dans le rappel avec $scope.$apply().
magnetronnie

2

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

Fondamentalement, la solution y indique de NE PAS utiliser $watchcar c'est une solution très lourde. Au lieu de cela, ils proposent d'utiliser $emitet$on .

Mon problème était de regarder une variable dans mon service et de réagir en 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 regarde mon user - chaque fois qu'il est réglé sur une nouvelle valeur, j'ai $emitun user:changestatut.

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

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 $rootScopeet sur le changement donné - je réagis respectivement. Très simple et élégant!


1

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

Un 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');
}

Merci. Une petite question: pourquoi revenez-vous thisde ce service au lieu de self?
shrekuu

4
Parce que des erreurs se font parfois. ;-)
nclu

Bonne pratique pour return this;sortir de vos constructeurs ;-)
Cody

1

Jetez un oeil à ce plongeur :: c'est l'exemple le plus simple auquel 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;
});

0

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

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

La fonction watch surveille les changements de référence (types primitifs) si vous voulez regarder quelque chose comme push de tableau, 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 getter de services. Tout ce qui est une primitive sera mis à jour automatiquement.


0

Je suis en retard dans la partie, mais j'ai trouvé une meilleure façon de le faire que la réponse affichée ci-dessus. Au lieu d'attribuer une variable pour contenir la valeur de la variable de service, j'ai créé une fonction attachée à la portée, qui renvoie la variable de service.

manette

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


pourquoi il a été rejeté. J'ai également utilisé plusieurs fois des techniques similaires et cela a fonctionné.
indéfini

0

J'ai écrit deux services utilitaires simples qui m'aident à suivre les modifications des propriétés des services.

Si vous voulez sauter la longue explication, vous pouvez aller directement à 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 souhaitez surveiller et affecter à l'étendue «prop1» et «prop2». Avec le service de montre, 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 ce n'est pas suffisant si vous avez du code asynchrone dans votre service. Pour ce cas, j'utilise un deuxième utilitaire qui ressemble à ça:

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 ressemblera à ça (vous pouvez l'exécuter ou ouvrir le violon ):

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.