From fca21fe6b3293ae17534b48fa3838a4a4cc99bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Sat, 27 Apr 2013 17:37:30 -0300 Subject: [PATCH 1/7] feat(compile): Add transclude type "multi-element" feat(ng-repeat): Make ng-repeat of transclude type multi-element Add a new transclude type multi-element to be able to handle transclusion that includes more elements than the one where the directive is present Make the changes to ngRepeat so it is of transclude type multi-element Closes #1891 --- src/ng/compile.js | 63 ++ src/ng/directive/ngRepeat.js | 10 +- test/ng/directive/ngRepeatStartSpec.js | 784 +++++++++++++++++++++++++ 3 files changed, 852 insertions(+), 5 deletions(-) create mode 100644 test/ng/directive/ngRepeatStartSpec.js diff --git a/src/ng/compile.js b/src/ng/compile.js index ac8d13ceb8a5..9fca432e97d0 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -645,6 +645,14 @@ function $CompileProvider($provide) { compileNode = $compileNode[0]; replaceWith($rootElement, jqLite($template[0]), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority); + } else if (directiveValue == 'multi-element') { + childTranscludeFn = compile(jqLite(extractMultiElementTransclude(compileNode, directiveName)), + transcludeFn, terminalPriority); + $template = jqLite(compileNode); + $compileNode = templateAttrs.$$element = + jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); + compileNode = $compileNode[0]; + replaceWith($rootElement, $template, compileNode); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); $compileNode.html(''); // clear contents @@ -731,6 +739,61 @@ function $CompileProvider($provide) { //////////////////// + + function extractMultiElementTransclude(cursor, directiveName) { + var transcludeContent = [], + c, count = 0, + transcludeStart = directiveName + 'Start', + transcludeEnd = directiveName + 'End'; + + do { + if (containsAttr(cursor, transcludeStart)) count++; + if (containsAttr(cursor, transcludeEnd)) count--; + transcludeContent.push(cursor); + cursor = cursor.nextSibling; + } while(count > 0 && cursor); + if (count > 0) throw Error('Unmatched ' + transcludeStart + '.'); + if (count < 0) throw Error('Unexpected ' + transcludeEnd + '.'); + for (var j = 0; j < transcludeContent.length; ++j) { + c = transcludeContent[j]; + transcludeContent[j] = JQLiteClone(transcludeContent[j]); + // The first element will be replaced by a comment + if (j != 0) jqLite(c).remove(); + } + return transcludeContent; + } + + + function containsAttr(node, attributeName) { + var nodeType = node.nodeType, + result = false; + + switch(nodeType) { + case 1: + // iterate over the attributes + for (var attr, name, nName, ngAttrName, nAttrs = node.attributes, + j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + attr = nAttrs[j]; + if (attr.specified) { + name = attr.name; + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (NG_ATTR_BINDING.test(ngAttrName)) { + name = ngAttrName.substr(6).toLowerCase(); + } + nName = directiveNormalize(name.toLowerCase()); + if (nName == attributeName) { + result = true; + break; + } + } + } + break; + } + return result; + } + + function addLinkFns(pre, post) { if (pre) { pre.require = directive.require; diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index a7d558957d2d..050fa8767e6d 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -145,7 +145,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { var NG_REMOVED = '$$NG_REMOVED'; return { - transclude: 'element', + transclude: 'multi-element', priority: 1000, terminal: true, compile: function(element, attr, linker) { @@ -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(leavingElement) { leavingElement[NG_REMOVED] = true; }); block.scope.$destroy(); } } @@ -274,7 +274,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { // associated scope/element childScope = block.scope; - nextCursor = cursor[0]; + nextCursor = cursor[cursor.length - 1]; do { nextCursor = nextCursor.nextSibling; } while(nextCursor && nextCursor[NG_REMOVED]); @@ -284,7 +284,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { cursor = block.element; } else { // existing item which got moved - animate.move(block.element, null, cursor); + animate.move(block.element, null, cursor.eq(-1)); cursor = block.element; } } else { @@ -301,7 +301,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (!block.element) { linker(childScope, function(clone) { - animate.enter(clone, null, cursor); + animate.enter(clone, null, cursor.eq(-1)); cursor = clone; block.scope = childScope; block.element = clone; diff --git a/test/ng/directive/ngRepeatStartSpec.js b/test/ng/directive/ngRepeatStartSpec.js new file mode 100644 index 000000000000..d42a4ed60b0c --- /dev/null +++ b/test/ng/directive/ngRepeatStartSpec.js @@ -0,0 +1,784 @@ +'use strict'; + +describe('ngRepeatStart', function() { + var element, $compile, scope, $exceptionHandler; + + + beforeEach(module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + })); + + beforeEach(inject(function(_$compile_, $rootScope, _$exceptionHandler_) { + $compile = _$compile_; + $exceptionHandler = _$exceptionHandler_; + scope = $rootScope.$new(); + })); + + + afterEach(function() { + if ($exceptionHandler.errors.length) { + dump(jasmine.getEnv().currentSpec.getFullName()); + dump('$exceptionHandler has errors'); + dump($exceptionHandler.errors); + expect($exceptionHandler.errors).toBe([]); + } + dealoc(element); + }); + + + it('should iterate over an array of objects', function() { + element = $compile( + '')(scope); + + Array.prototype.extraProperty = "should be ignored"; + // INIT + scope.items = [{name: 'misko', number: 1, color: 'red'}, {name:'shyam', number: 2, color: 'blue'}]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('misko;1;red;shyam;2;blue;'); + delete Array.prototype.extraProperty; + + // GROW + scope.items.push({name: 'adam', number: 3, color: 'green'}); + scope.$digest(); + expect(element.find('li').length).toEqual(9); + expect(element.text()).toEqual('misko;1;red;shyam;2;blue;adam;3;green;'); + + // SHRINK + scope.items.pop(); + scope.items.shift(); + scope.$digest(); + expect(element.find('li').length).toEqual(3); + expect(element.text()).toEqual('shyam;2;blue;'); + }); + + + it('should iterate over on object/map', function() { + element = $compile( + '')(scope); + scope.items = {misko:'swe', shyam:'set'}; + scope.$digest(); + expect(element.text()).toEqual('misko:swe|shyam:set|'); + }); + + + describe('track by', function() { + it('should track using expression function', function() { + element = $compile( + '')(scope); + scope.items = [{id: 'misko'}, {id: 'igor'}]; + scope.$digest(); + var li0 = element.find('li')[0]; + var li1 = element.find('li')[1]; + var li2 = element.find('li')[2]; + var li3 = element.find('li')[3]; + + scope.items.push(scope.items.shift()); + scope.$digest(); + expect(element.find('li')[0]).toBe(li2); + expect(element.find('li')[1]).toBe(li3); + expect(element.find('li')[2]).toBe(li0); + expect(element.find('li')[3]).toBe(li1); + }); + + + it('should track using build in $id function', function() { + element = $compile( + '')(scope); + scope.items = [{name: 'misko'}, {name: 'igor'}]; + scope.$digest(); + var li0 = element.find('li')[0]; + var li1 = element.find('li')[1]; + var li2 = element.find('li')[2]; + var li3 = element.find('li')[3]; + + scope.items.push(scope.items.shift()); + scope.$digest(); + expect(element.find('li')[0]).toBe(li2); + expect(element.find('li')[1]).toBe(li3); + expect(element.find('li')[2]).toBe(li0); + expect(element.find('li')[3]).toBe(li1); + }); + + + it('should iterate over an array of primitives', function() { + element = $compile( + '')(scope); + + Array.prototype.extraProperty = "should be ignored"; + // INIT + scope.items = [true, true, true]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('true;true;true;true;true;true;'); + delete Array.prototype.extraProperty; + + scope.items = [false, true, true]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('false;false;true;true;true;true;'); + + scope.items = [false, true, false]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('false;false;true;true;false;false;'); + + scope.items = [true]; + scope.$digest(); + expect(element.find('li').length).toEqual(2); + expect(element.text()).toEqual('true;true;'); + + scope.items = [true, true, false]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('true;true;true;true;false;false;'); + + scope.items = [true, false, false]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('true;true;false;false;false;false;'); + + // string + scope.items = ['a', 'a', 'a']; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('a;a;a;a;a;a;'); + + scope.items = ['ab', 'a', 'a']; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('ab;ab;a;a;a;a;'); + + scope.items = ['test']; + scope.$digest(); + expect(element.find('li').length).toEqual(2); + expect(element.text()).toEqual('test;test;'); + + scope.items = ['same', 'value']; + scope.$digest(); + expect(element.find('li').length).toEqual(4); + expect(element.text()).toEqual('same;same;value;value;'); + + // number + scope.items = [12, 12, 12]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('12;12;12;12;12;12;'); + + scope.items = [53, 12, 27]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('53;53;12;12;27;27;'); + + scope.items = [89]; + scope.$digest(); + expect(element.find('li').length).toEqual(2); + expect(element.text()).toEqual('89;89;'); + + scope.items = [89, 42]; + scope.$digest(); + expect(element.find('li').length).toEqual(4); + expect(element.text()).toEqual('89;89;42;42;'); + }); + + + it('should iterate over object with changing primitive property values', function() { + // test for issue #933 + + element = $compile( + '')(scope); + + scope.items = {misko: true, shyam: true, zhenbo:true}; + scope.$digest(); + expect(element.find('li').length).toEqual(9); + expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;'); + + browserTrigger(element.find('input').eq(0), 'click'); + + expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;'); + expect(element.find('input')[0].checked).toBe(false); + expect(element.find('input')[1].checked).toBe(true); + expect(element.find('input')[2].checked).toBe(true); + + browserTrigger(element.find('input').eq(0), 'click'); + expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;'); + expect(element.find('input')[0].checked).toBe(true); + expect(element.find('input')[1].checked).toBe(true); + expect(element.find('input')[2].checked).toBe(true); + + browserTrigger(element.find('input').eq(1), 'click'); + expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;'); + expect(element.find('input')[0].checked).toBe(true); + expect(element.find('input')[1].checked).toBe(false); + expect(element.find('input')[2].checked).toBe(true); + + scope.items = {misko: false, shyam: true, zhenbo: true}; + scope.$digest(); + expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;'); + expect(element.find('input')[0].checked).toBe(false); + expect(element.find('input')[1].checked).toBe(true); + expect(element.find('input')[2].checked).toBe(true); + }); + }); + + + it('should not ngRepeatStart over parent properties', function() { + var Class = function() {}; + Class.prototype.abc = function() {}; + Class.prototype.value = 'abc'; + + element = $compile( + '')(scope); + scope.items = new Class(); + scope.items.name = 'value'; + scope.$digest(); + expect(element.text()).toEqual('name:value;'); + }); + + + it('should error on wrong parsing of ngRepeatStart', function() { + element = jqLite(''); + $compile(element)(scope); + expect($exceptionHandler.errors.shift()[0].message). + toBe("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); + }); + + + it("should throw error when left-hand-side of ngRepeat can't be parsed", function() { + element = jqLite(''); + $compile(element)(scope); + expect($exceptionHandler.errors.shift()[0].message). + toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'."); + }); + + + it('should error on unexpected ngRepeatEnd', function() { + element = jqLite(''); + try { + $compile(element)(scope); + fail(); + } catch(e) { + expect(e.message).toBe('Unexpected ngRepeatEnd.'); + } + }); + + + it('should error on unmatched ngRepeatStart', function() { + element = jqLite(''); + try { + $compile(element)(scope); + fail(); + } catch(e) { + expect(e.message).toBe('Unmatched ngRepeatStart.'); + } + }); + + + it('should expose iterator offset as $index when iterating over arrays', + function() { + element = $compile( + '')(scope); + scope.items = ['misko', 'shyam', 'frodo']; + scope.$digest(); + expect(element.text()).toEqual('misko:0|shyam:1|frodo:2|'); + }); + + + it('should expose iterator offset as $index when iterating over objects', function() { + element = $compile( + '')(scope); + scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'}; + scope.$digest(); + expect(element.text()).toEqual('frodo:f:0|misko:m:1|shyam:s:2|'); + }); + + + it('should expose iterator position as $first, $middle and $last when iterating over arrays', + function() { + element = $compile( + '')(scope); + scope.items = ['misko', 'shyam', 'doug']; + scope.$digest(); + expect(element.text()). + toEqual('misko:true-false-false|shyam:false-true-false|doug:false-false-true|'); + + scope.items.push('frodo'); + scope.$digest(); + expect(element.text()). + toEqual('misko:true-false-false|' + + 'shyam:false-true-false|' + + 'doug:false-true-false|' + + 'frodo:false-false-true|'); + + scope.items.pop(); + scope.items.pop(); + scope.$digest(); + expect(element.text()).toEqual('misko:true-false-false|shyam:false-false-true|'); + + scope.items.pop(); + scope.$digest(); + expect(element.text()).toEqual('misko:true-false-true|'); + }); + + + it('should expose iterator position as $first, $middle and $last when iterating over objects', + function() { + element = $compile( + '')(scope); + scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'}; + scope.$digest(); + expect(element.text()). + toEqual('doug:d:true-false-false|' + + 'frodo:f:false-true-false|' + + 'misko:m:false-true-false|' + + 'shyam:s:false-false-true|'); + + delete scope.items.doug; + delete scope.items.frodo; + scope.$digest(); + expect(element.text()).toEqual('misko:m:true-false-false|shyam:s:false-false-true|'); + + delete scope.items.shyam; + scope.$digest(); + expect(element.text()).toEqual('misko:m:true-false-true|'); + }); + + + it('should calculate $first, $middle and $last when we filter out properties from an obj', function() { + element = $compile( + '')(scope); + scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f', '$toBeFilteredOut': 'xxxx'}; + scope.$digest(); + expect(element.text()). + toEqual('doug:d:true-false-false|' + + 'frodo:f:false-true-false|' + + 'misko:m:false-true-false|' + + 'shyam:s:false-false-true|'); + }); + + + it('should ignore $ and $$ properties', function() { + element = $compile('')(scope); + scope.items = ['a', 'b', 'c']; + scope.items.$$hashKey = 'xxx'; + scope.items.$root = 'yyy'; + scope.$digest(); + + expect(element.text()).toEqual('a|ab|bc|c'); + }); + + + it('should repeat over nested arrays', function() { + element = $compile( + '')(scope); + scope.groups = [['a', 'b'], ['c','d']]; + scope.$digest(); + + expect(element.text()).toEqual('a|ab|bXa|ab|bYc|cd|dXc|cd|dY'); + }); + + + it('should ignore non-array element properties when iterating over an array', function() { + element = $compile('')(scope); + scope.array = ['a', 'b', 'c']; + scope.array.foo = '23'; + scope.array.bar = function() {}; + scope.$digest(); + + expect(element.text()).toBe('a|b|c|'); + }); + + + it('should iterate over non-existent elements of a sparse array', function() { + element = $compile('')(scope); + scope.array = ['a', 'b']; + scope.array[4] = 'c'; + scope.array[6] = 'd'; + scope.$digest(); + + expect(element.text()).toBe('a|b|||c||d|'); + }); + + + it('should iterate over all kinds of types', function() { + element = $compile('')(scope); + scope.array = ['a', 1, null, undefined, {}]; + scope.$digest(); + + expect(element.text()).toMatch(/a\|1\|\|\|\{\s*\}\|/); + }); + + + it('should preserve data on move of elements', function() { + element = $compile('')(scope); + scope.array = ['a', 'b']; + scope.$digest(); + + var lis = element.find('li'); + lis.eq(0).data('mark', 'a'); + lis.eq(1).data('mark', 'b'); + + scope.array = ['b', 'a']; + scope.$digest(); + + var lis = element.find('li'); + expect(lis.eq(0).data('mark')).toEqual('b'); + expect(lis.eq(1).data('mark')).toEqual('a'); + }); + + + describe('stability', function() { + var a, b, c, d, lis; + + beforeEach(function() { + element = $compile( + '')(scope); + a = {}; + b = {}; + c = {}; + d = {}; + + scope.items = [a, b, c]; + scope.$digest(); + lis = element.find('li'); + }); + + + it('should preserve the order of elements', function() { + scope.items = [a, c, d]; + scope.$digest(); + var newElements = element.find('li'); + expect(newElements[0]).toEqual(lis[0]); + expect(newElements[3]).toEqual(lis[6]); + expect(newElements[6]).not.toEqual(lis[2]); + }); + + + it('should throw error on adding existing duplicates and recover', function() { + scope.items = [a, a, a]; + scope.$digest(); + expect($exceptionHandler.errors.shift().message). + toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:003'); + + // recover + scope.items = [a]; + scope.$digest(); + var newElements = element.find('li'); + expect(newElements.length).toEqual(3); + expect(newElements[0]).toEqual(lis[0]); + + scope.items = []; + scope.$digest(); + var newElements = element.find('li'); + expect(newElements.length).toEqual(0); + }); + + + it('should throw error on new duplicates and recover', function() { + scope.items = [d, d, d]; + scope.$digest(); + expect($exceptionHandler.errors.shift().message). + toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:009'); + + // recover + scope.items = [a]; + scope.$digest(); + var newElements = element.find('li'); + expect(newElements.length).toEqual(3); + expect(newElements[0]).toEqual(lis[0]); + + scope.items = []; + scope.$digest(); + var newElements = element.find('li'); + expect(newElements.length).toEqual(0); + }); + + + it('should reverse items when the collection is reversed', function() { + scope.items = [a, b, c]; + scope.$digest(); + lis = element.find('li'); + + scope.items = [c, b, a]; + scope.$digest(); + var newElements = element.find('li'); + expect(newElements.length).toEqual(9); + expect(newElements[0]).toEqual(lis[6]); + expect(newElements[3]).toEqual(lis[3]); + expect(newElements[6]).toEqual(lis[0]); + }); + + + it('should reuse elements even when model is composed of primitives', function() { + // rebuilding repeater from scratch can be expensive, we should try to avoid it even for + // model that is composed of primitives. + + scope.items = ['hello', 'cau', 'ahoj']; + scope.$digest(); + lis = element.find('li'); + lis[2].id = 'yes'; + + scope.items = ['ahoj', 'hello', 'cau']; + scope.$digest(); + var newLis = element.find('li'); + expect(newLis.length).toEqual(9); + expect(newLis[0]).toEqual(lis[6]); + expect(newLis[3]).toEqual(lis[0]); + expect(newLis[6]).toEqual(lis[3]); + }); + }); +}); + +describe('ngRepeatStart ngAnimate', function() { + var vendorPrefix, window; + var body, element; + + function html(html) { + body.html(html); + element = body.children().eq(0); + return element; + } + + beforeEach(function() { + // we need to run animation on attached elements; + body = jqLite(document.body); + }); + + afterEach(function(){ + dealoc(body); + dealoc(element); + }); + + beforeEach(module(function($animationProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + return function($sniffer, $animator) { + vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $animator.enabled(true); + }; + })); + + it('should fire off the enter animation + add and remove the css classes', + inject(function($compile, $rootScope, $sniffer) { + + element = $compile(html( + '
' + + '
{{ item }}
' + + '
{{item}}
' + + '
' + ))($rootScope); + + $rootScope.$digest(); // re-enable the animations; + + $rootScope.items = ['1','2','3']; + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var cssProp = vendorPrefix + 'transition'; + var cssValue = '1s linear all'; + var kids = element.children(); + for(var i=0;i' + + '
{{ item }}
' + + '
{{item}}
' + + '' + ))($rootScope); + + $rootScope.items = ['1','2','3']; + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var cssProp = vendorPrefix + 'transition'; + var cssValue = '1s linear all'; + var kids = element.children(); + for(var i=0;i' + + '
{{ item }}
' + + '
{{ item }}
' + + '' + ))($rootScope); + + $rootScope.items = ['1','2','3']; + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; + var cssValue = '1s linear all'; + var kids = element.children(); + for(var i=0;i' + + '
{{ item }}
' + + '
{{ item }}
' + + '' + ))($rootScope); + + $rootScope.$digest(); // re-enable the animations; + + $rootScope.items = ['a','b']; + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var kids = element.children(); + var first = jqLite(kids[0]); + var second = jqLite(kids[2]); + var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; + var cssValue = '0.5s linear all'; + first.css(cssProp, cssValue); + second.css(cssProp, cssValue); + + if ($sniffer.supportsTransitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(1).process(); + window.setTimeout.expect(500).process(); + window.setTimeout.expect(500).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + })); + +}); From 477ae0c6fb5edbbdf79af8d7683318c492bf444e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Sat, 27 Apr 2013 17:44:57 -0300 Subject: [PATCH 2/7] style(compile): Remove a tab Remove a tab from the code --- src/ng/compile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 9fca432e97d0..ae92b07bb7fa 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -645,7 +645,7 @@ function $CompileProvider($provide) { compileNode = $compileNode[0]; replaceWith($rootElement, jqLite($template[0]), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority); - } else if (directiveValue == 'multi-element') { + } else if (directiveValue == 'multi-element') { childTranscludeFn = compile(jqLite(extractMultiElementTransclude(compileNode, directiveName)), transcludeFn, terminalPriority); $template = jqLite(compileNode); From 68611422ee3ac99e49259ace4e9b0e1c725cda4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Sat, 27 Apr 2013 23:47:58 -0300 Subject: [PATCH 3/7] fix(compile): Fix for nested multi-element directives Fix an issue with nested multi-element directives that are siblings at the DOM level --- src/ng/compile.js | 33 ++++++++++++++++---------- test/ng/directive/ngRepeatStartSpec.js | 16 +++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index ae92b07bb7fa..ce7284996ab8 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -357,7 +357,7 @@ function $CompileProvider($provide) { //================================ - function compile($compileNodes, transcludeFn, maxPriority) { + function compile($compileNodes, transcludeFn, maxPriority, limitMaxPriorityToFirstElement) { if (!($compileNodes instanceof jqLite)) { // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it. $compileNodes = jqLite($compileNodes); @@ -369,7 +369,8 @@ function $CompileProvider($provide) { $compileNodes[index] = jqLite(node).wrap('').parent()[0]; } }); - var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority); + var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, + limitMaxPriorityToFirstElement); return function publicLinkFn(scope, cloneConnectFn){ assertArg(scope, 'scope'); // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart @@ -418,9 +419,12 @@ function $CompileProvider($provide) { * rootElement must be set the jqLite collection of the compile root. This is * needed so that the jqLite collection items can be replaced with widgets. * @param {number=} max directive priority + * @param {boolean=} if the max priority should only apply to the first element in the list. + * A true value here will make the maxPriority only apply to the first element on the + * list while the other elements on the list will not have a maxPriority set * @returns {?function} A composite linking function of all of the matched directives or null. */ - function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority) { + function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, limitMaxPriorityToFirstElement) { var linkFns = [], nodeLinkFn, childLinkFn, directives, attrs, linkFnFound; @@ -428,7 +432,8 @@ 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, + (limitMaxPriorityToFirstElement && i != 0) ? undefined : maxPriority); nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) @@ -646,13 +651,23 @@ function $CompileProvider($provide) { replaceWith($rootElement, jqLite($template[0]), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority); } else if (directiveValue == 'multi-element') { - childTranscludeFn = compile(jqLite(extractMultiElementTransclude(compileNode, directiveName)), - transcludeFn, terminalPriority); + // We need to compile a clone of the elements, but at the same time we have to be sure that these elements are siblings + var nestedContent = extractMultiElementTransclude(compileNode, directiveName); + var cloneContent = jqLite('
'); + forEach(nestedContent, function(nestedElement) { + cloneContent.append(JQLiteClone(nestedElement)); + }); + childTranscludeFn = compile(cloneContent.contents(), transcludeFn, terminalPriority, true); $template = jqLite(compileNode); $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; replaceWith($rootElement, $template, compileNode); + forEach(nestedContent.splice(1), + function(toRemove) { + replaceWith($rootElement, jqLite(toRemove), document.createComment(' placeholder ')); + } + ); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); $compileNode.html(''); // clear contents @@ -754,12 +769,6 @@ function $CompileProvider($provide) { } while(count > 0 && cursor); if (count > 0) throw Error('Unmatched ' + transcludeStart + '.'); if (count < 0) throw Error('Unexpected ' + transcludeEnd + '.'); - for (var j = 0; j < transcludeContent.length; ++j) { - c = transcludeContent[j]; - transcludeContent[j] = JQLiteClone(transcludeContent[j]); - // The first element will be replaced by a comment - if (j != 0) jqLite(c).remove(); - } return transcludeContent; } diff --git a/test/ng/directive/ngRepeatStartSpec.js b/test/ng/directive/ngRepeatStartSpec.js index d42a4ed60b0c..0bb149c501ac 100644 --- a/test/ng/directive/ngRepeatStartSpec.js +++ b/test/ng/directive/ngRepeatStartSpec.js @@ -428,6 +428,22 @@ describe('ngRepeatStart', function() { }); + it('should repeat over nested repeats at the same level', function() { + element = $compile( + '
' + + '
{{item1}}|
' + + '
{{item2}}
' + + '
X
' + + '
Y
' + + '
')(scope); + scope.array = ['a', 'b']; + scope.array2 = ['d', 'e']; + scope.$digest(); + + expect(element.text()).toEqual('a|dXeXYb|dXeXY'); + }); + + it('should ignore non-array element properties when iterating over an array', function() { element = $compile('
  • {{item}}
  • |
')(scope); scope.array = ['a', 'b', 'c']; From 130fd030279fdd1fcc251d54c68e54bf482ff54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Sat, 4 May 2013 19:30:40 -0300 Subject: [PATCH 4/7] refactor(compile): Optimizations for the directive 'multi-element' Avoid cloning elements during a multi-element transclude Put only one comment that will be sent to the linking function and remove all the placeholde elements Cleanup the containsAttr method with an implementation from @petebacondarwin Fixes on the unit tests to follow #2492 Document the transclude type 'multi-element' at the docs Fix a typo on the method compileNodes Move the important unit tests from ngRepeatStartSpec to ngRepeatSpec Add more tests at compile for the new multi-element transclude --- docs/content/guide/directive.ngdoc | 6 +- src/ng/compile.js | 63 +- test/ng/compileSpec.js | 94 +++ test/ng/directive/ngRepeatSpec.js | 210 +++++-- test/ng/directive/ngRepeatStartSpec.js | 800 ------------------------- 5 files changed, 278 insertions(+), 895 deletions(-) delete mode 100644 test/ng/directive/ngRepeatStartSpec.js diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index 22dd639ee970..cfc4af90e0ae 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -444,7 +444,11 @@ compiler}. The attributes are: * `true` - transclude the content of the directive. * `'element'` - transclude the whole element including any directives defined at lower priority. - + * `'multi-element'` - if the node where this directive is present contains an attribute + named `${directiveName}-start` (were `${directiveName}` is the name of the directive) then + the entire block of elements is transcluded until a sibling node is found with and attribute + named `${directiveName}-end`. If and attribute name `${directiveName}-start` is not found + then it behaves like the `'element'` transclude. * `compile`: This is the compile function described in the section below. diff --git a/src/ng/compile.js b/src/ng/compile.js index ce7284996ab8..040d74d3fc7c 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -419,9 +419,10 @@ function $CompileProvider($provide) { * rootElement must be set the jqLite collection of the compile root. This is * needed so that the jqLite collection items can be replaced with widgets. * @param {number=} max directive priority - * @param {boolean=} if the max priority should only apply to the first element in the list. - * A true value here will make the maxPriority only apply to the first element on the - * list while the other elements on the list will not have a maxPriority set + * @param {boolean=} limitMaxPriorityToFirstElement if the max priority should only apply to + * the first element in the list. A true value here will make the maxPriority only apply + * to the first element on the list while the other elements on the list will not have + * a maxPriority set * @returns {?function} A composite linking function of all of the matched directives or null. */ function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, limitMaxPriorityToFirstElement) { @@ -654,20 +655,20 @@ function $CompileProvider($provide) { // We need to compile a clone of the elements, but at the same time we have to be sure that these elements are siblings var nestedContent = extractMultiElementTransclude(compileNode, directiveName); var cloneContent = jqLite('
'); - forEach(nestedContent, function(nestedElement) { - cloneContent.append(JQLiteClone(nestedElement)); - }); - childTranscludeFn = compile(cloneContent.contents(), transcludeFn, terminalPriority, true); + $template = jqLite(compileNode); $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith($rootElement, $template, compileNode); + replaceWith($rootElement, jqLite($template[0]), compileNode); + cloneContent.append($template); forEach(nestedContent.splice(1), function(toRemove) { - replaceWith($rootElement, jqLite(toRemove), document.createComment(' placeholder ')); + removeElement($rootElement, toRemove); + cloneContent.append(toRemove); } ); + childTranscludeFn = compile(cloneContent.contents(), transcludeFn, terminalPriority, true); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); $compileNode.html(''); // clear contents @@ -774,32 +775,16 @@ function $CompileProvider($provide) { function containsAttr(node, attributeName) { - var nodeType = node.nodeType, - result = false; - - switch(nodeType) { - case 1: - // iterate over the attributes - for (var attr, name, nName, ngAttrName, nAttrs = node.attributes, - j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { - attr = nAttrs[j]; - if (attr.specified) { - name = attr.name; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { - name = ngAttrName.substr(6).toLowerCase(); - } - nName = directiveNormalize(name.toLowerCase()); - if (nName == attributeName) { - result = true; - break; - } + var attr, attrs = node.attributes, length = attrs && attrs.length; + if ( length ) { + for (var j = 0; j < length; j++) { + attr = attrs[j]; + if (attr.specified && directiveNormalize(attr.name) == attributeName) { + return true; } } - break; } - return result; + return false; } @@ -1224,6 +1209,20 @@ function $CompileProvider($provide) { newNode[jqLite.expando] = oldNode[jqLite.expando]; $element[0] = newNode; } + + + function removeElement($rootElement, element) { + var i, ii; + + if ($rootElement) { + for(i = 0, ii = $rootElement.length; i < ii; i++) { + if ($rootElement[i] == element) { + $rootElement.splice(i, 1); + break; + } + } + } + } }]; } diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 26f61357bee3..9fb5627e29be 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -381,6 +381,47 @@ describe('$compile', function() { }) }); + it("should complain when the multi-element end tag can't be found among one of the following siblings", inject(function($compile) { + forEach([ + // no content, no end tag + '
', + + // content, no end tag + '
' + + '{{item.text}}>' + + '{{item.done}}', + + // content, end tag too deep + '
' + + '
' + + '{{item.text}}>' + + '{{item.done}}' + + '' + + '
', + + // content, end tag too high + '
' + + '
' + + '{{item.text}}>' + + '{{item.done}}' + + '
' + + '
', + + ], function(template) { + expect(function() { + $compile('
' + template + '
'); + }).toThrow("Unmatched ngRepeatStart."); + }); + })); + + + it("should complain when there is an end attribute at the start of a multi-element directive", inject(function($compile) { + expect(function() { + $compile('
  • '); + }).toThrow("Unexpected ngRepeatEnd."); + })); + + describe('compiler control', function() { describe('priority', function() { it('should honor priority', inject(function($compile, $rootScope, log){ @@ -2345,6 +2386,35 @@ describe('$compile', function() { }); + it('should compile get templateFn on multi-element', function() { + module(function() { + directive('trans', function(log) { + return { + transclude: 'multi-element', + priority: 2, + controller: function($transclude) { this.$transclude = $transclude; }, + compile: function(element, attrs, template) { + log('compile: ' + angular.mock.dump(element)); + return function(scope, element, attrs, ctrl) { + log('link'); + var cursor = element; + template(scope.$new(), function(clone) {cursor.after(clone); cursor = clone.eq(-1);}); + ctrl.$transclude(function(clone) {cursor.after(clone)}); + }; + } + } + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
    {{$parent.$id}}-{{$id}};
    {{$parent.$id}}-{{$id}};
    ') + ($rootScope); + $rootScope.$apply(); + expect(log).toEqual('compile: ; HIGH; link; LOG; LOG'); + expect(element.text()).toEqual('001-002;001-002;001-003;001-003;'); + }); + }); + + it('should support transclude directive', function() { module(function() { directive('trans', function() { @@ -2485,6 +2555,30 @@ describe('$compile', function() { }); + it('should support transcluded multi-element on root content', function() { + var comment; + module(function() { + directive('transclude', valueFn({ + transclude: 'multi-element', + compile: function(element, attr, linker) { + return function(scope, element, attr) { + comment = element; + }; + } + })); + }); + inject(function($compile, $rootScope) { + var element = jqLite('
    before
    after
    ').contents(); + expect(element.length).toEqual(4); + expect(nodeName_(element[1])).toBe('DIV'); + $compile(element)($rootScope); + expect(nodeName_(element[1])).toBe('#comment'); + expect(nodeName_(comment)).toBe('#comment'); + expect(nodeName_(element[2])).toBe('DIV'); + }); + }); + + it('should safely create transclude comment node and not break with "-->"', inject(function($rootScope) { // see: https://github.com/angular/angular.js/issues/1740 diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index ac1ce7e737be..c5691c5d0d00 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -75,6 +75,37 @@ describe('ngRepeat', function() { }); + it('should be able to handle multi-element blocks', function() { + element = $compile( + '
      ' + + '
    • {{item.name}};
    • ' + + '
    • {{item.number}};
    • ' + + '
    • {{item.color}};
    • ' + + '
    ')(scope); + + Array.prototype.extraProperty = "should be ignored"; + // INIT + scope.items = [{name: 'misko', number: 1, color: 'red'}, {name:'shyam', number: 2, color: 'blue'}]; + scope.$digest(); + expect(element.find('li').length).toEqual(6); + expect(element.text()).toEqual('misko;1;red;shyam;2;blue;'); + delete Array.prototype.extraProperty; + + // GROW + scope.items.push({name: 'adam', number: 3, color: 'green'}); + scope.$digest(); + expect(element.find('li').length).toEqual(9); + expect(element.text()).toEqual('misko;1;red;shyam;2;blue;adam;3;green;'); + + // SHRINK + scope.items.pop(); + scope.items.shift(); + scope.$digest(); + expect(element.find('li').length).toEqual(3); + expect(element.text()).toEqual('shyam;2;blue;'); + }); + + it('should iterate over on object/map', function() { element = $compile( '
      ' + @@ -399,6 +430,41 @@ describe('ngRepeat', function() { }); + it('should repeat over nested arrays with multi-elements', function() { + element = $compile( + '
        ' + + '
      • ' + + '
        {{group}}|
        ' + + '
        {{group}}
        X' + + '
      • ' + + '
      • ' + + '
        {{group}}|
        ' + + '
        {{group}}
        Y' + + '
      • ' + + '
      ')(scope); + scope.groups = [['a', 'b'], ['c','d']]; + scope.$digest(); + + expect(element.text()).toEqual('a|ab|bXa|ab|bYc|cd|dXc|cd|dY'); + }); + + + it('should repeat over nested repeats at the same level', function() { + element = $compile( + '
      ' + + '
      {{item1}}|
      ' + + '
      {{item2}}
      ' + + '
      X
      ' + + '
      Y
      ' + + '
      ')(scope); + scope.array = ['a', 'b']; + scope.array2 = ['d', 'e']; + scope.$digest(); + + expect(element.text()).toEqual('a|dXeXYb|dXeXY'); + }); + + it('should ignore non-array element properties when iterating over an array', function() { element = $compile('
      • {{item}}|
      ')(scope); scope.array = ['a', 'b', 'c']; @@ -589,11 +655,10 @@ describe('ngRepeat ngAnimate', function() { inject(function($compile, $rootScope, $sniffer) { element = $compile(html( - '
      ' + - '{{ item }}' + - '
      ' + '
      ' + + '
      {{ item }}
      ' + + '
      {{item}}
      ' + + '
      ' ))($rootScope); $rootScope.$digest(); // re-enable the animations; @@ -609,15 +674,17 @@ describe('ngRepeat ngAnimate', function() { } if ($sniffer.supportsTransitions) { - angular.forEach(kids, function(kid) { - expect(kid.attr('class')).toContain('custom-enter-setup'); + for (var i = 0; i < $rootScope.items.length; ++i) { + expect(kids[2*i].attr('class')).toContain('custom-enter-setup'); + expect(kids[2*i + 1].attr('class')).toContain('custom-enter-setup'); window.setTimeout.expect(1).process(); - }); + } - angular.forEach(kids, function(kid) { - expect(kid.attr('class')).toContain('custom-enter-start'); + for (var i = 0; i < $rootScope.items.length; ++i) { + expect(kids[2*i].attr('class')).toContain('custom-enter-start'); + expect(kids[2*i + 1].attr('class')).toContain('custom-enter-start'); window.setTimeout.expect(1000).process(); - }); + } } else { expect(window.setTimeout.queue).toEqual([]); } @@ -631,49 +698,52 @@ describe('ngRepeat ngAnimate', function() { it('should fire off the leave animation + add and remove the css classes', inject(function($compile, $rootScope, $sniffer) { - element = $compile(html( - '
      ' + - '{{ item }}' + - '
      ' - ))($rootScope); + element = $compile(html( + '
      ' + + '
      {{ item }}
      ' + + '
      {{item}}
      ' + + '
      ' + ))($rootScope); - $rootScope.items = ['1','2','3']; - $rootScope.$digest(); + $rootScope.items = ['1','2','3']; + $rootScope.$digest(); - //if we add the custom css stuff here then it will get picked up before the animation takes place - var kids = element.children(); - for(var i=0;i' + - '
      ' + - '{{ item }}' + - '
      ' + + '
      {{ item }}
      ' + + '
      {{ item }}
      ' + '' ))($rootScope); @@ -681,6 +751,8 @@ describe('ngRepeat ngAnimate', function() { $rootScope.$digest(); //if we add the custom css stuff here then it will get picked up before the animation takes place + var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; + var cssValue = '1s linear all'; var kids = element.children(); for(var i=0;i
      ' + - '{{ item }}' + - '
      ' + '
      ' + + '
      {{ item }}
      ' + + '
      {{ item }}
      ' + + '
      ' ))($rootScope); $rootScope.$digest(); // re-enable the animations; @@ -737,11 +821,13 @@ describe('ngRepeat ngAnimate', function() { //if we add the custom css stuff here then it will get picked up before the animation takes place var kids = element.children(); var first = jqLite(kids[0]); - var second = jqLite(kids[1]); + var second = jqLite(kids[2]); var cssProp = 'transition'; var cssValue = '0.5s linear all'; applyCSS(first, cssProp, cssValue); + applyCSS(jqLite(kids[1]), cssProp, cssValue); applyCSS(second, cssProp, cssValue); + applyCSS(jqLite(kids[3]), cssProp, cssValue); if ($sniffer.supportsTransitions) { window.setTimeout.expect(1).process(); diff --git a/test/ng/directive/ngRepeatStartSpec.js b/test/ng/directive/ngRepeatStartSpec.js deleted file mode 100644 index 0bb149c501ac..000000000000 --- a/test/ng/directive/ngRepeatStartSpec.js +++ /dev/null @@ -1,800 +0,0 @@ -'use strict'; - -describe('ngRepeatStart', function() { - var element, $compile, scope, $exceptionHandler; - - - beforeEach(module(function($exceptionHandlerProvider) { - $exceptionHandlerProvider.mode('log'); - })); - - beforeEach(inject(function(_$compile_, $rootScope, _$exceptionHandler_) { - $compile = _$compile_; - $exceptionHandler = _$exceptionHandler_; - scope = $rootScope.$new(); - })); - - - afterEach(function() { - if ($exceptionHandler.errors.length) { - dump(jasmine.getEnv().currentSpec.getFullName()); - dump('$exceptionHandler has errors'); - dump($exceptionHandler.errors); - expect($exceptionHandler.errors).toBe([]); - } - dealoc(element); - }); - - - it('should iterate over an array of objects', function() { - element = $compile( - '
        ' + - '
      • {{item.name}};
      • ' + - '
      • {{item.number}};
      • ' + - '
      • {{item.color}};
      • ' + - '
      ')(scope); - - Array.prototype.extraProperty = "should be ignored"; - // INIT - scope.items = [{name: 'misko', number: 1, color: 'red'}, {name:'shyam', number: 2, color: 'blue'}]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('misko;1;red;shyam;2;blue;'); - delete Array.prototype.extraProperty; - - // GROW - scope.items.push({name: 'adam', number: 3, color: 'green'}); - scope.$digest(); - expect(element.find('li').length).toEqual(9); - expect(element.text()).toEqual('misko;1;red;shyam;2;blue;adam;3;green;'); - - // SHRINK - scope.items.pop(); - scope.items.shift(); - scope.$digest(); - expect(element.find('li').length).toEqual(3); - expect(element.text()).toEqual('shyam;2;blue;'); - }); - - - it('should iterate over on object/map', function() { - element = $compile( - '
        ' + - '
      • {{key}}:
      • ' + - '
      • {{value}}|
      • ' + - '
      ')(scope); - scope.items = {misko:'swe', shyam:'set'}; - scope.$digest(); - expect(element.text()).toEqual('misko:swe|shyam:set|'); - }); - - - describe('track by', function() { - it('should track using expression function', function() { - element = $compile( - '
        ' + - '
      • {{item.id}};
      • ' + - '
      • {{item.id}};
      • ' + - '
      ')(scope); - scope.items = [{id: 'misko'}, {id: 'igor'}]; - scope.$digest(); - var li0 = element.find('li')[0]; - var li1 = element.find('li')[1]; - var li2 = element.find('li')[2]; - var li3 = element.find('li')[3]; - - scope.items.push(scope.items.shift()); - scope.$digest(); - expect(element.find('li')[0]).toBe(li2); - expect(element.find('li')[1]).toBe(li3); - expect(element.find('li')[2]).toBe(li0); - expect(element.find('li')[3]).toBe(li1); - }); - - - it('should track using build in $id function', function() { - element = $compile( - '
        ' + - '
      • {{item.name}};
      • ' + - '
      • {{item.name}};
      • ' + - '
      ')(scope); - scope.items = [{name: 'misko'}, {name: 'igor'}]; - scope.$digest(); - var li0 = element.find('li')[0]; - var li1 = element.find('li')[1]; - var li2 = element.find('li')[2]; - var li3 = element.find('li')[3]; - - scope.items.push(scope.items.shift()); - scope.$digest(); - expect(element.find('li')[0]).toBe(li2); - expect(element.find('li')[1]).toBe(li3); - expect(element.find('li')[2]).toBe(li0); - expect(element.find('li')[3]).toBe(li1); - }); - - - it('should iterate over an array of primitives', function() { - element = $compile( - '
        ' + - '
      • {{item}};
      • ' + - '
      • {{item}};
      • ' + - '
      ')(scope); - - Array.prototype.extraProperty = "should be ignored"; - // INIT - scope.items = [true, true, true]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('true;true;true;true;true;true;'); - delete Array.prototype.extraProperty; - - scope.items = [false, true, true]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('false;false;true;true;true;true;'); - - scope.items = [false, true, false]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('false;false;true;true;false;false;'); - - scope.items = [true]; - scope.$digest(); - expect(element.find('li').length).toEqual(2); - expect(element.text()).toEqual('true;true;'); - - scope.items = [true, true, false]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('true;true;true;true;false;false;'); - - scope.items = [true, false, false]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('true;true;false;false;false;false;'); - - // string - scope.items = ['a', 'a', 'a']; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('a;a;a;a;a;a;'); - - scope.items = ['ab', 'a', 'a']; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('ab;ab;a;a;a;a;'); - - scope.items = ['test']; - scope.$digest(); - expect(element.find('li').length).toEqual(2); - expect(element.text()).toEqual('test;test;'); - - scope.items = ['same', 'value']; - scope.$digest(); - expect(element.find('li').length).toEqual(4); - expect(element.text()).toEqual('same;same;value;value;'); - - // number - scope.items = [12, 12, 12]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('12;12;12;12;12;12;'); - - scope.items = [53, 12, 27]; - scope.$digest(); - expect(element.find('li').length).toEqual(6); - expect(element.text()).toEqual('53;53;12;12;27;27;'); - - scope.items = [89]; - scope.$digest(); - expect(element.find('li').length).toEqual(2); - expect(element.text()).toEqual('89;89;'); - - scope.items = [89, 42]; - scope.$digest(); - expect(element.find('li').length).toEqual(4); - expect(element.text()).toEqual('89;89;42;42;'); - }); - - - it('should iterate over object with changing primitive property values', function() { - // test for issue #933 - - element = $compile( - '
        ' + - '
      • {{key}}:
      • ' + - '
      • {{value}};
      • ' + - '
      • ' + - '
      ')(scope); - - scope.items = {misko: true, shyam: true, zhenbo:true}; - scope.$digest(); - expect(element.find('li').length).toEqual(9); - expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;'); - - browserTrigger(element.find('input').eq(0), 'click'); - - expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;'); - expect(element.find('input')[0].checked).toBe(false); - expect(element.find('input')[1].checked).toBe(true); - expect(element.find('input')[2].checked).toBe(true); - - browserTrigger(element.find('input').eq(0), 'click'); - expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;'); - expect(element.find('input')[0].checked).toBe(true); - expect(element.find('input')[1].checked).toBe(true); - expect(element.find('input')[2].checked).toBe(true); - - browserTrigger(element.find('input').eq(1), 'click'); - expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;'); - expect(element.find('input')[0].checked).toBe(true); - expect(element.find('input')[1].checked).toBe(false); - expect(element.find('input')[2].checked).toBe(true); - - scope.items = {misko: false, shyam: true, zhenbo: true}; - scope.$digest(); - expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;'); - expect(element.find('input')[0].checked).toBe(false); - expect(element.find('input')[1].checked).toBe(true); - expect(element.find('input')[2].checked).toBe(true); - }); - }); - - - it('should not ngRepeatStart over parent properties', function() { - var Class = function() {}; - Class.prototype.abc = function() {}; - Class.prototype.value = 'abc'; - - element = $compile( - '
        ' + - '
      • {{key}}:
      • ' + - '
      • {{value}};
      • ' + - '
      ')(scope); - scope.items = new Class(); - scope.items.name = 'value'; - scope.$digest(); - expect(element.text()).toEqual('name:value;'); - }); - - - it('should error on wrong parsing of ngRepeatStart', function() { - element = jqLite('
      '); - $compile(element)(scope); - expect($exceptionHandler.errors.shift()[0].message). - toBe("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); - }); - - - it("should throw error when left-hand-side of ngRepeat can't be parsed", function() { - element = jqLite('
      '); - $compile(element)(scope); - expect($exceptionHandler.errors.shift()[0].message). - toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'."); - }); - - - it('should error on unexpected ngRepeatEnd', function() { - element = jqLite('
      '); - try { - $compile(element)(scope); - fail(); - } catch(e) { - expect(e.message).toBe('Unexpected ngRepeatEnd.'); - } - }); - - - it('should error on unmatched ngRepeatStart', function() { - element = jqLite('
      '); - try { - $compile(element)(scope); - fail(); - } catch(e) { - expect(e.message).toBe('Unmatched ngRepeatStart.'); - } - }); - - - it('should expose iterator offset as $index when iterating over arrays', - function() { - element = $compile( - '
        ' + - '
      • {{item}}:
      • ' + - '
      • {{$index}}|
      • ' + - '
      ')(scope); - scope.items = ['misko', 'shyam', 'frodo']; - scope.$digest(); - expect(element.text()).toEqual('misko:0|shyam:1|frodo:2|'); - }); - - - it('should expose iterator offset as $index when iterating over objects', function() { - element = $compile( - '
        ' + - '
      • {{key}}:
      • ' + - '
      • {{val}}:
      • ' + - '
      • {{$index}}|
      • ' + - '
      ')(scope); - scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'}; - scope.$digest(); - expect(element.text()).toEqual('frodo:f:0|misko:m:1|shyam:s:2|'); - }); - - - it('should expose iterator position as $first, $middle and $last when iterating over arrays', - function() { - element = $compile( - '
        ' + - '
      • {{item}}:
      • ' + - '
      • {{$first}}-
      • ' + - '
      • {{$middle}}-
      • ' + - '
      • {{$last}}|
      • ' + - '
      ')(scope); - scope.items = ['misko', 'shyam', 'doug']; - scope.$digest(); - expect(element.text()). - toEqual('misko:true-false-false|shyam:false-true-false|doug:false-false-true|'); - - scope.items.push('frodo'); - scope.$digest(); - expect(element.text()). - toEqual('misko:true-false-false|' + - 'shyam:false-true-false|' + - 'doug:false-true-false|' + - 'frodo:false-false-true|'); - - scope.items.pop(); - scope.items.pop(); - scope.$digest(); - expect(element.text()).toEqual('misko:true-false-false|shyam:false-false-true|'); - - scope.items.pop(); - scope.$digest(); - expect(element.text()).toEqual('misko:true-false-true|'); - }); - - - it('should expose iterator position as $first, $middle and $last when iterating over objects', - function() { - element = $compile( - '
        ' + - '
      • {{key}}:{{val}}:
      • ' + - '
      • {{$first}}-{{$middle}}-{{$last}}|
      • ' + - '
      ')(scope); - scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'}; - scope.$digest(); - expect(element.text()). - toEqual('doug:d:true-false-false|' + - 'frodo:f:false-true-false|' + - 'misko:m:false-true-false|' + - 'shyam:s:false-false-true|'); - - delete scope.items.doug; - delete scope.items.frodo; - scope.$digest(); - expect(element.text()).toEqual('misko:m:true-false-false|shyam:s:false-false-true|'); - - delete scope.items.shyam; - scope.$digest(); - expect(element.text()).toEqual('misko:m:true-false-true|'); - }); - - - it('should calculate $first, $middle and $last when we filter out properties from an obj', function() { - element = $compile( - '
        ' + - '
      • {{key}}:{{val}}:
      • ' + - '
      • {{$first}}-{{$middle}}-{{$last}}|
      • ' + - '
      ')(scope); - scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f', '$toBeFilteredOut': 'xxxx'}; - scope.$digest(); - expect(element.text()). - toEqual('doug:d:true-false-false|' + - 'frodo:f:false-true-false|' + - 'misko:m:false-true-false|' + - 'shyam:s:false-false-true|'); - }); - - - it('should ignore $ and $$ properties', function() { - element = $compile('
      • {{i}}|
      • {{i}}
      ')(scope); - scope.items = ['a', 'b', 'c']; - scope.items.$$hashKey = 'xxx'; - scope.items.$root = 'yyy'; - scope.$digest(); - - expect(element.text()).toEqual('a|ab|bc|c'); - }); - - - it('should repeat over nested arrays', function() { - element = $compile( - '
        ' + - '
      • ' + - '
        {{group}}|
        ' + - '
        {{group}}
        X' + - '
      • ' + - '
      • ' + - '
        {{group}}|
        ' + - '
        {{group}}
        Y' + - '
      • ' + - '
      ')(scope); - scope.groups = [['a', 'b'], ['c','d']]; - scope.$digest(); - - expect(element.text()).toEqual('a|ab|bXa|ab|bYc|cd|dXc|cd|dY'); - }); - - - it('should repeat over nested repeats at the same level', function() { - element = $compile( - '
      ' + - '
      {{item1}}|
      ' + - '
      {{item2}}
      ' + - '
      X
      ' + - '
      Y
      ' + - '
      ')(scope); - scope.array = ['a', 'b']; - scope.array2 = ['d', 'e']; - scope.$digest(); - - expect(element.text()).toEqual('a|dXeXYb|dXeXY'); - }); - - - it('should ignore non-array element properties when iterating over an array', function() { - element = $compile('
      • {{item}}
      • |
      ')(scope); - scope.array = ['a', 'b', 'c']; - scope.array.foo = '23'; - scope.array.bar = function() {}; - scope.$digest(); - - expect(element.text()).toBe('a|b|c|'); - }); - - - it('should iterate over non-existent elements of a sparse array', function() { - element = $compile('
      • {{item}}
      • |
      ')(scope); - scope.array = ['a', 'b']; - scope.array[4] = 'c'; - scope.array[6] = 'd'; - scope.$digest(); - - expect(element.text()).toBe('a|b|||c||d|'); - }); - - - it('should iterate over all kinds of types', function() { - element = $compile('
      • {{item}}
      • |
      ')(scope); - scope.array = ['a', 1, null, undefined, {}]; - scope.$digest(); - - expect(element.text()).toMatch(/a\|1\|\|\|\{\s*\}\|/); - }); - - - it('should preserve data on move of elements', function() { - element = $compile('
      • {{item}}|
      ')(scope); - scope.array = ['a', 'b']; - scope.$digest(); - - var lis = element.find('li'); - lis.eq(0).data('mark', 'a'); - lis.eq(1).data('mark', 'b'); - - scope.array = ['b', 'a']; - scope.$digest(); - - var lis = element.find('li'); - expect(lis.eq(0).data('mark')).toEqual('b'); - expect(lis.eq(1).data('mark')).toEqual('a'); - }); - - - describe('stability', function() { - var a, b, c, d, lis; - - beforeEach(function() { - element = $compile( - '
        ' + - '
      • {{key}}
      • :{{val}}
      • |
      • ' + - '
      ')(scope); - a = {}; - b = {}; - c = {}; - d = {}; - - scope.items = [a, b, c]; - scope.$digest(); - lis = element.find('li'); - }); - - - it('should preserve the order of elements', function() { - scope.items = [a, c, d]; - scope.$digest(); - var newElements = element.find('li'); - expect(newElements[0]).toEqual(lis[0]); - expect(newElements[3]).toEqual(lis[6]); - expect(newElements[6]).not.toEqual(lis[2]); - }); - - - it('should throw error on adding existing duplicates and recover', function() { - scope.items = [a, a, a]; - scope.$digest(); - expect($exceptionHandler.errors.shift().message). - toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:003'); - - // recover - scope.items = [a]; - scope.$digest(); - var newElements = element.find('li'); - expect(newElements.length).toEqual(3); - expect(newElements[0]).toEqual(lis[0]); - - scope.items = []; - scope.$digest(); - var newElements = element.find('li'); - expect(newElements.length).toEqual(0); - }); - - - it('should throw error on new duplicates and recover', function() { - scope.items = [d, d, d]; - scope.$digest(); - expect($exceptionHandler.errors.shift().message). - toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:009'); - - // recover - scope.items = [a]; - scope.$digest(); - var newElements = element.find('li'); - expect(newElements.length).toEqual(3); - expect(newElements[0]).toEqual(lis[0]); - - scope.items = []; - scope.$digest(); - var newElements = element.find('li'); - expect(newElements.length).toEqual(0); - }); - - - it('should reverse items when the collection is reversed', function() { - scope.items = [a, b, c]; - scope.$digest(); - lis = element.find('li'); - - scope.items = [c, b, a]; - scope.$digest(); - var newElements = element.find('li'); - expect(newElements.length).toEqual(9); - expect(newElements[0]).toEqual(lis[6]); - expect(newElements[3]).toEqual(lis[3]); - expect(newElements[6]).toEqual(lis[0]); - }); - - - it('should reuse elements even when model is composed of primitives', function() { - // rebuilding repeater from scratch can be expensive, we should try to avoid it even for - // model that is composed of primitives. - - scope.items = ['hello', 'cau', 'ahoj']; - scope.$digest(); - lis = element.find('li'); - lis[2].id = 'yes'; - - scope.items = ['ahoj', 'hello', 'cau']; - scope.$digest(); - var newLis = element.find('li'); - expect(newLis.length).toEqual(9); - expect(newLis[0]).toEqual(lis[6]); - expect(newLis[3]).toEqual(lis[0]); - expect(newLis[6]).toEqual(lis[3]); - }); - }); -}); - -describe('ngRepeatStart ngAnimate', function() { - var vendorPrefix, window; - var body, element; - - function html(html) { - body.html(html); - element = body.children().eq(0); - return element; - } - - beforeEach(function() { - // we need to run animation on attached elements; - body = jqLite(document.body); - }); - - afterEach(function(){ - dealoc(body); - dealoc(element); - }); - - beforeEach(module(function($animationProvider, $provide) { - $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; - $animator.enabled(true); - }; - })); - - it('should fire off the enter animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer) { - - element = $compile(html( - '
      ' + - '
      {{ item }}
      ' + - '
      {{item}}
      ' + - '
      ' - ))($rootScope); - - $rootScope.$digest(); // re-enable the animations; - - $rootScope.items = ['1','2','3']; - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var cssProp = vendorPrefix + 'transition'; - var cssValue = '1s linear all'; - var kids = element.children(); - for(var i=0;i' + - '
      {{ item }}
      ' + - '
      {{item}}
      ' + - '' - ))($rootScope); - - $rootScope.items = ['1','2','3']; - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var cssProp = vendorPrefix + 'transition'; - var cssValue = '1s linear all'; - var kids = element.children(); - for(var i=0;i' + - '
      {{ item }}
      ' + - '
      {{ item }}
      ' + - '' - ))($rootScope); - - $rootScope.items = ['1','2','3']; - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; - var cssValue = '1s linear all'; - var kids = element.children(); - for(var i=0;i' + - '
      {{ item }}
      ' + - '
      {{ item }}
      ' + - '' - ))($rootScope); - - $rootScope.$digest(); // re-enable the animations; - - $rootScope.items = ['a','b']; - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var kids = element.children(); - var first = jqLite(kids[0]); - var second = jqLite(kids[2]); - var cssProp = '-' + $sniffer.vendorPrefix + '-transition'; - var cssValue = '0.5s linear all'; - first.css(cssProp, cssValue); - second.css(cssProp, cssValue); - - if ($sniffer.supportsTransitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect(1).process(); - window.setTimeout.expect(500).process(); - window.setTimeout.expect(500).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - })); - -}); From 95d89999dfac8ee35b4bccf2781338b78b6783f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Mon, 6 May 2013 11:32:06 -0300 Subject: [PATCH 5/7] fix(compile): Fix for extended `remove` function Fix for the extended `remove` function that is used during `multi-element` transclusion to also remove the element from its parent when there is one --- src/ng/compile.js | 5 ++++- test/ng/directive/ngRepeatSpec.js | 18 +++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 040d74d3fc7c..871e8c15ebe3 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1212,7 +1212,7 @@ function $CompileProvider($provide) { function removeElement($rootElement, element) { - var i, ii; + var i, ii, parent = element.parentNode; if ($rootElement) { for(i = 0, ii = $rootElement.length; i < ii; i++) { @@ -1222,6 +1222,9 @@ function $CompileProvider($provide) { } } } + if (parent) { + parent.removeChild(element); + } } }]; } diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index c5691c5d0d00..30bc4e33d5e6 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -434,34 +434,34 @@ describe('ngRepeat', function() { element = $compile( '
        ' + '
      • ' + - '
        {{group}}|
        ' + + 'A
        {{group}}|
        ' + '
        {{group}}
        X' + '
      • ' + '
      • ' + - '
        {{group}}|
        ' + + 'B
        {{group}}|
        ' + '
        {{group}}
        Y' + - '
      • ' + + 'Z' + '
      ')(scope); scope.groups = [['a', 'b'], ['c','d']]; scope.$digest(); - expect(element.text()).toEqual('a|ab|bXa|ab|bYc|cd|dXc|cd|dY'); + expect(element.text()).toEqual('Aa|ab|bXBa|ab|bYAc|cd|dXBc|cd|dYZ'); }); it('should repeat over nested repeats at the same level', function() { element = $compile( '
      ' + - '
      {{item1}}|
      ' + - '
      {{item2}}
      ' + - '
      X
      ' + - '
      Y
      ' + + 'A
      {{item1}}|
      ' + + 'B
      {{item2}}
      ' + + '
      X
      Z' + + '
      Y
      W' + '
      ')(scope); scope.array = ['a', 'b']; scope.array2 = ['d', 'e']; scope.$digest(); - expect(element.text()).toEqual('a|dXeXYb|dXeXY'); + expect(element.text()).toEqual('Aa|BdXeXZYb|BdXeXZYW'); }); From 0fae109979142afa37afaf18f927f7640bb953c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Fri, 10 May 2013 14:43:52 -0300 Subject: [PATCH 6/7] fix(compile): IE8 specific fix Fix for [].splice on IE8 that has the second argument as mandatory Fix for arrays on IE8 that adds an extra element on trailing comma --- src/ng/compile.js | 2 +- test/ng/compileSpec.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 5277b941696e..e3d10a78da7c 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -662,7 +662,7 @@ function $CompileProvider($provide) { compileNode = $compileNode[0]; replaceWith(jqCollection, jqLite($template[0]), compileNode); cloneContent.append($template); - forEach(nestedContent.splice(1), + forEach(nestedContent.splice(1, nestedContent.length - 1), function(toRemove) { removeElement(jqCollection, toRemove); cloneContent.append(toRemove); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 9fb5627e29be..2f5181db0675 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -405,8 +405,7 @@ describe('$compile', function() { '{{item.text}}>' + '{{item.done}}' + '' + - '
      ', - + '
      ' ], function(template) { expect(function() { $compile('
      ' + template + '
      '); From 9af7cb418da45aed0af03e02f822093a7b20a522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Fri, 17 May 2013 11:45:15 -0300 Subject: [PATCH 7/7] fix(compile): add `new` when throwing an error Add a `new` when throwing an error --- src/ng/compile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index e3d10a78da7c..1bd5ecb38529 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -768,8 +768,8 @@ function $CompileProvider($provide) { transcludeContent.push(cursor); cursor = cursor.nextSibling; } while(count > 0 && cursor); - if (count > 0) throw Error('Unmatched ' + transcludeStart + '.'); - if (count < 0) throw Error('Unexpected ' + transcludeEnd + '.'); + if (count > 0) throw new Error('Unmatched ' + transcludeStart + '.'); + if (count < 0) throw new Error('Unexpected ' + transcludeEnd + '.'); return transcludeContent; }