diff options
| author | Misko Hevery | 2013-05-24 12:41:38 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2013-05-28 22:28:32 -0700 | 
| commit | e46100f7097d9a8f174bdb9e15d4c6098395c3f2 (patch) | |
| tree | 781564141fc9cf580886201d97f7d45064218d82 | |
| parent | b8ea7f6aba2e675b85826b0bee1f21ddd7b866a5 (diff) | |
| download | angular.js-e46100f7097d9a8f174bdb9e15d4c6098395c3f2.tar.bz2 | |
feat($compile): support multi-element directive
By appending  directive-start and directive-end to a
directive it is now possible to have the directive
act on a group of elements.
It is now possible to iterate over multiple elements like so:
<table>
  <tr ng-repeat-start="item in list">I get repeated</tr>
  <tr ng-repeat-end>I also get repeated</tr>
</table>
| -rw-r--r-- | src/jqLite.js | 50 | ||||
| -rw-r--r-- | src/ng/animator.js | 15 | ||||
| -rw-r--r-- | src/ng/compile.js | 123 | ||||
| -rw-r--r-- | src/ng/directive/ngRepeat.js | 2 | ||||
| -rw-r--r-- | test/jqLiteSpec.js | 12 | ||||
| -rwxr-xr-x | test/ng/compileSpec.js | 125 | 
6 files changed, 276 insertions, 51 deletions
| diff --git a/src/jqLite.js b/src/jqLite.js index cf9d1fa1..4959a2ed 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -165,7 +165,8 @@ function JQLite(element) {      div.innerHTML = '<div> </div>' + element; // IE insanity to make NoScope elements work!      div.removeChild(div.firstChild); // remove the superfluous div      JQLiteAddNodes(this, div.childNodes); -    this.remove(); // detach the elements from the temporary DOM div. +    var fragment = jqLite(document.createDocumentFragment()); +    fragment.append(this); // detach the elements from the temporary DOM div.    } else {      JQLiteAddNodes(this, element);    } @@ -456,24 +457,26 @@ forEach({      }    }, -  text: extend((msie < 9) -      ? function(element, value) { -        if (element.nodeType == 1 /** Element */) { -          if (isUndefined(value)) -            return element.innerText; -          element.innerText = value; -        } else { -          if (isUndefined(value)) -            return element.nodeValue; -          element.nodeValue = value; -        } +  text: (function() { +    var NODE_TYPE_TEXT_PROPERTY = []; +    if (msie < 9) { +      NODE_TYPE_TEXT_PROPERTY[1] = 'innerText';    /** Element **/ +      NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue';    /** Text **/ +    } else { +      NODE_TYPE_TEXT_PROPERTY[1] =                 /** Element **/ +      NODE_TYPE_TEXT_PROPERTY[3] = 'textContent';  /** Text **/ +    } +    getText.$dv = ''; +    return getText; + +    function getText(element, value) { +      var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType] +      if (isUndefined(value)) { +        return textProp ? element[textProp] : '';        } -      : function(element, value) { -        if (isUndefined(value)) { -          return element.textContent; -        } -        element.textContent = value; -      }, {$dv:''}), +      element[textProp] = value; +    } +  })(),    val: function(element, value) {      if (isUndefined(value)) { @@ -518,8 +521,14 @@ forEach({          return this;        } else {          // we are a read, so read the first child. -        if (this.length) -          return fn(this[0], arg1, arg2); +        var value = fn.$dv; +        // Only if we have $dv do we iterate over all, otherwise it is just the first element. +        var jj = value == undefined ? Math.min(this.length, 1) : this.length; +        for (var j = 0; j < jj; j++) { +          var nodeValue = fn(this[j], arg1, arg2); +          value = value ? value + nodeValue : nodeValue; +        } +        return value;        }      } else {        // we are a write, so apply to all children @@ -529,7 +538,6 @@ forEach({        // return self for chaining        return this;      } -    return fn.$dv;    };  }); diff --git a/src/ng/animator.js b/src/ng/animator.js index 2965717b..2b399813 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -395,11 +395,16 @@ var $AnimatorProvider = function() {          }          function insert(element, parent, after) { -          if (after) { -            after.after(element); -          } else { -            parent.append(element); -          } +          var afterNode = after && after[after.length - 1]; +          var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; +          var afterNextSibling = afterNode && afterNode.nextSibling; +          forEach(element, function(node) { +            if (afterNextSibling) { +              parentNode.insertBefore(node, afterNextSibling); +            } else { +              parentNode.appendChild(node); +            } +          });          }          function remove(element) { diff --git a/src/ng/compile.js b/src/ng/compile.js index be22482b..2dddf82d 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -358,11 +358,12 @@ function $CompileProvider($provide) {          // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.          $compileNodes = jqLite($compileNodes);        } +      var tempParent = document.createDocumentFragment();        // We can not compile top level text elements since text nodes can be merged and we will        // not be able to attach scope data to them, so we will wrap them in <span>        forEach($compileNodes, function(node, index){          if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { -          $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0]; +          $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];          }        });        var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority); @@ -420,7 +421,7 @@ function $CompileProvider($provide) {          attrs = new Attributes();          // we must always refer to nodeList[i] since the nodes can be replaced underneath us. -        directives = collectDirectives(nodeList[i], [], attrs, maxPriority); +        directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined);          nodeLinkFn = (directives.length)              ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) @@ -509,6 +510,10 @@ function $CompileProvider($provide) {            // iterate over the attributes            for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,                     j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { +            var attrStartName; +            var attrEndName; +            var index; +              attr = nAttrs[j];              if (attr.specified) {                name = attr.name; @@ -517,6 +522,11 @@ function $CompileProvider($provide) {                if (NG_ATTR_BINDING.test(ngAttrName)) {                  name = ngAttrName.substr(6).toLowerCase();                } +              if ((index = ngAttrName.lastIndexOf('Start')) != -1 && index == ngAttrName.length - 5) { +                attrStartName = name; +                attrEndName = name.substr(0, name.length - 5) + 'end'; +                name = name.substr(0, name.length - 6); +              }                nName = directiveNormalize(name.toLowerCase());                attrsMap[nName] = name;                attrs[nName] = value = trim((msie && name == 'href') @@ -526,7 +536,7 @@ function $CompileProvider($provide) {                  attrs[nName] = true; // presence means true                }                addAttrInterpolateDirective(node, directives, value, nName); -              addDirective(directives, nName, 'A', maxPriority); +              addDirective(directives, nName, 'A', maxPriority, attrStartName, attrEndName);              }            } @@ -565,6 +575,47 @@ function $CompileProvider($provide) {        return directives;      } +    /** +     * Given a node with an directive-start it collects all of the siblings until it find directive-end. +     * @param node +     * @param attrStart +     * @param attrEnd +     * @returns {*} +     */ +    function groupScan(node, attrStart, attrEnd) { +      var nodes = []; +      var depth = 0; +      if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { +        var startNode = node; +        do { +          if (!node) { +            throw ngError(51, "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); +          } +          if (node.hasAttribute(attrStart)) depth++; +          if (node.hasAttribute(attrEnd)) depth--; +          nodes.push(node); +          node = node.nextSibling; +        } while (depth > 0); +      } else { +        nodes.push(node); +      } +      return jqLite(nodes); +    } + +    /** +     * Wrapper for linking function which converts normal linking function into a grouped +     * linking function. +     * @param linkFn +     * @param attrStart +     * @param attrEnd +     * @returns {Function} +     */ +    function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { +      return function(scope, element, attrs, controllers) { +        element = groupScan(element[0], attrStart, attrEnd); +        return linkFn(scope, element, attrs, controllers); +      } +    }      /**       * Once the directives have been collected, their compile functions are executed. This method @@ -601,6 +652,13 @@ function $CompileProvider($provide) {        // executes all directives on the current element        for(var i = 0, ii = directives.length; i < ii; i++) {          directive = directives[i]; +        var attrStart = directive.$$start; +        var attrEnd = directive.$$end; + +        // collect multiblock sections +        if (attrStart) { +          $compileNode = groupScan(compileNode, attrStart, attrEnd) +        }          $template = undefined;          if (terminalPriority > directive.priority) { @@ -631,11 +689,11 @@ function $CompileProvider($provide) {            transcludeDirective = directive;            terminalPriority = directive.priority;            if (directiveValue == 'element') { -            $template = jqLite(compileNode); +            $template = groupScan(compileNode, attrStart, attrEnd)              $compileNode = templateAttrs.$$element =                  jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));              compileNode = $compileNode[0]; -            replaceWith(jqCollection, jqLite($template[0]), compileNode); +            replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode);              childTranscludeFn = compile($template, transcludeFn, terminalPriority);            } else {              $template = jqLite(JQLiteClone(compileNode)).contents(); @@ -699,9 +757,9 @@ function $CompileProvider($provide) {            try {              linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);              if (isFunction(linkFn)) { -              addLinkFns(null, linkFn); +              addLinkFns(null, linkFn, attrStart, attrEnd);              } else if (linkFn) { -              addLinkFns(linkFn.pre, linkFn.post); +              addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);              }            } catch (e) {              $exceptionHandler(e, startingTag($compileNode)); @@ -723,12 +781,14 @@ function $CompileProvider($provide) {        //////////////////// -      function addLinkFns(pre, post) { +      function addLinkFns(pre, post, attrStart, attrEnd) {          if (pre) { +          if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);            pre.require = directive.require;            preLinkFns.push(pre);          }          if (post) { +          if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);            post.require = directive.require;            postLinkFns.push(post);          } @@ -907,8 +967,8 @@ function $CompileProvider($provide) {       *   * `M`: comment       * @returns true if directive was added.       */ -    function addDirective(tDirectives, name, location, maxPriority) { -      var match = false; +    function addDirective(tDirectives, name, location, maxPriority, startAttrName, endAttrName) { +      var match = null;        if (hasDirectives.hasOwnProperty(name)) {          for(var directive, directives = $injector.get(name + Suffix),              i = 0, ii = directives.length; i<ii; i++) { @@ -916,8 +976,11 @@ function $CompileProvider($provide) {              directive = directives[i];              if ( (maxPriority === undefined || maxPriority > directive.priority) &&                   directive.restrict.indexOf(location) != -1) { +              if (startAttrName) { +                directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); +              }                tDirectives.push(directive); -              match = true; +              match = directive;              }            } catch(e) { $exceptionHandler(e); }          } @@ -1120,30 +1183,50 @@ function $CompileProvider($provide) {       *       * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes       *    in the root of the tree. -     * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell, +     * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell,       *    but replace its DOM node reference.       * @param {Node} newNode The new DOM node.       */ -    function replaceWith($rootElement, $element, newNode) { -      var oldNode = $element[0], -          parent = oldNode.parentNode, +    function replaceWith($rootElement, elementsToRemove, newNode) { +      var firstElementToRemove = elementsToRemove[0], +          removeCount = elementsToRemove.length, +          parent = firstElementToRemove.parentNode,            i, ii;        if ($rootElement) {          for(i = 0, ii = $rootElement.length; i < ii; i++) { -          if ($rootElement[i] == oldNode) { -            $rootElement[i] = newNode; +          if ($rootElement[i] == firstElementToRemove) { +            $rootElement[i++] = newNode; +            for (var j = i, j2 = j + removeCount - 1, +                     jj = $rootElement.length; +                 j < jj; j++, j2++) { +              if (j2 < jj) { +                $rootElement[j] = $rootElement[j2]; +              } else { +                delete $rootElement[j]; +              } +            } +            $rootElement.length -= removeCount - 1;              break;            }          }        }        if (parent) { -        parent.replaceChild(newNode, oldNode); +        parent.replaceChild(newNode, firstElementToRemove); +      } +      var fragment = document.createDocumentFragment(); +      fragment.appendChild(firstElementToRemove); +      newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; +      for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { +        var element = elementsToRemove[k]; +        jqLite(element).remove(); // must do this way to clean up expando +        fragment.appendChild(element); +        delete elementsToRemove[k];        } -      newNode[jqLite.expando] = oldNode[jqLite.expando]; -      $element[0] = newNode; +      elementsToRemove[0] = newNode; +      elementsToRemove.length = 1      }    }];  } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 34d32f59..6c2da071 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -258,7 +258,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {              if (lastBlockMap.hasOwnProperty(key)) {                block = lastBlockMap[key];                animate.leave(block.element); -              block.element[0][NG_REMOVED] = true; +              forEach(block.element, function(element) { element[NG_REMOVED] = true});                block.scope.$destroy();              }            } diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 1ebe6ad4..70c18d35 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -56,6 +56,9 @@ describe('jqLite', function() {      it('should allow construction with html', function() {        var nodes = jqLite('<div>1</div><span>2</span>'); +      expect(nodes[0].parentNode).toBeDefined(); +      expect(nodes[0].parentNode.nodeType).toBe(11); /** Document Fragment **/; +      expect(nodes[0].parentNode).toBe(nodes[1].parentNode);        expect(nodes.length).toEqual(2);        expect(nodes[0].innerHTML).toEqual('1');        expect(nodes[1].innerHTML).toEqual('2'); @@ -644,12 +647,13 @@ describe('jqLite', function() {      it('should read/write value', function() { -      var element = jqLite('<div>abc</div>'); -      expect(element.length).toEqual(1); -      expect(element[0].innerHTML).toEqual('abc'); +      var element = jqLite('<div>ab</div><span>c</span>'); +      expect(element.length).toEqual(2); +      expect(element[0].innerHTML).toEqual('ab'); +      expect(element[1].innerHTML).toEqual('c');        expect(element.text()).toEqual('abc');        expect(element.text('xyz') == element).toBeTruthy(); -      expect(element.text()).toEqual('xyz'); +      expect(element.text()).toEqual('xyzxyz');      });    }); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index bf3d0b77..95b2ab72 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2718,4 +2718,129 @@ describe('$compile', function() {        expect(element.attr('test4')).toBe('Misko');      }));    }); + + +  describe('multi-element directive', function() { +    it('should group on link function', inject(function($compile, $rootScope) { +      $rootScope.show = false; +      element = $compile( +          '<div>' + +              '<span ng-show-start="show"></span>' + +              '<span ng-show-end></span>' + +          '</div>')($rootScope); +      $rootScope.$digest(); +      var spans = element.find('span'); +      expect(spans.eq(0).css('display')).toBe('none'); +      expect(spans.eq(1).css('display')).toBe('none'); +    })); + + +    it('should group on compile function', inject(function($compile, $rootScope) { +      $rootScope.show = false; +      element = $compile( +          '<div>' + +              '<span ng-repeat-start="i in [1,2]">{{i}}A</span>' + +              '<span ng-repeat-end>{{i}}B;</span>' + +          '</div>')($rootScope); +      $rootScope.$digest(); +      expect(element.text()).toEqual('1A1B;2A2B;'); +    })); + + +    it('should group on $root compile function', inject(function($compile, $rootScope) { +      $rootScope.show = false; +      element = $compile( +          '<div></div>' + +              '<span ng-repeat-start="i in [1,2]">{{i}}A</span>' + +              '<span ng-repeat-end>{{i}}B;</span>' + +          '<div></div>')($rootScope); +      $rootScope.$digest(); +      element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. +      expect(element.text()).toEqual('1A1B;2A2B;'); +    })); + + +    it('should group on nested groups', inject(function($compile, $rootScope) { +      $rootScope.show = false; +      element = $compile( +          '<div></div>' + +              '<div ng-repeat-start="i in [1,2]">{{i}}A</div>' + +              '<span ng-bind-start="\'.\'"></span>' + +              '<span ng-bind-end></span>' + +              '<div ng-repeat-end>{{i}}B;</div>' + +          '<div></div>')($rootScope); +      $rootScope.$digest(); +      element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. +      expect(element.text()).toEqual('1A..1B;2A..2B;'); +    })); + + +    it('should group on nested groups', inject(function($compile, $rootScope) { +      $rootScope.show = false; +      element = $compile( +          '<div></div>' + +              '<div ng-repeat-start="i in [1,2]">{{i}}(</div>' + +              '<span ng-repeat-start="j in [2,3]">{{j}}-</span>' + +              '<span ng-repeat-end>{{j}}</span>' + +              '<div ng-repeat-end>){{i}};</div>' + +          '<div></div>')($rootScope); +      $rootScope.$digest(); +      element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. +      expect(element.text()).toEqual('1(2-23-3)1;2(2-23-3)2;'); +    })); + + +    it('should throw error if unterminated', function () { +      module(function($compileProvider) { +        $compileProvider.directive('foo', function() { +          return { +          }; +        }); +      }); +      inject(function($compile, $rootScope) { +        expect(function() { +          element = $compile( +              '<div>' + +                '<span foo-start></span>' + +              '</div>'); +        }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found."); +      }); +    }); + + +    it('should throw error if unterminated', function () { +      module(function($compileProvider) { +        $compileProvider.directive('foo', function() { +          return { +          }; +        }); +      }); +      inject(function($compile, $rootScope) { +        expect(function() { +          element = $compile( +              '<div>' + +                  '<span foo-start><span foo-end></span></span>' + +              '</div>'); +        }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found."); +      }); +    }); + + +    it('should support data- and x- prefix', inject(function($compile, $rootScope) { +      $rootScope.show = false; +      element = $compile( +          '<div>' + +              '<span data-ng-show-start="show"></span>' + +              '<span data-ng-show-end></span>' + +              '<span x-ng-show-start="show"></span>' + +              '<span x-ng-show-end></span>' + +          '</div>')($rootScope); +      $rootScope.$digest(); +      var spans = element.find('span'); +      expect(spans.eq(0).css('display')).toBe('none'); +      expect(spans.eq(1).css('display')).toBe('none'); +      expect(spans.eq(2).css('display')).toBe('none'); +      expect(spans.eq(3).css('display')).toBe('none'); +    })); +  });  }); | 
