diff options
Diffstat (limited to 'src/ng/directive/ngRepeat.js')
| -rw-r--r-- | src/ng/directive/ngRepeat.js | 181 | 
1 files changed, 181 insertions, 0 deletions
| diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js new file mode 100644 index 00000000..82f8b9c7 --- /dev/null +++ b/src/ng/directive/ngRepeat.js @@ -0,0 +1,181 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-repeat + * + * @description + * The `ng-repeat` directive instantiates a template once per item from a collection. Each template + * instance gets its own scope, where the given loop variable is set to the current collection item, + * and `$index` is set to the item index or key. + * + * Special properties are exposed on the local scope of each template instance, including: + * + *   * `$index` – `{number}` – iterator offset of the repeated element (0..length-1) + *   * `$position` – `{string}` – position of the repeated element in the iterator. One of: + *        * `'first'`, + *        * `'middle'` + *        * `'last'` + * + * + * @element ANY + * @scope + * @priority 1000 + * @param {repeat_expression} ng-repeat The expression indicating how to enumerate a collection. Two + *   formats are currently supported: + * + *   * `variable in expression` – where variable is the user defined loop variable and `expression` + *     is a scope expression giving the collection to enumerate. + * + *     For example: `track in cd.tracks`. + * + *   * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, + *     and `expression` is the scope expression giving the collection to enumerate. + * + *     For example: `(name, age) in {'adam':10, 'amalie':12}`. + * + * @example + * This example initializes the scope to a list of names and + * then uses `ng-repeat` to display every person: +    <doc:example> +      <doc:source> +        <div ng-init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]"> +          I have {{friends.length}} friends. They are: +          <ul> +            <li ng-repeat="friend in friends"> +              [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +            </li> +          </ul> +        </div> +      </doc:source> +      <doc:scenario> +         it('should check ng-repeat', function() { +           var r = using('.doc-example-live').repeater('ul li'); +           expect(r.count()).toBe(2); +           expect(r.row(0)).toEqual(["1","John","25"]); +           expect(r.row(1)).toEqual(["2","Mary","28"]); +         }); +      </doc:scenario> +    </doc:example> + */ +var ngRepeatDirective = ngDirective({ +  transclude: 'element', +  priority: 1000, +  terminal: true, +  compile: function(element, attr, linker) { +    return function(scope, iterStartElement, attr){ +      var expression = attr.ngRepeat; +      var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), +        lhs, rhs, valueIdent, keyIdent; +      if (! match) { +        throw Error("Expected ng-repeat in form of '_item_ in _collection_' but got '" + +          expression + "'."); +      } +      lhs = match[1]; +      rhs = match[2]; +      match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); +      if (!match) { +        throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + +            lhs + "'."); +      } +      valueIdent = match[3] || match[1]; +      keyIdent = match[2]; + +      // Store a list of elements from previous run. This is a hash where key is the item from the +      // iterator, and the value is an array of objects with following properties. +      //   - scope: bound scope +      //   - element: previous element. +      //   - index: position +      // We need an array of these objects since the same object can be returned from the iterator. +      // We expect this to be a rare case. +      var lastOrder = new HashQueueMap(); +      scope.$watch(function(scope){ +        var index, length, +            collection = scope.$eval(rhs), +            collectionLength = size(collection, true), +            childScope, +            // Same as lastOrder but it has the current state. It will become the +            // lastOrder on the next iteration. +            nextOrder = new HashQueueMap(), +            key, value, // key/value of iteration +            array, last,       // last object information {scope, element, index} +            cursor = iterStartElement;     // current position of the node + +        if (!isArray(collection)) { +          // if object, extract keys, sort them and use to determine order of iteration over obj props +          array = []; +          for(key in collection) { +            if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { +              array.push(key); +            } +          } +          array.sort(); +        } else { +          array = collection || []; +        } + +        // we are not using forEach for perf reasons (trying to avoid #call) +        for (index = 0, length = array.length; index < length; index++) { +          key = (collection === array) ? index : array[index]; +          value = collection[key]; +          last = lastOrder.shift(value); +          if (last) { +            // if we have already seen this object, then we need to reuse the +            // associated scope/element +            childScope = last.scope; +            nextOrder.push(value, last); + +            if (index === last.index) { +              // do nothing +              cursor = last.element; +            } else { +              // existing item which got moved +              last.index = index; +              // This may be a noop, if the element is next, but I don't know of a good way to +              // figure this out,  since it would require extra DOM access, so let's just hope that +              // the browsers realizes that it is noop, and treats it as such. +              cursor.after(last.element); +              cursor = last.element; +            } +          } else { +            // new item which we don't know about +            childScope = scope.$new(); +          } + +          childScope[valueIdent] = value; +          if (keyIdent) childScope[keyIdent] = key; +          childScope.$index = index; +          childScope.$position = index === 0 ? +              'first' : +              (index == collectionLength - 1 ? 'last' : 'middle'); + +          if (!last) { +            linker(childScope, function(clone){ +              cursor.after(clone); +              last = { +                  scope: childScope, +                  element: (cursor = clone), +                  index: index +                }; +              nextOrder.push(value, last); +            }); +          } +        } + +        //shrink children +        for (key in lastOrder) { +          if (lastOrder.hasOwnProperty(key)) { +            array = lastOrder[key]; +            while(array.length) { +              value = array.pop(); +              value.element.remove(); +              value.scope.$destroy(); +            } +          } +        } + +        lastOrder = nextOrder; +      }); +    }; +  } +}); | 
