diff options
| -rw-r--r-- | src/ng/rootScope.js | 141 | ||||
| -rw-r--r-- | test/ng/rootScopeSpec.js | 159 | 
2 files changed, 300 insertions, 0 deletions
| diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 48ba038f..1fad7b0e 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -320,6 +320,147 @@ function $RootScopeProvider(){          };        }, + +      /** +       * @ngdoc function +       * @name ng.$rootScope.Scope#$watchCollection +       * @methodOf ng.$rootScope.Scope +       * @function +       * +       * @description +       * Shallow watches the properties of an object and fires whenever any of the properties change +       * (for arrays this implies watching the array items, for object maps this implies watching the properties). +       * If a change is detected the `listener` callback is fired. +       * +       * - The `obj` collection is observed via standard $watch operation and is examined on every call to $digest() to +       *   see if any items have been added, removed, or moved. +       * - The `listener` is called whenever anything within the `obj` has changed. Examples include adding new items +       *   into the object or array, removing and moving items around. +       * +       * +       * # Example +       * <pre> +          $scope.names = ['igor', 'matias', 'misko', 'james']; +          $scope.dataCount = 4; + +          $scope.$watchCollection('names', function(newNames, oldNames) { +            $scope.dataCount = newNames.length; +          }); + +          expect($scope.dataCount).toEqual(4); +          $scope.$digest(); + +          //still at 4 ... no changes +          expect($scope.dataCount).toEqual(4); + +          $scope.names.pop(); +          $scope.$digest(); + +          //now there's been a change +          expect($scope.dataCount).toEqual(3); +       * </pre> +       * +       * +       * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The expression value +       *    should evaluate to an object or an array which is observed on each +       *    {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the collection will trigger +       *    a call to the `listener`. +       * +       * @param {function(newCollection, oldCollection, scope)} listener a callback function that is fired with both +       *    the `newCollection` and `oldCollection` as parameters. +       *    The `newCollection` object is the newly modified data obtained from the `obj` expression and the +       *    `oldCollection` object is a copy of the former collection data. +       *    The `scope` refers to the current scope. +       * +       * @returns {function()} Returns a de-registration function for this listener. When the de-registration function is executed +       * then the internal watch operation is terminated. +       */ +      $watchCollection: function(obj, listener) { +        var self = this; +        var oldValue; +        var newValue; +        var changeDetected = 0; +        var objGetter = $parse(obj); +        var internalArray = []; +        var internalObject = {}; +        var oldLength = 0; + +        function $watchCollectionWatch() { +          newValue = objGetter(self); +          var newLength, key; + +          if (!isObject(newValue)) { +            if (oldValue !== newValue) { +              oldValue = newValue; +              changeDetected++; +            } +          } else if (isArray(newValue)) { +            if (oldValue !== internalArray) { +              // we are transitioning from something which was not an array into array. +              oldValue = internalArray; +              oldLength = oldValue.length = 0; +              changeDetected++; +            } + +            newLength = newValue.length; + +            if (oldLength !== newLength) { +              // if lengths do not match we need to trigger change notification +              changeDetected++; +              oldValue.length = oldLength = newLength; +            } +            // copy the items to oldValue and look for changes. +            for (var i = 0; i < newLength; i++) { +              if (oldValue[i] !== newValue[i]) { +                changeDetected++; +                oldValue[i] = newValue[i]; +              } +            } +          } else { +            if (oldValue !== internalObject) { +              // we are transitioning from something which was not an object into object. +              oldValue = internalObject = {}; +              oldLength = 0; +              changeDetected++; +            } +            // copy the items to oldValue and look for changes. +            newLength = 0; +            for (key in newValue) { +              if (newValue.hasOwnProperty(key)) { +                newLength++; +                if (oldValue.hasOwnProperty(key)) { +                  if (oldValue[key] !== newValue[key]) { +                    changeDetected++; +                    oldValue[key] = newValue[key]; +                  } +                } else { +                  oldLength++; +                  oldValue[key] = newValue[key]; +                  changeDetected++; +                } +              } +            } +            if (oldLength > newLength) { +              // we used to have more keys, need to find them and destroy them. +              changeDetected++; +              for(key in oldValue) { +                if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { +                  oldLength--; +                  delete oldValue[key]; +                } +              } +            } +          } +          return changeDetected; +        } + +        function $watchCollectionAction() { +          listener(newValue, oldValue, self); +        } + +        return this.$watch($watchCollectionWatch, $watchCollectionAction); +      }, +        /**         * @ngdoc function         * @name ng.$rootScope.Scope#$digest diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 33db814c..f4b6e8a1 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -362,6 +362,165 @@ describe('Scope', function() {        $rootScope.$digest();        expect(log).toEqual([]);      })); + +    describe('$watchCollection', function() { +      var log, $rootScope, deregister; + +      beforeEach(inject(function(_$rootScope_) { +        log = []; +        $rootScope = _$rootScope_; +        deregister = $rootScope.$watchCollection('obj', function logger(obj) { +          log.push(toJson(obj)); +        }); +      })); + + +      it('should not trigger if nothing change', inject(function($rootScope) { +        $rootScope.$digest(); +        expect(log).toEqual([undefined]); + +        $rootScope.$digest(); +        expect(log).toEqual([undefined]); +      })); + + +      it('should allow deregistration', inject(function($rootScope) { +        $rootScope.obj = []; +        $rootScope.$digest(); + +        expect(log).toEqual(['[]']); + +        $rootScope.obj.push('a'); +        deregister(); + +        $rootScope.$digest(); +        expect(log).toEqual(['[]']); +      })); + + +      describe('array', function() { +        it('should trigger when property changes into array', function() { +          $rootScope.obj = 'test'; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"']); + +          $rootScope.obj = []; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"', '[]']); + +          $rootScope.obj = {}; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"', '[]', '{}']); + +          $rootScope.obj = []; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"', '[]', '{}', '[]']); + +          $rootScope.obj = undefined; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"', '[]', '{}', '[]', undefined]); +        }); + + +        it('should not trigger change when object in collection changes', function() { +          $rootScope.obj = [{}]; +          $rootScope.$digest(); +          expect(log).toEqual(['[{}]']); + +          $rootScope.obj[0].name = 'foo'; +          $rootScope.$digest(); +          expect(log).toEqual(['[{}]']); +        }); + + +        it('should watch array properties', function() { +          $rootScope.obj = []; +          $rootScope.$digest(); +          expect(log).toEqual(['[]']); + +          $rootScope.obj.push('a'); +          $rootScope.$digest(); +          expect(log).toEqual(['[]', '["a"]']); + +          $rootScope.obj[0] = 'b'; +          $rootScope.$digest(); +          expect(log).toEqual(['[]', '["a"]', '["b"]']); + +          $rootScope.obj.push([]); +          $rootScope.obj.push({}); +          log = []; +          $rootScope.$digest(); +          expect(log).toEqual(['["b",[],{}]']); + +          var temp = $rootScope.obj[1]; +          $rootScope.obj[1] = $rootScope.obj[2]; +          $rootScope.obj[2] = temp; +          $rootScope.$digest(); +          expect(log).toEqual([ '["b",[],{}]', '["b",{},[]]' ]); + +          $rootScope.obj.shift() +          log = []; +          $rootScope.$digest(); +          expect(log).toEqual([ '[{},[]]' ]); +        }); +      }); + + +      describe('object', function() { +        it('should trigger when property changes into object', function() { +          $rootScope.obj = 'test'; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"']); + +          $rootScope.obj = {}; +          $rootScope.$digest(); +          expect(log).toEqual(['"test"', '{}']); +        }); + + +        it('should not trigger change when object in collection changes', function() { +          $rootScope.obj = {name: {}}; +          $rootScope.$digest(); +          expect(log).toEqual(['{"name":{}}']); + +          $rootScope.obj.name.bar = 'foo'; +          $rootScope.$digest(); +          expect(log).toEqual(['{"name":{}}']); +        }); + + +        it('should watch object properties', function() { +          $rootScope.obj = {}; +          $rootScope.$digest(); +          expect(log).toEqual(['{}']); + +          $rootScope.obj.a= 'A'; +          $rootScope.$digest(); +          expect(log).toEqual(['{}', '{"a":"A"}']); + +          $rootScope.obj.a = 'B'; +          $rootScope.$digest(); +          expect(log).toEqual(['{}', '{"a":"A"}', '{"a":"B"}']); + +          $rootScope.obj.b = []; +          $rootScope.obj.c = {}; +          log = []; +          $rootScope.$digest(); +          expect(log).toEqual(['{"a":"B","b":[],"c":{}}']); + +          var temp = $rootScope.obj.a; +          $rootScope.obj.a = $rootScope.obj.b; +          $rootScope.obj.c = temp; +          $rootScope.$digest(); +          expect(log).toEqual([ '{"a":"B","b":[],"c":{}}', '{"a":[],"b":[],"c":"B"}' ]); + +          delete $rootScope.obj.a; +          log = []; +          $rootScope.$digest(); +          expect(log).toEqual([ '{"b":[],"c":"B"}' ]); +        }) +      }); +    });    }); | 
