The library for web and native user interfaces.
[React Children] Step One: Refactor flattenChildren
Refactoring `flattenChildren` so that the traversal code is reusable for other purposes.
Jordan Walke committed
Jul 16, 2013 at 11:29 UTC
f4321f862485032f1f41cf2011475e57d53834f7
2 files changed
+160
-65
src/utils/flattenChildren.js
+16
-65
@@ -18,85 +18,36 @@
18
19
"use strict";
20
21
-var ReactComponent = require('ReactComponent');
22
-var ReactTextComponent = require('ReactTextComponent');
23
-
21
var throwIf = require('throwIf');
22
+var traverseAllChildren = require('traverseAllChildren');
23
24
/**
27
- * @polyFill Array.isArray
25
+ * @param {function} traverseContext Context passed through traversal.
26
+ * @param {?ReactComponent} child React child component.
27
+ * @param {!string} name String name of key path to child.
28
*/
29
-
30
-
31
-var INVALID_CHILD = 'INVALID_CHILD';
32
-if (__DEV__) {
33
- INVALID_CHILD =
34
- 'You may not pass a child of that type to a React component. It ' +
35
- 'is a common mistake to try to pass a standard browser DOM element ' +
36
- 'as a child of a React component.';
37
-}
38
-
39
-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
-
43
-var flattenChildrenImpl = function(res, children, nameSoFar) {
44
- if (Array.isArray(children)) {
45
- for (var i = 0; i < children.length; i++) {
46
- var child = children[i];
47
- flattenChildrenImpl(
48
- res,
49
- child,
50
- nameSoFar + '[' + ReactComponent.getKey(child, i) + ']'
51
- );
52
- }
53
- } else {
54
- var type = typeof children;
55
- var isOnlyChild = 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) {
64
- /* We found a component instance */
65
- if (__DEV__) {
66
- throwIf(res.hasOwnProperty(storageName), DUPLICATE_KEY_ERROR);
67
- }
68
- res[storageName] = children;
69
- } else {
70
- if (type === 'object') {
71
- throwIf(children && children.nodeType === 1, INVALID_CHILD);
72
- for (var key in children) {
73
- if (children.hasOwnProperty(key)) {
74
- flattenChildrenImpl(
75
- res,
76
- children[key],
77
- nameSoFar + '{' + key + '}'
78
- );
79
- }
80
- }
81
- } else if (type === 'string') {
82
- res[storageName] = new ReactTextComponent(children);
83
- } else if (type === 'number') {
84
- res[storageName] = new ReactTextComponent('' + children);
85
- }
86
- }
29
+function flattenSingleChildIntoContext(traverseContext, child, name) {
30
+ // We found a component instance
31
+ var result = traverseContext;
32
+ if (__DEV__) {
33
+ throwIf(
34
+ result.hasOwnProperty(name),
35
+ traverseAllChildren.DUPLICATE_KEY_ERROR
36
+ );
37
}
88
-};
38
+ result[name] = child;
39
+}
40
41
/**
42
* Flattens children that are typically specified as `props.children`.
92
- * @return {!Object} flattened children keyed by name.
43
+ * @return {!object} flattened children keyed by name.
44
*/
45
function flattenChildren(children) {
46
if (children === null || children === undefined) {
47
return children;
48
}
49
var result = {};
99
- flattenChildrenImpl(result, children, '');
50
+ traverseAllChildren(children, flattenSingleChildIntoContext, result);
51
return result;
52
}
53
src/utils/traverseAllChildren.js
new
+144
@@ -0,0 +1,144 @@
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 traverseAllChildren
17
+ */
18
+
19
+"use strict";
20
+
21
+var ReactComponent = require('ReactComponent');
22
+var ReactTextComponent = require('ReactTextComponent');
23
+
24
+var throwIf = require('throwIf');
25
+
26
+/**
27
+ * @polyFill Array.isArray
28
+ */
29
+var DUPLICATE_KEY_ERROR = 'DUPLICATE_KEY_ERROR';
30
+var INVALID_CHILD = 'INVALID_CHILD';
31
+if (__DEV__) {
32
+ INVALID_CHILD =
33
+ 'You may not pass a child of that type to a React component. It ' +
34
+ 'is a common mistake to try to pass a standard browser DOM element ' +
35
+ 'as a child of a React component.';
36
+ DUPLICATE_KEY_ERROR =
37
+ 'You have two children with identical keys. Make sure that you set the ' +
38
+ '"key" property to a unique value such as a row ID.';
39
+}
40
+
41
+/**
42
+ * TODO: Test that:
43
+ * 1. `mapChildren` transforms strings and numbers into `ReactTextComponent`.
44
+ * 2. it('should fail when supplied duplicate key', function() {
45
+ * 3. That a single child and an array with one item have the same key pattern.
46
+ * });
47
+ */
48
+
49
+/**
50
+ * @param {?*} children Children tree container.
51
+ * @param {!string} nameSoFar Name of the key path so far.
52
+ * @param {!number} indexSoFar Number of children encountered until this point.
53
+ * @param {!function} callback Callback to invoke with each child found.
54
+ * @param {?*} traverseContext Used to pass information throughout the traversal
55
+ * process.
56
+ * @return {!number} The number of children in this subtree.
57
+ */
58
+var traverseAllChildrenImpl =
59
+ function(children, nameSoFar, indexSoFar, callback, traverseContext) {
60
+ var subtreeCount = 0; // Count of children found in the current subtree.
61
+ if (Array.isArray(children)) {
62
+ for (var i = 0; i < children.length; i++) {
63
+ var child = children[i];
64
+ var nextName = nameSoFar + '[' + ReactComponent.getKey(child, i) + ']';
65
+ var nextIndex = indexSoFar + subtreeCount;
66
+ subtreeCount += traverseAllChildrenImpl(
67
+ child,
68
+ nextName,
69
+ nextIndex,
70
+ callback,
71
+ traverseContext
72
+ );
73
+ }
74
+ } else {
75
+ var type = typeof children;
76
+ var isOnlyChild = nameSoFar === '';
77
+ // If it's the only child, treat the name as if it was wrapped in an array
78
+ // so that it's consistent if the number of children grows
79
+ var storageName = isOnlyChild ?
80
+ '[' + ReactComponent.getKey(children, 0) + ']' :
81
+ nameSoFar;
82
+ if (children === null || children === undefined || type === 'boolean') {
83
+ // All of the above are perceived as null.
84
+ callback(traverseContext, null, storageName, indexSoFar);
85
+ subtreeCount = 1;
86
+ } else if (children.mountComponentIntoNode) {
87
+ callback(traverseContext, children, storageName, indexSoFar);
88
+ subtreeCount = 1;
89
+ } else {
90
+ if (type === 'object') {
91
+ throwIf(children && children.nodeType === 1, INVALID_CHILD);
92
+ for (var key in children) {
93
+ if (children.hasOwnProperty(key)) {
94
+ subtreeCount += traverseAllChildrenImpl(
95
+ children[key],
96
+ nameSoFar + '{' + key + '}',
97
+ indexSoFar + subtreeCount,
98
+ callback,
99
+ traverseContext
100
+ );
101
+ }
102
+ }
103
+ } else if (type === 'string') {
104
+ var normalizedText = new ReactTextComponent(children);
105
+ callback(traverseContext, normalizedText, storageName, indexSoFar);
106
+ subtreeCount += 1;
107
+ } else if (type === 'number') {
108
+ var normalizedNumber = new ReactTextComponent('' + children);
109
+ callback(traverseContext, normalizedNumber, storageName, indexSoFar);
110
+ subtreeCount += 1;
111
+ }
112
+ }
113
+ }
114
+ return subtreeCount;
115
+ };
116
+
117
+/**
118
+ * Traverses children that are typically specified as `props.children`, but
119
+ * might also be specified through attributes:
120
+ *
121
+ * - `traverseAllChildren(this.props.children, ...)`
122
+ * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
123
+ *
124
+ * The `traverseContext` is an optional argument that is passed through the
125
+ * entire traversal. It can be used to store accumulations or anything else that
126
+ * the callback might find relevant.
127
+ *
128
+ * @param {?*} children Children tree object.
129
+ * @param {!function} callback To invoke upon traversing each child.
130
+ * @param {?*} traverseContext Context for traversal.
131
+ */
132
+function traverseAllChildren(children, callback, traverseContext) {
133
+ if (children !== null && children !== undefined) {
134
+ traverseAllChildrenImpl(children, '', 0, callback, traverseContext);
135
+ }
136
+}
137
+
138
+/**
139
+ * Export the error code for use in other walking/mapping code.
140
+ */
141
+traverseAllChildren.DUPLICATE_KEY_ERROR = DUPLICATE_KEY_ERROR;
142
+
143
+
144
+module.exports = traverseAllChildren;