The library for web and native user interfaces.
[React Children] Step Two: Use `traverseChildren` to perform `mapChildren`
Use the new `traverseChildren` utility to perform `mapChildren`. The goal is to get as close to the bavior of the semantics of `Array.prototype.map`, but also in a way that understands deeply nested arrays and objects.
Jordan Walke committed
Jul 16, 2013 at 11:29 UTC
946029c9217362cd6cd638c967e9b31659983c22
4 files changed
+392
-197
src/utils/__tests__/mapAllChildren-test.js
new
+302
@@ -0,0 +1,302 @@
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
+ * @emails react-core
17
+ * @jsx React.DOM
18
+ */
19
+
20
+"use strict";
21
+
22
+describe('mapAllChildren', function() {
23
+ var mapAllChildren;
24
+ var React;
25
+
26
+ beforeEach(function() {
27
+ mapAllChildren = require('mapAllChildren');
28
+ React = require('React');
29
+ });
30
+
31
+
32
+ it('should support identity for simple', function() {
33
+ var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
34
+ return kid;
35
+ });
36
+
37
+ var simpleKid = <span key="simple" />;
38
+
39
+ // First pass children into a component to fully simulate what happens when
40
+ // using structures that arrive from transforms.
41
+
42
+ var instance = <div>{simpleKid}</div>;
43
+ var mappedChildren = mapAllChildren(instance.props.children, mapFn);
44
+ expect(mapFn).toHaveBeenCalledWith(simpleKid, '[simple]', 0);
45
+ expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
46
+ });
47
+
48
+ it('should treat single arrayless child as being in array', function() {
49
+ var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
50
+ return kid;
51
+ });
52
+
53
+ var simpleKid = <span />;
54
+ var instance = <div>{simpleKid}</div>;
55
+ var mappedChildren = mapAllChildren(instance.props.children, mapFn);
56
+ expect(mapFn).toHaveBeenCalledWith(simpleKid, '[0]', 0);
57
+ expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
58
+ });
59
+
60
+ it('should treat single child in array as expected', function() {
61
+ var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
62
+ return kid;
63
+ });
64
+
65
+ var simpleKid = <span />;
66
+ var instance = <div>{[simpleKid]}</div>;
67
+ var mappedChildren = mapAllChildren(instance.props.children, mapFn);
68
+ expect(mapFn).toHaveBeenCalledWith(simpleKid, '[0]', 0);
69
+ expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
70
+ });
71
+
72
+ it('should pass key to returned component', function() {
73
+ var mapFn = function (kid, key, index) {
74
+ return <div>{kid}</div>;
75
+ };
76
+
77
+ var simpleKid = <span key="simple" />;
78
+
79
+ var instance = <div>{simpleKid}</div>;
80
+ var mappedChildren = mapAllChildren(instance.props.children, mapFn);
81
+
82
+ var mappedKeys = Object.keys(mappedChildren);
83
+ expect(mappedKeys.length).toBe(1);
84
+ expect(mappedChildren[mappedKeys[0]]).not.toBe(simpleKid);
85
+ expect(mappedChildren[mappedKeys[0]].props.children).toBe(simpleKid);
86
+ expect(mappedKeys[0]).toBe('[simple]');
87
+ });
88
+
89
+ it('should invoke callback with the right context', function() {
90
+ var mapFn = function (kid, key, index) {
91
+ return this;
92
+ };
93
+
94
+ var scopeTester = {};
95
+
96
+ var simpleKid = <span key="simple" />;
97
+ var instance = <div>{simpleKid}</div>;
98
+ var mappedChildren =
99
+ mapAllChildren(instance.props.children, mapFn, scopeTester);
100
+
101
+ var mappedKeys = Object.keys(mappedChildren);
102
+ expect(mappedKeys.length).toBe(1);
103
+ expect(mappedChildren[mappedKeys[0]]).toBe(scopeTester);
104
+ });
105
+
106
+ it('should be called for each child', function() {
107
+ var zero = <div key="keyZero" />;
108
+ var one = null;
109
+ var two = <div key="keyTwo" />;
110
+ var three = null;
111
+ var four = <div key="keyFour" />;
112
+
113
+ var zeroMapped = <div key="giraffe" />; // Key should be overridden
114
+ var oneMapped = null; // Key should be added even if we don't supply it!
115
+ var twoMapped = <div />; // Key should be added even if not supplied!
116
+ var threeMapped = <span />; // Map from null to something.
117
+ var fourMapped = <div key="keyFour" />;
118
+
119
+ var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
120
+ return index === 0 ? zeroMapped :
121
+ index === 1 ? oneMapped :
122
+ index === 2 ? twoMapped :
123
+ index === 3 ? threeMapped : fourMapped;
124
+ });
125
+
126
+ var instance = (
127
+ <div>
128
+ {zero}
129
+ {one}
130
+ {two}
131
+ {three}
132
+ {four}
133
+ </div>
134
+ );
135
+
136
+ var mappedChildren = mapAllChildren(instance.props.children, mapFn);
137
+ var mappedKeys = Object.keys(mappedChildren);
138
+ expect(mapFn.calls.length).toBe(5);
139
+ expect(mappedKeys.length).toBe(5);
140
+ // Keys default to indices.
141
+ expect(mappedKeys).toEqual(
142
+ ['[keyZero]', '[1]', '[keyTwo]', '[3]', '[keyFour]']
143
+ );
144
+
145
+ expect(mapFn).toHaveBeenCalledWith(zero, '[keyZero]', 0);
146
+ expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
147
+
148
+ expect(mapFn).toHaveBeenCalledWith(one, '[1]', 1);
149
+ expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
150
+
151
+ expect(mapFn).toHaveBeenCalledWith(two, '[keyTwo]', 2);
152
+ expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
153
+
154
+ expect(mapFn).toHaveBeenCalledWith(three, '[3]', 3);
155
+ expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
156
+
157
+ expect(mapFn).toHaveBeenCalledWith(four, '[keyFour]', 4);
158
+ expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
159
+ });
160
+
161
+
162
+ it('should be called for each child in nested structure', function() {
163
+ var zero = <div key="keyZero" />;
164
+ var one = null;
165
+ var two = <div key="keyTwo" />;
166
+ var three = null;
167
+ var four = <div key="keyFour" />;
168
+ var five = <div key="keyFiveCompletelyIgnored" />;
169
+ // five is placed into a JS object with a key that takes precedence over the
170
+ // component key attribute.
171
+ // Precedence is as follows:
172
+ // 1. JavaScript Object key if in a JavaScript object:
173
+ // 2. If grouped in an Array, the `key` attribute.
174
+ // 3. The array index if in a JavaScript Array.
175
+
176
+ var zeroMapped = <div key="giraffe" />; // Key should be overridden
177
+ var oneMapped = null; // Key should be added even if we don't supply it!
178
+ var twoMapped = <div />; // Key should be added even if not supplied!
179
+ var threeMapped = <span />; // Map from null to something.
180
+ var fourMapped = <div key="keyFour" />;
181
+ var fiveMapped = <div />;
182
+
183
+ var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
184
+ return index === 0 ? zeroMapped :
185
+ index === 1 ? oneMapped :
186
+ index === 2 ? twoMapped :
187
+ index === 3 ? threeMapped :
188
+ index === 4 ? fourMapped : fiveMapped;
189
+ });
190
+
191
+ var instance = (
192
+ <div>{
193
+ [{
194
+ firstHalfKey: [zero, one, two],
195
+ secondHalfKey: [three, four],
196
+ keyFive: five
197
+ }]
198
+ }</div>
199
+ );
200
+
201
+ var mappedChildren = mapAllChildren(instance.props.children, mapFn);
202
+ var mappedKeys = Object.keys(mappedChildren);
203
+ expect(mapFn.calls.length).toBe(6);
204
+ expect(mappedKeys.length).toBe(6);
205
+ // Keys default to indices.
206
+ expect(mappedKeys).toEqual([
207
+ '[0]{firstHalfKey}[keyZero]',
208
+ '[0]{firstHalfKey}[1]',
209
+ '[0]{firstHalfKey}[keyTwo]',
210
+ '[0]{secondHalfKey}[0]',
211
+ '[0]{secondHalfKey}[keyFour]',
212
+ '[0]{keyFive}'
213
+ ]);
214
+
215
+ expect(mapFn).toHaveBeenCalledWith(zero, '[0]{firstHalfKey}[keyZero]', 0);
216
+ expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
217
+
218
+ expect(mapFn).toHaveBeenCalledWith(one, '[0]{firstHalfKey}[1]', 1);
219
+ expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
220
+
221
+ expect(mapFn).toHaveBeenCalledWith(two, '[0]{firstHalfKey}[keyTwo]', 2);
222
+ expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
223
+
224
+ expect(mapFn).toHaveBeenCalledWith(three, '[0]{secondHalfKey}[0]', 3);
225
+ expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
226
+
227
+ expect(mapFn).toHaveBeenCalledWith(four, '[0]{secondHalfKey}[keyFour]', 4);
228
+ expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
229
+
230
+ expect(mapFn).toHaveBeenCalledWith(five, '[0]{keyFive}', 5);
231
+ expect(mappedChildren[mappedKeys[5]]).toBe(fiveMapped);
232
+ });
233
+
234
+ it('should retain key across two mappings', function() {
235
+ var zeroForceKey = <div key="keyZero" />;
236
+ var oneForceKey = <div key="keyOne" />;
237
+
238
+ // Key should be overridden
239
+ var zeroForceKeyMapped = <div key="giraffe" />;
240
+ // Key should be added even if we don't supply it!
241
+ var oneForceKeyMapped = <div />;
242
+
243
+ var mapFn = function(kid, key, index) {
244
+ return index === 0 ? zeroForceKeyMapped : oneForceKeyMapped;
245
+ };
246
+
247
+ var forcedKeys = (
248
+ <div>
249
+ {zeroForceKey}
250
+ {oneForceKey}
251
+ </div>
252
+ );
253
+
254
+ var expectedForcedKeys = ['[keyZero]', '[keyOne]'];
255
+ var mappedChildrenForcedKeys =
256
+ mapAllChildren(forcedKeys.props.children, mapFn);
257
+ var mappedForcedKeys = Object.keys(mappedChildrenForcedKeys);
258
+ expect(mappedForcedKeys).toEqual(expectedForcedKeys);
259
+
260
+ var expectedRemappedForcedKeys = ['{[keyZero]}', '{[keyOne]}'];
261
+ var remappedChildrenForcedKeys =
262
+ mapAllChildren(mappedChildrenForcedKeys, mapFn);
263
+ expect(
264
+ Object.keys(remappedChildrenForcedKeys)
265
+ ).toEqual(expectedRemappedForcedKeys);
266
+
267
+ });
268
+
269
+ it('should throw if key provided is a dupe with array key', function() {
270
+ var zero = <div />;
271
+ var one = <div key="0" />;
272
+
273
+ var mapFn = function() {
274
+ return null;
275
+ };
276
+
277
+ var instance = (
278
+ <div>
279
+ {zero}
280
+ {one}
281
+ </div>
282
+ );
283
+
284
+ expect(function() {
285
+ mapAllChildren(instance.props.children, mapFn);
286
+ }).toThrow();
287
+ });
288
+
289
+ it('should throw if key provided is a dupe with explicit key', function() {
290
+ var zero = <div key="something"/>;
291
+ var one = <div key="something" />;
292
+
293
+ var mapFn = function() {return null;};
294
+ var instance = (
295
+ <div>{zero}{one}</div>
296
+ );
297
+
298
+ expect(function() {
299
+ mapAllChildren(instance.props.children, mapFn);
300
+ }).toThrow();
301
+ });
302
+});
src/utils/__tests__/mapChildren-test.js
deleted
-140
@@ -1,140 +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
- * @emails react-core
17
- * @jsx React.DOM
18
- */
19
-
20
-"use strict";
21
-
22
-describe('mapChildren', function() {
23
-
24
- var React;
25
- var ReactTestUtils;
26
-
27
- var mapChildren;
28
- var reactComponentExpect;
29
-
30
- var Wrap;
31
-
32
- beforeEach(function() {
33
- React = require('React');
34
- ReactTestUtils = require('ReactTestUtils');
35
-
36
- mapChildren = require('mapChildren');
37
- reactComponentExpect = require('reactComponentExpect');
38
-
39
- Wrap = React.createClass({
40
- render: function() {
41
- return (
42
- <div>
43
- {mapChildren(this.props.children, this.props.mapFn, this)}
44
- </div>
45
- );
46
- }
47
- });
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) {
61
- return component.props.key === expected[index];
62
- }, this);
63
- }
64
- });
65
- });
66
-
67
-
68
- it('should support identity for simple', function() {
69
- var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
70
- return kid;
71
- });
72
-
73
- var simpleKid = <span key="simple" />;
74
-
75
- var instance = <Wrap mapFn={mapFn}>{simpleKid}</Wrap>;
76
- ReactTestUtils.renderIntoDocument(instance);
77
-
78
- var rendered = reactComponentExpect(instance)
79
- .expectRenderedChild()
80
- .instance();
81
-
82
- expect(mapFn).toHaveBeenCalledWith(simpleKid, '[simple]', 0);
83
- expect(rendered.props.children[0]).toBe(simpleKid);
84
- expect(rendered.props.children).toHaveKeys(['[simple]']);
85
- });
86
-
87
- it('should pass key to returned component', function() {
88
- var mapFn = function (kid, key, index) {
89
- return <div>{kid}</div>;
90
- };
91
-
92
- var simpleKid = <span key="simple" />;
93
-
94
- var instance = <Wrap mapFn={mapFn}>{simpleKid}</Wrap>;
95
- ReactTestUtils.renderIntoDocument(instance);
96
-
97
- var rendered = reactComponentExpect(instance)
98
- .expectRenderedChild()
99
- .instance();
100
-
101
- expect(rendered.props.children[0]).not.toBe(simpleKid);
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)
105
- .toHaveKeys(['simple']);
106
- });
107
-
108
- it('should be called for each child', function() {
109
- var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) {
110
- return <div>{kid}</div>;
111
- });
112
-
113
- var kidOne = <div key="one" />;
114
- var kidTwo = <div key="two" />;
115
- var kidThree = <div key="three" />;
116
-
117
- var instance = ReactTestUtils.renderIntoDocument(
118
- <Wrap mapFn={mapFn}>
119
- {kidOne}
120
- {null}
121
- {kidTwo}
122
- {null}
123
- {kidThree}
124
- </Wrap>
125
- );
126
-
127
- var rendered = reactComponentExpect(instance)
128
- .expectRenderedChild()
129
- .instance();
130
-
131
- expect(mapFn.calls.length).toBe(3);
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([
137
- '[one]', '[two]', '[three]'
138
- ]);
139
- });
140
-});
src/utils/mapAllChildren.js
new
+90
@@ -0,0 +1,90 @@
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
+ * @providesModule mapAllChildren
17
+ */
18
+
19
+"use strict";
20
+
21
+var PooledClass = require('PooledClass');
22
+
23
+var throwIf = require('throwIf');
24
+var traverseAllChildren = require('traverseAllChildren');
25
+
26
+var threeArgumentPooler = PooledClass.threeArgumentPooler;
27
+
28
+/**
29
+ * PooledClass representing the bookkeeping associated with performing a child
30
+ * mapping. Allows avoiding binding callbacks.
31
+ *
32
+ * @constructor MapBookKeeping
33
+ * @param {!*} mapResult Object containing the ordered map of results.
34
+ * @param {!function} mapFunction Function to perform mapping with.
35
+ * @param {?*} mapContext Context to perform mapping with.
36
+ */
37
+function MapBookKeeping(mapResult, mapFunction, mapContext) {
38
+ this.mapResult = mapResult;
39
+ this.mapFunction = mapFunction;
40
+ this.mapContext = mapContext;
41
+}
42
+PooledClass.addPoolingTo(MapBookKeeping, threeArgumentPooler);
43
+
44
+module.exports = MapBookKeeping;
45
+
46
+
47
+function mapSingleChildIntoContext(traverseContext, child, name, i) {
48
+ var mapBookKeeping = traverseContext;
49
+ var mapResult = mapBookKeeping.mapResult;
50
+ var mapFunction = mapBookKeeping.mapFunction;
51
+ var mapContext = mapBookKeeping.mapContext;
52
+ var mappedChild = mapFunction.call(mapContext, child, name, i);
53
+ // We found a component instance
54
+ if (__DEV__) {
55
+ throwIf(
56
+ mapResult.hasOwnProperty(name),
57
+ traverseAllChildren.DUPLICATE_KEY_ERROR
58
+ );
59
+ }
60
+ mapResult[name] = mappedChild;
61
+}
62
+
63
+/**
64
+ * Maps children that are typically specified as `props.children`.
65
+ *
66
+ * The provided mapFunction(child, key, index) will be called for each
67
+ * leaf child.
68
+ *
69
+ * TODO: This may likely break any calls to `mapAllChildren` that were
70
+ * previously relying on the fact that we guarded against null children.
71
+ *
72
+ * @param {array} children
73
+ * @param {function(*, string, int)} mapFunction.
74
+ * @param {*} mapContext Context for mapFunction.
75
+ * @return {array} mirrored array with mapped children.
76
+ */
77
+function mapAllChildren(children, mapFunction, mapContext) {
78
+ if (children == null) {
79
+ return children;
80
+ }
81
+
82
+ var mapResult = {};
83
+ var traverseContext =
84
+ MapBookKeeping.getPooled(mapResult, mapFunction, mapContext);
85
+ traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
86
+ MapBookKeeping.release(traverseContext);
87
+ return mapResult;
88
+}
89
+
90
+module.exports = mapAllChildren;
src/utils/mapChildren.js
deleted
-57
@@ -1,57 +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
- * @providesModule mapChildren
17
- */
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
- *
29
- * @param {array} children
30
- * @param {function(*, string, int)} mapFunction
31
- * @param {*} context
32
- * @return {array} mirrored array with mapped children
33
- */
34
-function mapChildren(children, mapFunction, context) {
35
- if (children == null) {
36
- return children;
37
- }
38
- var mappedChildren = [];
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
-}
56
-
57
-module.exports = mapChildren;