AngularJS : How to watch service variables?

后端 未结 21 1386
盖世英雄少女心
盖世英雄少女心 2020-11-22 09:12

I have a service, say:

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

  return          


        
相关标签:
21条回答
  • 2020-11-22 09:20

    In a scenario like this, where multiple/unkown objects might be interested in changes, use $rootScope.$broadcast from the item being changed.

    Rather than creating your own registry of listeners (which have to be cleaned up on various $destroys), you should be able to $broadcast from the service in question.

    You must still code the $on handlers in each listener but the pattern is decoupled from multiple calls to $digest and thus avoids the risk of long-running watchers.

    This way, also, listeners can come and go from the DOM and/or different child scopes without the service changing its behavior.

    ** update: examples **

    Broadcasts would make the most sense in "global" services that could impact countless other things in your app. A good example is a User service where there are a number of events that could take place such as login, logout, update, idle, etc. I believe this is where broadcasts make the most sense because any scope can listen for an event, without even injecting the service, and it doesn't need to evaluate any expressions or cache results to inspect for changes. It just fires and forgets (so make sure it's a fire-and-forget notification, not something that requires 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;
    }]);
    

    The service above would broadcast a message to every scope when the save() function completed and the data was valid. Alternatively, if it's a $resource or an ajax submission, move the broadcast call into the callback so it fires when the server has responded. Broadcasts suit that pattern particularly well because every listener just waits for the event without the need to inspect the scope on every single $digest. The listener would look like:

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

    One of the benefits of this is the elimination of multiple watches. If you were combining fields or deriving values like the example above, you'd have to watch both the firstname and lastname properties. Watching the getUser() function would only work if the user object was replaced on updates, it would not fire if the user object merely had its properties updated. In which case you'd have to do a deep watch and that is more intensive.

    $broadcast sends the message from the scope it's called on down into any child scopes. So calling it from $rootScope will fire on every scope. If you were to $broadcast from your controller's scope, for example, it would fire only in the scopes that inherit from your controller scope. $emit goes the opposite direction and behaves similarly to a DOM event in that it bubbles up the scope chain.

    Keep in mind that there are scenarios where $broadcast makes a lot of sense, and there are scenarios where $watch is a better option - especially if in an isolate scope with a very specific watch expression.

    0 讨论(0)
  • 2020-11-22 09:20

    ==UPDATED==

    Very simple now in $watch.

    Pen here.

    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 + '!');
      });
    }]);
    
    0 讨论(0)
  • 2020-11-22 09:20

    I came to this question but it turned out my problem was that I was using setInterval when I should have been using the angular $interval provider. This is also the case for setTimeout (use $timeout instead). I know it's not the answer to the OP's question, but it might help some, as it helped me.

    0 讨论(0)
  • 2020-11-22 09:22

    As far as I can tell, you dont have to do something as elaborate as that. You have already assigned foo from the service to your scope and since foo is an array ( and in turn an object it is assigned by reference! ). So, all that you need to do is something like this :

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

    If some, other variable in this same Ctrl is dependant on foo changing then yes, you would need a watch to observe foo and make changes to that variable. But as long as it is a simple reference watching is unnecessary. Hope this helps.

    0 讨论(0)
  • 2020-11-22 09:22

    For those like me just looking for a simple solution, this does almost exactly what you expect from using normal $watch in controllers. The only difference is, that it evaluates the string in it's javascript context and not on a specific scope. You'll have to inject $rootScope into your service, although it is only used to hook into the digest cycles properly.

    function watch(target, callback, deep) {
        $rootScope.$watch(function () {return eval(target);}, callback, deep);
    };
    
    0 讨论(0)
  • 2020-11-22 09:22

    Have a look at this plunker:: this is the simplest example i could think of

    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 讨论(0)
提交回复
热议问题