@samitouri / QOS-React-2 / commits / dd61439061
Fork 0

The library for web and native user interfaces.

Revert Flattening of Children

I still think the semantics of flattening children is valid but we'll want to revert the flattening implementation while we solidify the semantics and try another approach. This reverts flattening so that this.props.children can once again be either a single child, flat array or nested array. mapChildren calls flattenChildren before doing anything else. This is not the most efficient approach but I wanted to keep this inital diff simple. It also ignores empty values for backwards compatibility. We may want to try another approach where empty values are included in the map. Validation of keys is still done inside ReactComponent. Ideally I'd like to extract that into a separate module but to avoid cyclic dependencies, I'm keeping it in ReactComponent for now.

CommitSyncScript committed Jul 1, 2013 at 12:59 UTC dd614390614426d7011442d07900eb40c0753bce
10 files changed +152 -396
src/core/ReactComponent.js
+85 -153
@@ -37,6 +37,12 @@ var merge = require('merge');
37 */
38 var OWNER = '{owner}';
39
40 +/**
41 + * Props key that determines if a component's key was already validated.
42 + * @private
43 + */
44 +var IS_KEY_VALIDATED = '{is.key.validated}';
45 +
46 /**
47 * Every React component is in one of these life cycles.
48 */
@@ -57,124 +63,70 @@ var ComponentLifeCycle = keyMirror({
63 * This allows us to keep track of children between updates.
64 */
65
60 -var CHILD_HAS_NO_IDENTITY =
61 - 'Each child in an array should have a unique "key" prop. ' +
62 - 'Check the render method of ';
63 -
64 -var CHILD_CAME_FROM_ANOTHER_OWNER = '. It was passed a child from ';
65 -
66 var ownerHasWarned = {};
67
68 /**
69 - * Helpers for flattening child arguments onto a new array or use an existing
70 - * one.
71 - */
72 -
73 -/**
74 - * Generate a unique key that identifies this child within a set.
69 + * Warn if the component doesn't have an explicit key assigned to it.
70 + * This component is in an array. The array could grow and shrink or be
71 + * reordered. All children, that hasn't already been validated, are required to
72 + * have a "key" property assigned to it.
73 *
76 - * @param {*} Manually provided key.
77 - * @param {number} Index that is used if a manual key is not provided.
78 - * @param {?number} Grouping index if this is used in a nested array.
79 - * @return {string}
80 - */
81 -function createKey(explicitKey, childIndex, groupingIndex) {
82 - return ReactCurrentOwner.getDepth() + ':' +
83 - (groupingIndex == null ? '' : groupingIndex + ':') +
84 - (explicitKey == null ? '' + childIndex : explicitKey);
85 -}
86 -
87 -/**
88 - * Returns true if this parameter type is considered an empty child slot.
89 - * Used to filter out empty slots and generate a compact array.
90 - *
91 - * @param {*} Child component or any value.
92 - * @return {boolean}
74 + * @internal
75 + * @param {ReactComponent} component Component that requires a key.
76 */
94 -function isEmptyChild(child) {
95 - return child == null || typeof child === 'boolean';
96 -}
77 +function validateExplicitKey(component) {
78 + if (component[IS_KEY_VALIDATED] || component.props.key != null) {
79 + return;
80 + }
81 + component[IS_KEY_VALIDATED] = true;
82
98 -/**
99 - * Assign an internal identity to a child component.
100 - *
101 - * @param {number} Index of the current array grouping.
102 - * @param {*} Child component or any value.
103 - * @param {number} Index of the current child within it's grouping.
104 - */
105 -function assignKey(groupingIndex, child, index) {
106 - // Only truthy internal keys are valid. If it's not, we assign one.
107 - if (ReactComponent.isValidComponent(child) && !child._key) {
108 - var key = child.props.key;
109 - if (__DEV__) {
110 - // This is in an array. This could grow and shrink or be reordered.
111 - // All children that doesn't already have autogenerated identity needs
112 - // to have an explicit key provided.
113 - if (key == null) {
114 - // Name of the component whose render method tried to pass children.
115 - var currentName =
116 - ReactCurrentOwner.current &&
117 - ReactCurrentOwner.current.constructor.displayName;
118 -
119 - // Name of the component that originally created this child.
120 - var childOwnerName =
121 - child.props[OWNER] && child.props[OWNER].constructor.displayName;
122 -
123 - if (currentName && !ownerHasWarned.hasOwnProperty(currentName)) {
124 - ownerHasWarned[currentName] = true;
125 - var message = CHILD_HAS_NO_IDENTITY + currentName;
126 - if (childOwnerName && currentName !== childOwnerName) {
127 - // Usually the current owner is the offender, but if it accepts
128 - // children as a property, it may be the creator of the child that's
129 - // responsible for assigning it a key.
130 - message += CHILD_CAME_FROM_ANOTHER_OWNER + childOwnerName;
131 - }
132 - message += '.';
133 - console && console.warn && console.warn(message);
134 - }
135 - }
136 - }
137 - child._key = createKey(key, index, groupingIndex);
83 + // We can't provide friendly warnings for top level components.
84 + if (!ReactCurrentOwner.current) {
85 + return;
86 }
139 -}
87
141 -/**
142 - * Make sure all children have an internal identity. Returns true if this is
143 - * already a compact array.
144 - *
145 - * @param {array} Children of any type.
146 - * @return {boolean}
147 - */
148 -function tryToReuseArray(children) {
149 - for (var i = 0; i < children.length; i++) {
150 - var child = children[i];
151 - if (isEmptyChild(child)) {
152 - return false;
153 - }
154 - assignKey(0, child, i);
88 + // Name of the component whose render method tried to pass children.
89 + var currentName = ReactCurrentOwner.current.constructor.displayName;
90 + if (ownerHasWarned.hasOwnProperty(currentName)) {
91 + return;
92 + }
93 + ownerHasWarned[currentName] = true;
94 +
95 + var message = 'Each child in an array should have a unique "key" prop. ' +
96 + 'Check the render method of ' + currentName + '.';
97 + if (!component.isOwnedBy(ReactCurrentOwner.current)) {
98 + // Name of the component that originally created this child.
99 + var childOwnerName =
100 + component.props[OWNER] && component.props[OWNER].constructor.displayName;
101 +
102 + // Usually the current owner is the offender, but if it accepts
103 + // children as a property, it may be the creator of the child that's
104 + // responsible for assigning it a key.
105 + message += ' It was passed a child from ' + childOwnerName + '.';
106 }
156 - return true;
107 +
108 + global.console && console.warn && console.warn(message);
109 }
110
111 /**
160 - * Append children from the source array to the target array. Make sure all
161 - * children have an internal identity assigned to it based on insertion point.
112 + * Ensure that every component either is passed in a static location or, if
113 + * if it's passed in an array, has an explicit key property defined.
114 *
163 - * @param {number} Index of the current array grouping.
164 - * @param {array} Source array.
165 - * @param {array} Target array that will be appended to.
115 + * @internal
116 + * @param {*} component Statically passed child of any type.
117 + * @return {boolean}
118 */
167 -function appendNestedChildren(groupingIndex, sourceArray, targetArray) {
168 - for (var i = 0; i < sourceArray.length; i++) {
169 - var child = sourceArray[i];
170 - if (isEmptyChild(child)) {
171 - continue;
119 +function validateChildKeys(component) {
120 + if (Array.isArray(component)) {
121 + for (var i = 0; i < component.length; i++) {
122 + var child = component[i];
123 + if (ReactComponent.isValidComponent(child)) {
124 + validateExplicitKey(child);
125 + }
126 }
173 - assignKey(groupingIndex, child, i);
174 - // TODO: Invalid components like strings could possibly need
175 - // keys assigned to them here. Usually they're not stateful but
176 - // CSS transitions and special events could make them stateful.
177 - targetArray.push(child);
127 + } else if (ReactComponent.isValidComponent(component)) {
128 + // This component was passed in a valid location.
129 + component[IS_KEY_VALIDATED] = true;
130 }
131 }
132
@@ -218,6 +170,23 @@ var ReactComponent = {
170 );
171 },
172
173 + /**
174 + * Generate a key string that identifies a component within a set.
175 + *
176 + * @param {*} component A component that could contain a manual key.
177 + * @param {number} index Index that is used if a manual key is not provided.
178 + * @return {string}
179 + * @internal
180 + */
181 + getKey: function(component, index) {
182 + if (component && component.props && component.props.key != null) {
183 + // Explicit key
184 + return '' + component.props.key;
185 + }
186 + // Implicit key determined by the index in the set
187 + return '' + index;
188 + },
189 +
190 /**
191 * @internal
192 */
@@ -337,60 +306,23 @@ var ReactComponent = {
306 // All components start unmounted.
307 this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
308
340 - // Children can be either an array or more than one argument
341 - if (arguments.length < 2) {
342 - return;
343 - }
344 -
345 - if (arguments.length === 2) {
346 -
347 - // A single string or number child is treated as content, not an array.
348 - var type = typeof children;
349 - if (children == null || type === 'string' || type === 'number') {
350 - this.props.children = children;
351 - return;
352 - }
353 -
354 - // A single array can be reused if it's already flat
355 - if (Array.isArray(children) && tryToReuseArray(children)) {
356 - this.props.children = children;
357 - return;
309 + // Children can be more than one argument
310 + var childrenLength = arguments.length - 1;
311 + if (childrenLength === 1) {
312 + if (__DEV__) {
313 + validateChildKeys(children);
314 }
359 -
360 - }
361 -
362 - // Subsequent arguments are rolled into one child array. Array arguments
363 - // are flattened onto it. This is inlined to avoid extra heap allocation.
364 - var targetArray = null;
365 - for (var i = 1; i < arguments.length; i++) {
366 - var child = arguments[i];
367 - if (Array.isArray(child)) {
368 - if (child.length === 0) {
369 - continue;
370 - }
371 -
372 - if (targetArray === null) {
373 - targetArray = [];
374 - }
375 - appendNestedChildren(i - 1, child, targetArray);
376 -
377 - } else if (!isEmptyChild(child)) {
378 -
379 - // Only truthy internal keys are valid. If it's not, we assign one.
380 - if (ReactComponent.isValidComponent(child) && !child._key) {
381 - // This is a static node and therefore safe to key by index.
382 - // No warning necessary.
383 - child._key = createKey(child.props.key, i - 1);
315 + this.props.children = children;
316 + } else if (childrenLength > 1) {
317 + var childArray = Array(childrenLength);
318 + for (var i = 0; i < childrenLength; i++) {
319 + if (__DEV__) {
320 + validateChildKeys(arguments[i + 1]);
321 }
385 -
386 - if (targetArray === null) {
387 - targetArray = [];
388 - }
389 - targetArray.push(child);
390 -
322 + childArray[i] = arguments[i + 1];
323 }
324 + this.props.children = childArray;
325 }
393 - this.props.children = targetArray;
326 },
327
328 /**
src/core/ReactCompositeComponent.js
-3
@@ -472,7 +472,6 @@ var ReactCompositeComponentMixin = {
472 this.state = null;
473 this._pendingState = null;
474 this._compositeLifeCycleState = null;
475 - this._compositionLevel = ReactCurrentOwner.getDepth() + 1;
475 },
476
477 /**
@@ -521,8 +520,6 @@ var ReactCompositeComponentMixin = {
520
521 this._renderedComponent = this._renderValidatedComponent();
522
524 - this._renderedComponent._compositionLevel = this._compositionLevel + 1;
525 -
523 // Done with mounting, `setState` will now trigger UI changes.
524 this._compositeLifeCycleState = null;
525 var markup = this._renderedComponent.mountComponent(rootID, transaction);
src/core/ReactCurrentOwner.js
+1 -13
@@ -32,19 +32,7 @@ var ReactCurrentOwner = {
32 * @internal
33 * @type {ReactComponent}
34 */
35 - current: null,
36 -
37 - /**
38 - * @internal
39 - * @return {number}
40 - */
41 - getDepth: function() {
42 - var owner = ReactCurrentOwner.current;
43 - if (!owner) {
44 - return 0;
45 - }
46 - return owner._compositionLevel;
47 - }
35 + current: null
36
37 };
38
src/core/__tests__/ReactComponentFlattenChildren-test.js deleted
-160
@@ -1,160 +0,0 @@
1 -/**
2 - * Copyright 2013 Facebook, Inc.
3 - *
4 - * Licensed under the Apache License, Version 2.0 (the "License");
5 - * you may not use this file except in compliance with the License.
6 - * You may obtain a copy of the License at
7 - *
8 - * http://www.apache.org/licenses/LICENSE-2.0
9 - *
10 - * Unless required by applicable law or agreed to in writing, software
11 - * distributed under the License is distributed on an "AS IS" BASIS,
12 - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 - * See the License for the specific language governing permissions and
14 - * limitations under the License.
15 - *
16 - * @jsx React.DOM
17 - * @emails react-core
18 - */
19 -
20 -"use strict";
21 -
22 -var React;
23 -
24 -var Fake;
25 -
26 -var singleChild = function(children) {
27 - return (<Fake>{children}</Fake>).props.children;
28 -};
29 -
30 -var multiChild = function(child1, child2, child3, child4) {
31 - return (<Fake>{child1} {child2} {child3}{child4}</Fake>).props.children;
32 -};
33 -
34 -
35 -describe('ReactComponentFlatten', function() {
36 - beforeEach(function() {
37 - require('mock-modules').dumpCache();
38 - React = require('React');
39 - var ReactComponent = require('ReactComponent');
40 - var copyProperties = require('copyProperties');
41 - var Constructor = function() {};
42 - copyProperties(Constructor.prototype, ReactComponent.Mixin);
43 - Fake = function() {
44 - var instance = new Constructor();
45 - instance.construct.apply(instance, arguments);
46 - return instance;
47 - };
48 - });
49 -
50 - it("should reuse a flat array", function() {
51 - var children = [
52 - <div key="foo" />,
53 - <div key="bar" />
54 - ];
55 -
56 - var flat = singleChild(children);
57 -
58 - expect(flat).toBe(children);
59 - expect(flat[0]._key).toEqual('0:0:foo');
60 - expect(flat[1]._key).toEqual('0:0:bar');
61 - });
62 -
63 - it("should collapse a sparse array", function() {
64 - var children = [
65 - null,
66 - <div key="foo" />,
67 - <div key="bar" />
68 - ];
69 -
70 - var flat = singleChild(children);
71 -
72 - expect(flat).not.toBe(children);
73 - expect(flat.length).toEqual(2);
74 - expect(flat[0]._key).toEqual('0:0:foo');
75 - expect(flat[1]._key).toEqual('0:0:bar');
76 - });
77 -
78 - it("should collapse a nested array into a flat array", function() {
79 - var flat = multiChild(
80 - null,
81 - <div key="foo" />,
82 - [<div key="bar" />, null]
83 - );
84 -
85 - expect(flat.length).toEqual(2);
86 - expect(flat[0]._key).toEqual('0:foo');
87 - expect(flat[1]._key).toEqual('0:2:bar');
88 - });
89 -
90 - it("should retain key index despite static empty values", function() {
91 - var before = multiChild(
92 - <div />,
93 - <div />,
94 - [<div key="foo" />],
95 - [<div key="foo" />]
96 - );
97 -
98 - var after = multiChild(
99 - null,
100 - <div />,
101 - null,
102 - [<div key="foo" />]
103 - );
104 -
105 - expect(before.length).toEqual(4);
106 - expect(after.length).toEqual(2);
107 - expect(before[1]._key).toEqual(after[0]._key);
108 - expect(before[3]._key).toEqual(after[1]._key);
109 - });
110 -
111 - it("should retain key index when wrapped", function() {
112 - var before = singleChild(multiChild(
113 - <div />,
114 - <div />,
115 - [<div key="foo" />],
116 - [<div key="foo" />]
117 - ));
118 -
119 - var after = singleChild(multiChild(
120 - null,
121 - <div />,
122 - null,
123 - [<div key="foo" />]
124 - ));
125 -
126 - expect(before.length).toEqual(4);
127 - expect(after.length).toEqual(2);
128 - expect(before[1]._key).toEqual(after[0]._key);
129 - expect(before[3]._key).toEqual(after[1]._key);
130 - });
131 -
132 - it("should assign idempotent strings through flattening", function() {
133 - var children = [
134 - 'FOO',
135 - 'BAR'
136 - ];
137 - var flat = singleChild(children);
138 - var wrappedFlat = multiChild(null, flat);
139 - expect(flat).toBe(children);
140 - expect(wrappedFlat.length).toBe(2);
141 - expect(wrappedFlat[0]).toBe(flat[0]);
142 - expect(wrappedFlat[1]).toBe(flat[1]);
143 - });
144 -
145 - it("cannot keep keys unique when children are unboxed", function() {
146 - // This is a case that difficult to solve and should not actually be solved.
147 - var children1 = [<div key="foo" />];
148 - var children2 = [<div key="foo" />];
149 - var flat1 = singleChild(children1);
150 - var flat2 = singleChild(children2);
151 - // There's no way to tell flat1[0] and flat2[0] apart.
152 - var mergedChildren = [flat1[0], flat2[0]];
153 - var newChildren = multiChild(mergedChildren);
154 - expect(newChildren.length).toBe(2);
155 - expect(newChildren[0]._key).toBe(newChildren[1]._key);
156 - });
157 -
158 -});
159 -
160 -
src/core/__tests__/ReactIdentity-test.js
+18 -26
@@ -55,8 +55,8 @@ describe('ReactIdentity', function() {
55 React.renderComponent(instance, document.createElement('div'));
56 var node = instance.getDOMNode();
57 reactComponentExpect(instance).toBeDOMComponentWithChildCount(2);
58 - checkId(node.childNodes[0], '.r[0].[0]{first}');
59 - checkId(node.childNodes[1], '.r[0].[0]{second}');
58 + checkId(node.childNodes[0], '.r[0].{first}');
59 + checkId(node.childNodes[1], '.r[0].{second}');
60 });
61
62 it('should allow key property to express identity', function() {
@@ -71,10 +71,10 @@ describe('ReactIdentity', function() {
71 React.renderComponent(instance, document.createElement('div'));
72 var node = instance.getDOMNode();
73 reactComponentExpect(instance).toBeDOMComponentWithChildCount(4);
74 - checkId(node.childNodes[0], '.r[0].[0:apple]');
75 - checkId(node.childNodes[1], '.r[0].[0:banana]');
76 - checkId(node.childNodes[2], '.r[0].[0:0]');
77 - checkId(node.childNodes[3], '.r[0].[0:123]');
74 + checkId(node.childNodes[0], '.r[0].[apple]');
75 + checkId(node.childNodes[1], '.r[0].[banana]');
76 + checkId(node.childNodes[2], '.r[0].[0]');
77 + checkId(node.childNodes[3], '.r[0].[123]');
78 });
79
80 it('should use instance identity', function() {
@@ -95,15 +95,13 @@ describe('ReactIdentity', function() {
95 React.renderComponent(instance, document.createElement('div'));
96 var node = instance.getDOMNode();
97 reactComponentExpect(instance).toBeDOMComponentWithChildCount(3);
98 - checkId(node.childNodes[0], '.r[0].[0:wrap1]');
99 - checkId(
100 - node.childNodes[0].firstChild,
101 - '.r[0].[0:wrap1].[0:squirrel]'
102 - );
103 - checkId(node.childNodes[1], '.r[0].[0:wrap2]');
104 - checkId(node.childNodes[1].firstChild, '.r[0].[0:wrap2].[0:bunny]');
105 - checkId(node.childNodes[2], '.r[0].[0:2]');
106 - checkId(node.childNodes[2].firstChild, '.r[0].[0:2].[0:chipmunk]');
98 +
99 + checkId(node.childNodes[0], '.r[0].[wrap1]');
100 + checkId(node.childNodes[0].firstChild, '.r[0].[wrap1].[squirrel]');
101 + checkId(node.childNodes[1], '.r[0].[wrap2]');
102 + checkId(node.childNodes[1].firstChild, '.r[0].[wrap2].[bunny]');
103 + checkId(node.childNodes[2], '.r[0].[2]');
104 + checkId(node.childNodes[2].firstChild, '.r[0].[2].[chipmunk]');
105 });
106
107 function renderAComponentWithKeyIntoContainer(key, container) {
@@ -118,7 +116,7 @@ describe('ReactIdentity', function() {
116 expect(span1.getDOMNode()).not.toBe(null);
117 expect(span2.getDOMNode()).not.toBe(null);
118
121 - checkId(span1.getDOMNode(), '.r[0].[0:0:' + key + ']');
119 + checkId(span1.getDOMNode(), '.r[0].[' + key + ']');
120 checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}');
121 }
122
@@ -247,7 +245,7 @@ describe('ReactIdentity', function() {
245 }).not.toThrow();
246 });
247
250 - it('should retain keys during updates in composite components', function() {
248 + it('should retain key during updates in composite components', function() {
249
250 var TestComponent = React.createClass({
251 render: function() {
@@ -283,19 +281,13 @@ describe('ReactIdentity', function() {
281
282 React.renderComponent(wrapped, document.createElement('div'));
283
286 - var beforeKey = wrapped
287 - ._renderedComponent
288 - ._renderedComponent
289 - .props.children[0]._key;
284 + var beforeID = ReactID.getID(wrapped.getDOMNode().firstChild);
285
286 wrapped.swap();
287
293 - var afterKey = wrapped
294 - ._renderedComponent
295 - ._renderedComponent
296 - .props.children[0]._key;
288 + var afterID = ReactID.getID(wrapped.getDOMNode().firstChild);
289
298 - expect(beforeKey).not.toEqual(afterKey);
290 + expect(beforeID).not.toEqual(afterID);
291
292 });
293
src/core/__tests__/ReactMultiChildReconcile-test.js
+2 -3
@@ -42,12 +42,11 @@ var stripEmptyValues = function(obj) {
42 };
43
44 /**
45 - * These children are wrapped in an array and therefore their keys are prefixed.
46 - * Their name is also wrapped in an prefix and suffix character. We strip those
45 + * Child names is are wrapped in an prefix and suffix character. We strip those
46 * out. This relies on a tiny implementation detail of the rendering system.
47 */
48 var getOriginalKey = function(childName) {
50 - return childName.substr(4, childName.length - 5);
49 + return childName.substr(1, childName.length - 2);
50 };
51
52 /**
src/utils/__tests__/mapChildren-test.js
+16 -10
@@ -48,11 +48,17 @@ describe('mapChildren', function() {
48
49 this.addMatchers({
50 toHaveKeys: function(expected) {
51 + if (!Array.isArray(this.actual)) {
52 + if (expected.length !== 1) {
53 + return false;
54 + }
55 + return this.actual.props.key === expected[0];
56 + }
57 if (this.actual.length != expected.length) {
58 return false;
59 }
60 return this.actual.every(function(component, index) {
55 - return component._key === expected[index];
61 + return component.props.key === expected[index];
62 }, this);
63 }
64 });
@@ -73,9 +79,9 @@ describe('mapChildren', function() {
79 .expectRenderedChild()
80 .instance();
81
76 - expect(mapFn).toHaveBeenCalledWith(simpleKid, '0:simple', 0);
82 + expect(mapFn).toHaveBeenCalledWith(simpleKid, '[simple]', 0);
83 expect(rendered.props.children[0]).toBe(simpleKid);
78 - expect(rendered.props.children).toHaveKeys(['0:simple']);
84 + expect(rendered.props.children).toHaveKeys(['[simple]']);
85 });
86
87 it('should pass key to returned component', function() {
@@ -93,10 +99,10 @@ describe('mapChildren', function() {
99 .instance();
100
101 expect(rendered.props.children[0]).not.toBe(simpleKid);
96 - expect(rendered.props.children[0].props.children[0]).toBe(simpleKid);
97 - expect(rendered.props.children).toHaveKeys(['1:0:0:simple']);
102 + expect(rendered.props.children[0].props.children).toBe(simpleKid);
103 + expect(rendered.props.children).toHaveKeys(['[simple]']);
104 expect(rendered.props.children[0].props.children)
99 - .toHaveKeys(['0:simple']);
105 + .toHaveKeys(['simple']);
106 });
107
108 it('should be called for each child', function() {
@@ -123,12 +129,12 @@ describe('mapChildren', function() {
129 .instance();
130
131 expect(mapFn.calls.length).toBe(3);
126 - expect(mapFn).toHaveBeenCalledWith(kidOne, '0:one', 0);
127 - expect(mapFn).toHaveBeenCalledWith(kidTwo, '0:two', 1);
128 - expect(mapFn).toHaveBeenCalledWith(kidThree, '0:three', 2);
132 + expect(mapFn).toHaveBeenCalledWith(kidOne, '[one]', 0);
133 + expect(mapFn).toHaveBeenCalledWith(kidTwo, '[two]', 1);
134 + expect(mapFn).toHaveBeenCalledWith(kidThree, '[three]', 2);
135 expect(rendered.props.children).not.toEqual(instance.props.children);
136 expect(rendered.props.children).toHaveKeys([
131 - '1:0:0:one', '1:0:0:two', '1:0:0:three'
137 + '[one]', '[two]', '[three]'
138 ]);
139 });
140 });
src/utils/flattenChildren.js
+8 -10
@@ -18,6 +18,7 @@
18
19 "use strict";
20
21 +var ReactComponent = require('ReactComponent');
22 var ReactTextComponent = require('ReactTextComponent');
23
24 var throwIf = require('throwIf');
@@ -39,27 +40,24 @@ var DUPLICATE_KEY_ERROR =
40 'You have two children with identical keys. Make sure that you set the ' +
41 '"key" property to a unique value such as a row ID.';
42
42 -/**
43 - * If there is only a single child, it still needs a name.
44 - */
45 -var ONLY_CHILD_NAME = '0';
46 -
43 var flattenChildrenImpl = function(res, children, nameSoFar) {
48 - var key;
44 if (Array.isArray(children)) {
45 for (var i = 0; i < children.length; i++) {
46 var child = children[i];
52 - key = child && (child._key || (child.props && child.props.key));
47 flattenChildrenImpl(
48 res,
49 child,
56 - nameSoFar + '[' + (key ? key : ('' + i)) + ']'
50 + nameSoFar + '[' + ReactComponent.getKey(child, i) + ']'
51 );
52 }
53 } else {
54 var type = typeof children;
55 var isOnlyChild = nameSoFar === '';
62 - var storageName = isOnlyChild ? ONLY_CHILD_NAME : nameSoFar;
56 + // If it's the only child, treat the name as if it was wrapped in an array
57 + // so that it's consistent if the number of children grows
58 + var storageName = isOnlyChild ?
59 + '[' + ReactComponent.getKey(children, 0) + ']' :
60 + nameSoFar;
61 if (children === null || children === undefined || type === 'boolean') {
62 res[storageName] = null;
63 } else if (children.mountComponentIntoNode) {
@@ -71,7 +69,7 @@ var flattenChildrenImpl = function(res, children, nameSoFar) {
69 } else {
70 if (type === 'object') {
71 throwIf(children && children.nodeType === 1, INVALID_CHILD);
74 - for (key in children) {
72 + for (var key in children) {
73 if (children.hasOwnProperty(key)) {
74 flattenChildrenImpl(
75 res,
src/utils/mapChildren.js
+16 -8
@@ -18,14 +18,14 @@
18
19 "use strict";
20
21 +var flattenChildren = require('flattenChildren');
22 +
23 /**
24 * Maps children that are typically specified as `props.children`.
25 *
26 * The provided mapFunction(child, key, index) will be called for each
27 * leaf child.
28 *
27 - * Note: mapChildren assumes children have already been flattened.
28 - *
29 * @param {array} children
30 * @param {function(*, string, int)} mapFunction
31 * @param {*} context
@@ -36,12 +36,20 @@ function mapChildren(children, mapFunction, context) {
36 return children;
37 }
38 var mappedChildren = [];
39 - for (var ii = 0; ii < children.length; ii++) {
40 - var child = children[ii];
41 - var key = child._key;
42 - var mappedChild = mapFunction.call(context, child, key, ii);
43 - mappedChild.props.key = key;
44 - mappedChildren.push(mappedChild);
39 + var flattenedMap = flattenChildren(children);
40 + var ii = 0;
41 + for (var key in flattenedMap) {
42 + if (!flattenedMap.hasOwnProperty(key)) {
43 + continue;
44 + }
45 + var child = flattenedMap[key];
46 + // In this version of map children we ignore empty children.
47 + if (child !== null) {
48 + var mappedChild = mapFunction.call(context, child, key, ii);
49 + mappedChild.props.key = key;
50 + mappedChildren.push(mappedChild);
51 + ii++;
52 + }
53 }
54 return mappedChildren;
55 }
src/utils/onlyChild.js
+6 -10
@@ -24,24 +24,20 @@ var invariant = require('invariant');
24 /**
25 * Returns the first child in a collection of children and verifies that there
26 * is only one child in the collection. The current implementation of this
27 - * function assumes that children have been flattened, but the purpose of this
28 - * helper function is to abstract away the particular structure of children.
27 + * function assumes that a single child gets passed without a wrapper, but the
28 + * purpose of this helper function is to abstract away the particular structure
29 + * of children.
30 *
31 * @param {?object} children Child collection structure.
32 * @return {ReactComponent} The first and only `ReactComponent` contained in the
33 * structure.
34 */
35 function onlyChild(children) {
35 - invariant(Array.isArray(children), 'onlyChild must be passed a valid Array.');
36 invariant(
37 - children.length === 1,
38 - 'onlyChild must be passed an Array with exactly one child.'
37 + ReactComponent.isValidComponent(children),
38 + 'onlyChild must be passed a children with exactly one child.'
39 );
40 - invariant(
41 - ReactComponent.isValidComponent(children[0]),
42 - 'onlyChild must be passed an Array with exactly one child.'
43 - );
44 - return children[0];
40 + return children;
41 }
42
43 module.exports = onlyChild;