From 70edec947c7b189694ae66b129568182e3369cab Mon Sep 17 00:00:00 2001 From: Jack Viers Date: Fri, 8 Nov 2013 12:59:28 -0600 Subject: [PATCH] fix(Angular.js): fix `isArrayLike` for unusual cases Closes #10186 Closes #8000 Closes #4855 Closes #4751 Closes #10272 --- src/Angular.js | 24 ++++++++------ test/AngularSpec.js | 55 +++++++++++++++++++++++++++++++ test/ng/directive/ngRepeatSpec.js | 9 +++++ 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index fdd72ecf6883..fea935d0e316 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -191,6 +191,11 @@ var msie = document.documentMode; +function isNodeList(obj) { + return typeof obj.length == 'number' && + typeof obj.item == 'function'; +} + /** * @private * @param {*} obj @@ -198,20 +203,19 @@ msie = document.documentMode; * String ...) */ function isArrayLike(obj) { - if (obj == null || isWindow(obj)) { - return false; - } + + // `null`, `undefined` and `window` are not array-like + if (obj == null || isWindow(obj)) return false; + + // arrays and strings are array like + if (isArray(obj) || isString(obj)) return true; // Support: iOS 8.2 (not reproducible in simulator) // "length" in obj used to prevent JIT error (gh-11508) var length = "length" in Object(obj) && obj.length; - if (obj.nodeType === NODE_TYPE_ELEMENT && length) { - return true; - } - - return isString(obj) || isArray(obj) || length === 0 || - typeof length === 'number' && length > 0 && (length - 1) in obj; + // node lists and objects with suitable length characteristics are array-like + return (isNumber(length) && length >= 0 && (length - 1) in obj) || isNodeList(obj); } /** @@ -471,7 +475,7 @@ identity.$inject = []; function valueFn(value) {return function() {return value;};} function hasCustomToString(obj) { - return isFunction(obj.toString) && obj.toString !== Object.prototype.toString; + return isFunction(obj.toString) && obj.toString !== toString; } diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 7a21a0feea39..e39471ed3bef 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -461,6 +461,7 @@ describe('angular', function() { }); describe("extend", function() { + it('should not copy the private $$hashKey', function() { var src,dst; src = {}; @@ -471,6 +472,24 @@ describe('angular', function() { }); + it('should copy the properties of the source object onto the destination object', function() { + var destination, source; + destination = {}; + source = {foo: true}; + destination = extend(destination, source); + expect(isDefined(destination.foo)).toBe(true); + }); + + + it('ISSUE #4751 - should copy the length property of an object source to the destination object', function() { + var destination, source; + destination = {}; + source = {radius: 30, length: 0}; + destination = extend(destination, source); + expect(isDefined(destination.length)).toBe(true); + expect(isDefined(destination.radius)).toBe(true); + }); + it('should retain the previous $$hashKey', function() { var src,dst,h; src = {}; @@ -1035,6 +1054,42 @@ describe('angular', function() { }); }); + describe('isArrayLike', function() { + + it('should return false if passed a number', function() { + expect(isArrayLike(10)).toBe(false); + }); + + it('should return true if passed an array', function() { + expect(isArrayLike([1,2,3,4])).toBe(true); + }); + + it('should return true if passed an object', function() { + expect(isArrayLike({0:"test", 1:"bob", 2:"tree", length:3})).toBe(true); + }); + + it('should return true if passed arguments object', function() { + function test(a,b,c) { + expect(isArrayLike(arguments)).toBe(true); + } + test(1,2,3); + }); + + it('should return true if passed a nodelist', function() { + var nodes = document.body.childNodes; + expect(isArrayLike(nodes)).toBe(true); + }); + + it('should return false for objects with `length` but no matching indexable items', function() { + var obj = { + a: 'a', + b:'b', + length: 10 + }; + expect(isArrayLike(obj)).toBe(false); + }); + }); + describe('forEach', function() { it('should iterate over *own* object properties', function() { diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 018564f02356..4523718b64f0 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -612,6 +612,15 @@ describe('ngRepeat', function() { expect(element.text()).toEqual('misko:m:0|shyam:s:1|frodo:f:2|'); }); + it('should expose iterator offset as $index when iterating over objects with length key value 0', function() { + element = $compile( + '')(scope); + scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f', 'length':0}; + scope.$digest(); + expect(element.text()).toEqual('misko:m:0|shyam:s:1|frodo:f:2|length:0:3|'); + }); it('should expose iterator position as $first, $middle and $last when iterating over arrays', function() {