@samitouri / QOS-React-2 / commits / 946029c921
Fork 0

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;