diff --git a/src/core/ReactElementValidator.js b/src/core/ReactElementValidator.js
index a231f3c3c8ce1..84f8b547292fe 100644
--- a/src/core/ReactElementValidator.js
+++ b/src/core/ReactElementValidator.js
@@ -22,6 +22,7 @@ var ReactElement = require('ReactElement');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactCurrentOwner = require('ReactCurrentOwner');
+var getIteratorFn = require('getIteratorFn');
var monitorCodeUse = require('monitorCodeUse');
/**
@@ -68,7 +69,7 @@ function validateExplicitKey(component, parentType) {
warnAndMonitorForKeyUse(
'react_key_warning',
- 'Each child in an array should have a unique "key" prop.',
+ 'Each child in an array or iterator should have a unique "key" prop.',
component,
parentType
);
@@ -123,7 +124,9 @@ function warnAndMonitorForKeyUse(warningID, message, component, parentType) {
// property, it may be the creator of the child that's responsible for
// assigning it a key.
var childOwnerName = null;
- if (component._owner && component._owner !== ReactCurrentOwner.current) {
+ if (component &&
+ component._owner &&
+ component._owner !== ReactCurrentOwner.current) {
// Name of the component that originally created this child.
childOwnerName = component._owner.constructor.displayName;
@@ -161,7 +164,6 @@ function monitorUseOfObjectMap() {
* @internal
* @param {*} component Statically passed child of any type.
* @param {*} parentType component's parent's type.
- * @return {boolean}
*/
function validateChildKeys(component, parentType) {
if (Array.isArray(component)) {
@@ -174,10 +176,24 @@ function validateChildKeys(component, parentType) {
} else if (ReactElement.isValidElement(component)) {
// This component was passed in a valid location.
component._store.validated = true;
- } else if (component && typeof component === 'object') {
- monitorUseOfObjectMap();
- for (var name in component) {
- validatePropertyKey(name, component[name], parentType);
+ } else if (component) {
+ var iteratorFn = getIteratorFn(component);
+ // Entry iterators provide implicit keys.
+ if (iteratorFn && iteratorFn !== component.entries) {
+ var iterator = iteratorFn.call(component);
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (ReactElement.isValidElement(step.value)) {
+ validateExplicitKey(step.value, parentType);
+ }
+ }
+ } else if (typeof component === 'object') {
+ monitorUseOfObjectMap();
+ for (var key in component) {
+ if (component.hasOwnProperty(key)) {
+ validatePropertyKey(key, component[key], parentType);
+ }
+ }
}
}
}
diff --git a/src/core/__tests__/ReactElement-test.js b/src/core/__tests__/ReactElement-test.js
index ff9ec13e79742..dd0927342923d 100644
--- a/src/core/__tests__/ReactElement-test.js
+++ b/src/core/__tests__/ReactElement-test.js
@@ -169,10 +169,101 @@ describe('ReactElement', function() {
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain(
- 'Each child in an array should have a unique "key" prop'
+ 'Each child in an array or iterator should have a unique "key" prop.'
);
});
+ it('warns for keys for iterables of elements in rest args', function() {
+ spyOn(console, 'warn');
+ var Component = React.createFactory(ComponentFactory);
+
+ var iterable = {
+ '@@iterator': function() {
+ var i = 0;
+ return {
+ next: function() {
+ var done = ++i > 2;
+ return { value: done ? undefined : Component(), done: done };
+ }
+ };
+ }
+ };
+
+ Component(null, iterable);
+
+ expect(console.warn.argsForCall.length).toBe(1);
+ expect(console.warn.argsForCall[0][0]).toContain(
+ 'Each child in an array or iterator should have a unique "key" prop.'
+ );
+ });
+
+ it('does not warns for arrays of elements with keys', function() {
+ spyOn(console, 'warn');
+ var Component = React.createFactory(ComponentFactory);
+
+ Component(null, [ Component({key: '#1'}), Component({key: '#2'}) ]);
+
+ expect(console.warn.argsForCall.length).toBe(0);
+ });
+
+ it('does not warns for iterable elements with keys', function() {
+ spyOn(console, 'warn');
+ var Component = React.createFactory(ComponentFactory);
+
+ var iterable = {
+ '@@iterator': function() {
+ var i = 0;
+ return {
+ next: function() {
+ var done = ++i > 2;
+ return {
+ value: done ? undefined : Component({key: '#' + i}),
+ done: done
+ };
+ }
+ };
+ }
+ };
+
+ Component(null, iterable);
+
+ expect(console.warn.argsForCall.length).toBe(0);
+ });
+
+ it('warns for numeric keys on objects in rest args', function() {
+ spyOn(console, 'warn');
+ var Component = React.createFactory(ComponentFactory);
+
+ Component(null, { 1: Component(), 2: Component() });
+
+ expect(console.warn.argsForCall.length).toBe(1);
+ expect(console.warn.argsForCall[0][0]).toContain(
+ 'Child objects should have non-numeric keys so ordering is preserved.'
+ );
+ });
+
+ it('does not warn for numeric keys in entry iterables in rest args', function() {
+ spyOn(console, 'warn');
+ var Component = React.createFactory(ComponentFactory);
+
+ var iterable = {
+ '@@iterator': function() {
+ var i = 0;
+ return {
+ next: function() {
+ var done = ++i > 2;
+ return { value: done ? undefined : [i, Component()], done: done };
+ }
+ };
+ }
+ };
+ iterable.entries = iterable['@@iterator'];
+
+ Component(null, iterable);
+
+ expect(console.warn.argsForCall.length).toBe(0);
+ });
+
it('does not warn when the element is directly in rest args', function() {
spyOn(console, 'warn');
var Component = React.createFactory(ComponentFactory);
diff --git a/src/utils/__tests__/traverseAllChildren-test.js b/src/utils/__tests__/traverseAllChildren-test.js
index 8becfc2c87988..625f89f5b81e7 100644
--- a/src/utils/__tests__/traverseAllChildren-test.js
+++ b/src/utils/__tests__/traverseAllChildren-test.js
@@ -125,7 +125,64 @@ describe('traverseAllChildren', function() {
);
});
- // Todo: test that nums/strings are converted to ReactComponents.
+ it('should traverse children of different kinds', function() {
+ var div =
;
+ var span = ;
+ var a = ;
+
+ var traverseContext = [];
+ var traverseFn =
+ jasmine.createSpy().andCallFake(function(context, kid, key, index) {
+ context.push(true);
+ });
+
+ var instance = (
+
+ {div}
+ {[{span}]}
+ {{a: a}}
+ {'string'}
+ {1234}
+ {true}
+ {false}
+ {null}
+ {undefined}
+
+ );
+
+ traverseAllChildren(instance.props.children, traverseFn, traverseContext);
+
+ expect(traverseFn.calls.length).toBe(9);
+ expect(traverseContext.length).toEqual(9);
+
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, div, '.$divNode', 0
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, span, '.1:0:$span:$spanNode', 1
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, a, '.2:$a:$aNode', 2
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, 'string', '.3', 3
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, 1234, '.4', 4
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, null, '.5', 5
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, null, '.6', 6
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, null, '.7', 7
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext, null, '.8', 8
+ );
+ });
it('should be called for each child in nested structure', function() {
var zero = ;
@@ -231,4 +288,107 @@ describe('traverseAllChildren', function() {
);
});
+ it('should be called for each child in an iterable', function() {
+ var threeDivIterable = {
+ '@@iterator': function() {
+ var i = 0;
+ return {
+ next: function() {
+ if (i++ < 3) {
+ return { value: , done: false };
+ } else {
+ return { value: undefined, done: true };
+ }
+ }
+ };
+ }
+ };
+
+ var traverseContext = [];
+ var traverseFn =
+ jasmine.createSpy().andCallFake(function(context, kid, key, index) {
+ context.push(kid);
+ });
+
+ var instance = (
+
+ {threeDivIterable}
+
+ );
+
+ traverseAllChildren(instance.props.children, traverseFn, traverseContext);
+ expect(traverseFn.calls.length).toBe(3);
+
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext,
+ traverseContext[0],
+ '.$#1',
+ 0
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext,
+ traverseContext[1],
+ '.$#2',
+ 1
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext,
+ traverseContext[2],
+ '.$#3',
+ 2
+ );
+ });
+
+ it('should use keys from entry iterables', function() {
+ var threeDivEntryIterable = {
+ '@@iterator': function() {
+ var i = 0;
+ return {
+ next: function() {
+ if (i++ < 3) {
+ return { value: ['#'+i, ], done: false };
+ } else {
+ return { value: undefined, done: true };
+ }
+ }
+ };
+ }
+ };
+ threeDivEntryIterable.entries = threeDivEntryIterable['@@iterator'];
+
+ var traverseContext = [];
+ var traverseFn =
+ jasmine.createSpy().andCallFake(function(context, kid, key, index) {
+ context.push(kid);
+ });
+
+ var instance = (
+
+ {threeDivEntryIterable}
+
+ );
+
+ traverseAllChildren(instance.props.children, traverseFn, traverseContext);
+ expect(traverseFn.calls.length).toBe(3);
+
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext,
+ traverseContext[0],
+ '.$#1:0',
+ 0
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext,
+ traverseContext[1],
+ '.$#2:0',
+ 1
+ );
+ expect(traverseFn).toHaveBeenCalledWith(
+ traverseContext,
+ traverseContext[2],
+ '.$#3:0',
+ 2
+ );
+ });
+
});
diff --git a/src/utils/getIteratorFn.js b/src/utils/getIteratorFn.js
new file mode 100644
index 0000000000000..9fa8bbedf8aa4
--- /dev/null
+++ b/src/utils/getIteratorFn.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2013-2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getIteratorFn
+ * @typechecks static-only
+ */
+
+"use strict";
+
+/* global Symbol */
+var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
+
+/**
+ * Returns the iterator method function contained on the iterable object.
+ *
+ * Be sure to invoke the function with the iterable as context:
+ *
+ * var iteratorFn = getIteratorFn(myIterable);
+ * if (iteratorFn) {
+ * var iterator = iteratorFn.call(myIterable);
+ * ...
+ * }
+ *
+ * @param {?object} maybeIterable
+ * @return {?function}
+ */
+function getIteratorFn(maybeIterable) {
+ var iteratorFn = maybeIterable && (
+ (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL]) ||
+ maybeIterable[FAUX_ITERATOR_SYMBOL]
+ );
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+}
+
+module.exports = getIteratorFn;
diff --git a/src/utils/traverseAllChildren.js b/src/utils/traverseAllChildren.js
index 03a6a06b68ad6..9120c6fdd46ab 100644
--- a/src/utils/traverseAllChildren.js
+++ b/src/utils/traverseAllChildren.js
@@ -14,6 +14,7 @@
var ReactElement = require('ReactElement');
var ReactInstanceHandles = require('ReactInstanceHandles');
+var getIteratorFn = require('getIteratorFn');
var invariant = require('invariant');
var SEPARATOR = ReactInstanceHandles.SEPARATOR;
@@ -88,58 +89,91 @@ function wrapUserProvidedKey(key) {
* process.
* @return {!number} The number of children in this subtree.
*/
-var traverseAllChildrenImpl =
- function(children, nameSoFar, indexSoFar, callback, traverseContext) {
- var nextName, nextIndex;
- var subtreeCount = 0; // Count of children found in the current subtree.
- if (Array.isArray(children)) {
- for (var i = 0; i < children.length; i++) {
- var child = children[i];
- nextName = (
- nameSoFar +
- (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
- getComponentKey(child, i)
- );
- nextIndex = indexSoFar + subtreeCount;
- subtreeCount += traverseAllChildrenImpl(
- child,
- nextName,
- nextIndex,
- callback,
- traverseContext
- );
- }
- } else {
- var type = typeof children;
- var isOnlyChild = nameSoFar === '';
+function traverseAllChildrenImpl(
+ children,
+ nameSoFar,
+ indexSoFar,
+ callback,
+ traverseContext
+) {
+ var type = typeof children;
+
+ if (type === 'undefined' || type === 'boolean') {
+ // All of the above are perceived as null.
+ children = null;
+ }
+
+ if (children === null ||
+ type === 'string' ||
+ type === 'number' ||
+ ReactElement.isValidElement(children)) {
+ callback(
+ traverseContext,
+ children,
// If it's the only child, treat the name as if it was wrapped in an array
- // so that it's consistent if the number of children grows
- var storageName =
- isOnlyChild ? SEPARATOR + getComponentKey(children, 0) : nameSoFar;
- if (children == null || type === 'boolean') {
- // All of the above are perceived as null.
- callback(traverseContext, null, storageName, indexSoFar);
- subtreeCount = 1;
- } else if (type === 'string' || type === 'number' ||
- ReactElement.isValidElement(children)) {
- callback(traverseContext, children, storageName, indexSoFar);
- subtreeCount = 1;
- } else if (type === 'object') {
- invariant(
- !children || children.nodeType !== 1,
- 'traverseAllChildren(...): Encountered an invalid child; DOM ' +
- 'elements are not valid children of React components.'
- );
- for (var key in children) {
- if (children.hasOwnProperty(key)) {
+ // so that it's consistent if the number of children grows.
+ nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
+ indexSoFar
+ );
+ return 1;
+ }
+
+ var child, nextName, nextIndex;
+ var subtreeCount = 0; // Count of children found in the current subtree.
+
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ nextName = (
+ nameSoFar +
+ (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
+ getComponentKey(child, i)
+ );
+ nextIndex = indexSoFar + subtreeCount;
+ subtreeCount += traverseAllChildrenImpl(
+ child,
+ nextName,
+ nextIndex,
+ callback,
+ traverseContext
+ );
+ }
+ } else {
+ var iteratorFn = getIteratorFn(children);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(children);
+ var step;
+ if (iteratorFn !== children.entries) {
+ while (!(step = iterator.next()).done) {
+ child = step.value;
+ nextName = (
+ nameSoFar +
+ (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
+ getComponentKey(child, i)
+ );
+ nextIndex = indexSoFar + subtreeCount;
+ subtreeCount += traverseAllChildrenImpl(
+ child,
+ nextName,
+ nextIndex,
+ callback,
+ traverseContext
+ );
+ }
+ } else {
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ child = entry[1];
nextName = (
nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
- wrapUserProvidedKey(key) + SUBSEPARATOR +
- getComponentKey(children[key], 0)
+ wrapUserProvidedKey(entry[0]) + SUBSEPARATOR +
+ getComponentKey(child, 0)
);
nextIndex = indexSoFar + subtreeCount;
subtreeCount += traverseAllChildrenImpl(
- children[key],
+ child,
nextName,
nextIndex,
callback,
@@ -148,9 +182,35 @@ var traverseAllChildrenImpl =
}
}
}
+ } else if (type === 'object') {
+ invariant(
+ children.nodeType !== 1,
+ 'traverseAllChildren(...): Encountered an invalid child; DOM ' +
+ 'elements are not valid children of React components.'
+ );
+ for (var key in children) {
+ if (children.hasOwnProperty(key)) {
+ child = children[key];
+ nextName = (
+ nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
+ wrapUserProvidedKey(key) + SUBSEPARATOR +
+ getComponentKey(child, 0)
+ );
+ nextIndex = indexSoFar + subtreeCount;
+ subtreeCount += traverseAllChildrenImpl(
+ child,
+ nextName,
+ nextIndex,
+ callback,
+ traverseContext
+ );
+ }
+ }
}
- return subtreeCount;
- };
+ }
+
+ return subtreeCount;
+}
/**
* Traverses children that are typically specified as `props.children`, but