AngularJS: Comment regarder les variables de service?

j'ai un service à la, dire:

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

  return service;
}]);

et je voudrais utiliser foo pour contrôler une liste qui est rendue en HTML:

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

pour que le contrôleur détecte quand aService.foo est mis à jour, j'ai bricolé Ce modèle où j'ajoute un service au $scope du contrôleur et 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. Y a-t-il une meilleure façon d'accomplir la surveillance des variables partagées?

399
demandé sur John Slegers 2012-09-25 09:41:06

21 réponses

vous pouvez toujours utiliser le bon vieux modèle d'observateur si vous voulez éviter la tyrannie et au-dessus 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
};
273
répondu dtheodor 2013-08-21 21:53:51

dans un scénario comme celui-ci, où plusieurs objets/unkown pourraient être intéressés par des changements, utilisez $rootScope.$broadcast de l'élément en cours de modification.

plutôt que de créer votre propre registre d'auditeurs (qui doivent être nettoyés sur divers $détruit), vous devriez être en mesure de $broadcast à partir du service en question.

vous devez toujours coder les gestionnaires $on dans chaque auditeur, mais le pattern est découplé des appels multiples vers $digest et évite ainsi le risque des observateurs de longue date.

de cette façon, aussi, les auditeurs peuvent aller et venir du DOM et/ou différents scopes enfant sans que le service change son comportement.

** mise à jour: exemples **

les émissions seraient les plus sensées dans les services" mondiaux " qui pourraient avoir un impact sur d'innombrables autres choses dans votre application. Un bon exemple est un service D'Utilisateur où il y a un nombre des événements qui pourraient avoir lieu tel que la connexion, la déconnexion, la mise à jour, ralenti, etc. Je crois que c'est là que les émissions sont le plus sensées parce que n'importe quel scope peut écouter un événement, sans même injecter le service, et il n'a pas besoin d'évaluer des expressions ou des résultats de cache pour inspecter les changements. Il vient de feux et oublie (donc, assurez-sûr que c'est un feu-et-oublier de notification, et non 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 () est terminée et que les données sont valides. Alternativement, si c'est une ressource $ou une soumission ajax, déplacez l'appel de diffusion dans le rappel de sorte qu'il déclenche quand le serveur a répondu. Les diffusions conviennent particulièrement bien à ce modèle parce que chaque auditeur attend simplement l'événement sans avoir à inspecter la portée sur 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 ceci est l'élimination de la plusieurs montres. Si vous combiniez des champs OU dériviez des valeurs comme dans l'exemple ci-dessus, vous devriez regarder à la fois 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, vous devriez faire une surveillance profonde et c'est plus intensif.

$broadcast envoie le message de la portée qu'il est appelé vers le bas dans n'importe quel enfant portée. Donc l'appeler de $ rootScope va tirer sur chaque scope. Si vous deviez diffuser $à partir de la portée de votre contrôleur, par exemple, cela ne se déclencherait que dans les portées héritées de la portée de votre contrôleur. $emit va dans la direction opposée et se comporte de la même manière qu'un événement DOM en ce qu'il remonte la chaîne scope.

gardez à l'esprit qu'il ya des scénarios où $broadcast fait beaucoup de sens, et il ya des scénarios où $watch est une meilleure option - surtout si dans une lunette isolante avec une expression de montre très spécifique.

222
répondu Matt Pileggi 2014-02-06 20:11:30

j'utilise une approche similaire à @dtheodot mais en utilisant la promesse angulaire au lieu de passer des appels

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

puis n'importe où il suffit d'utiliser myService.setFoo(foo) méthode pour mettre à jour foo sur le service. Dans votre controller vous pouvez l'utiliser comme:

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

les deux premiers arguments de then sont des callbacks de succès et d'erreur, le troisième est notify callback.

référence pour $ Q.

43
répondu Krym 2014-05-02 11:09:09

Sans montre ni rappel d'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 comme 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.

aussi noter que j'utilise ' var self = ceci' car j'ai besoin de garder une référence à l'objet original lorsque $timeout s'exécute, sinon 'ceci' se référera à l'objet window.

38
répondu Zymotik 2016-09-01 13:26:14

autant que je sache, vous n'avez pas à faire quelque chose d'aussi élaboré que ça. Vous avez déjà assigné foo du service à votre scope et puisque foo est 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 une autre variable de cette même Ctrl dépend de la modification de la foo, alors oui, vous aurez besoin d'une montre pour observer la foo et apporter des modifications à cette variable. Mais tant qu'il est une simple référence Regarder est inutile. Espérons que cette aide.

28
répondu ganaraj 2012-09-25 08:11:57

je suis tombé sur cette question à la recherche de quelque chose de similaire, mais je pense qu'il mérite une explication complète 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 HTML, Angular configure automatiquement un $watch pour $scope.foo , et mettra à jour le HTML chaque fois que $scope.foo change.

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

la question non dite ici est que l'un des deux les choses affectent aService.foo de sorte que les changements ne sont pas détectés. Ces deux possibilités sont:

  1. aService.foo est configuré sur un nouveau tableau à chaque fois, causant la référence est dépassée.
  2. aService.foo est mis à jour de telle sorte qu'un cycle $digest ne soit pas déclenché lors de la mise à jour.

Problème 1: Références Périmées

en considérant la première possibilité, en supposant qu'un $digest soit appliqué, si aService.foo était toujours le même tableau, le paramètre automatique $watch détecterait les changements, 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 sur 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 voir, le ng-repeat soi-disant attaché à aService.foo ne met pas à jour quand aService.foo change, mais le ng-repeat attaché à aService2.foo fait . C'est parce que notre référence à aService.foo est dépassée, 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é rejeté par le service sur sa prochaine mise à jour, ce qui signifie $scope.foo ne référencé plus le tableau que nous voulions plus.

Cependant, bien qu'il y ait plusieurs façons de s'assurer que la référence initiale est gardée en tact, il peut parfois être nécessaire de changer l'objet ou le tableau. Ou si la propriété de service fait référence à une primitive comme String ou Number ? Dans ces cas, nous ne pouvons pas simplement nous fier à une référence. Alors que peut nous faisons?

plusieurs des réponses données précédemment donnent déjà des solutions à ce problème. Toutefois, je suis personnellement en faveur de en utilisant la méthode simple suggérée par Jin et thetallweeks dans les commentaires:

juste référence aService.foo dans le balisage html

Solution 1-b: joindre le service à la portée, et la référence {service}.{property} dans le HTML.

ce qui veut dire, 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;
}

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="angular.js@1.4.7" 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 , qui obtiendra la valeur correctement mise à jour.

C'est un peu ce que vous essayiez de faire avec votre contournement, mais d'une manière beaucoup moins ronde. Vous avez ajouté un $watch inutile dans le contrôleur qui met explicitement foo sur le $scope chaque fois qu'il change. Vous n'avez pas besoin de ce supplément $watch lorsque vous attachez aService au lieu de aService.foo au $scope , et liez explicitement à aService.foo dans le markup.


maintenant, c'est bien, en supposant qu'un cycle $digest est appliqué. Dans les exemples ci-dessus, J'ai utilisé le service $interval D'Angular pour mettre à jour les tableaux, ce qui déclenche automatiquement une boucle $digest après chaque mise à jour. Mais et si les variables de service (pour quelque raison que ce soit) ne sont pas mises à jour dans le "monde angulaire"? En d'autres termes, nous dont avons un cycle $digest activé automatiquement chaque fois que la propriété de service Change?


Problème 2: Données Manquantes $digest

plusieurs des solutions ici vont résoudre ce problème, mais je suis d'accord avec code Whisper :

la raison pour laquelle nous utilisons un cadre comme Angular est de ne pas faire cuire 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, et ne pas avoir à enregistrer un rappel supplémentaire au sein du contrôleur.

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

j'ai été surpris personne n'a encore suggéré l'utilisation d'un setter et getter . Cette capacité a été introduite dans L'ECMAScript5 et existe donc depuis des années. Bien sûr, cela signifie que si, pour quelque raison que ce soit, vous avez besoin de prendre en charge des navigateurs très anciens, alors cette méthode ne fonctionnera pas, mais je pense que 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;
      }
    };
  // ...
}

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>

J'ai ajouté ici une variable "privée" dans la fonction de service: realFoo . Ce get est mis à jour et récupéré en utilisant les fonctions get foo() et set foo() respectivement sur l'objet service .

noter l'utilisation de $rootScope.$apply() dans la fonction set. Cela garantit que Angular sera au courant de tout changement à 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() .

se méfie également de cela si aService.foo est mis à jour très fréquemment, car cela pourrait avoir un impact significatif sur la performance. Si la performance serait un problème, vous pourriez configurer un pattern d'observateur similaire aux autres réponses ici en utilisant le setter.

28
répondu NanoWizard 2018-08-27 15:18:43

vous pouvez insérer le service en $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';
});

L'autre option est d'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 , le Rhinocéros de 1,7+

vous pouvez observer les changements d'un, plusieurs ou tous les attributs d'un 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");​
8
répondu Rodolfo Jorge Nemer Nogueira 2014-09-13 16:30:37

vous pouvez regarder les changements à l'intérieur de l'usine elle-même et ensuite 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 tous les codes d'usine liés dans sa description, alors vous ne pouvez compter sur la diffusion de l'extérieur

6
répondu Flavien Volken 2015-02-10 11:08:48

==mise à jour= =

très simple maintenant en $ montre.

Plume 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 + '!');
  });
}]);
5
répondu hayatbiralem 2016-07-23 14:04:30

s'appuyant sur dtheodor réponse vous pouvez utiliser quelque chose de similaire à l'-dessous pour vous assurer que vous n'oubliez pas d'annuler l'enregistrement de la fonction de rappel... Certains peuvent s'opposer à la $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();
  });
});

tableau.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
répondu Jamie 2013-09-02 11:37:48

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
répondu elitechance 2013-09-17 03:44:30

tout en faisant face à une question très similaire, j'ai regardé une fonction dans la portée et j'ai demandé à la fonction de retourner 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
répondu chandings 2014-12-29 08:01:43

j'ai posé 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 $interval angulaire. C'est également le cas pour setTimeout (utilisez $timeout à la place). Je sais que ce n'est pas la réponse à la question de L'OP, mais ça pourrait aider certains, comme ça m'a aidé.

2
répondu honkskillet 2015-02-18 01:02:36

j'ai trouvé une très bonne solution sur l'autre thread avec un problème similaire, mais approche totalement différente. Source: AngularJS : $regarder à l'intérieur de la directive n'est pas de travailler quand $rootScope valeur est modifiée

fondamentalement la solution là dit de ne pas utiliser $watch car c'est une solution très lourde. au lieu de ils proposent d'utiliser $emit et $on .

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

mon module / exemple de 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 I montre mon user - chaque fois qu'il est mis à la nouvelle valeur I $emit a 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 le directive j'écoute sur le $rootScope et sur le changement - je réagir respectivement. Très facile et élégant!

2
répondu Atais 2017-05-23 10:31:31

pour ceux comme moi juste à la recherche d'une solution simple, Cela fait presque exactement ce que vous attendez de l'utilisation normale $watch dans les contrôleurs. La seule différence est qu'il évalue la chaîne dans son contexte javascript et non pas 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 digestion.

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};
1
répondu Justus Wingert 2014-09-29 09:26:18

/ / 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
répondu Nawlbergs 2016-05-13 18:54:11

un peu moche, mais j'ai ajouté l'enregistrement des variables scope à 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;
});

puis dans le contrôleur:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}
1
répondu nclu 2016-07-18 23:51:48

j'ai vu ici de terribles modèles d'observateur qui causent 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 de montre montres pour des changements de référence (types primitifs) si vous voulez regarder quelque chose comme la poussée de réseau utiliser simplement:

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

cela mettra à jour la référence et mettre à jour la montre de n'importe où. Y compris les services de méthode de lecture. Tout ce qui est primitif sera mis à jour automatiquement.

0
répondu Oliver Dixon 2015-12-26 12:35:25

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

contrôleur

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

je pense que ça fera ce que vous voulez. Mon contrôleur n'arrête pas 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
répondu alaboudi 2016-08-18 20:00:13

j'ai écrit deux services utilitaires simples qui m'aident à suivre les changements de propriétés de service.

si vous voulez sauter l'explication longue, vous pouvez aller strait à 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 assigner au scope ' prop1 'et'prop2'. Avec le service de montre il ressemblera à cela:

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. s'appliquent Regarder obj est génial, mais il ne suffit pas si vous avez le code asynchrone à votre service. Dans ce cas, j'utilise un second 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 async 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 ça ensemble ressemblera à ceci (vous pouvez l'exécuter ou ouvrir 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>
0
répondu Max 2016-08-25 10:51:50

Regardez ce plongeur:: c'est l'exemple le plus simple que j'ai pu imaginer

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
répondu anandharshan 2016-09-07 11:44:37