@samitouri / QOS-React-2 / commits / 56f51156ba

Make keyed object fragments an opaque type

This triggers a warning if you try to pass a keyed object as a child. You now have to wrap it in React.addons.createFragment(object) which creates a proxy (in dev) which warns if it is accessed. The purpose of this is to make these into opaque objects so that nobody relies on its data structure. After that we can turn it into a different data structure such as a ReactFragment node or an iterable of flattened ReactElements.

Sebastian Markbage committed Feb 9, 2015 at 14:24 UTC 56f51156bab53e39b80397211d4b1ba853902ac4
19 files changed +468 -120
src/addons/ReactFragment.js new
+164
@@ -0,0 +1,164 @@
1 +/**
2 + * Copyright 2015, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 +* @providesModule ReactFragment
10 +*/
11 +
12 +'use strict';
13 +
14 +var ReactElement = require('ReactElement');
15 +
16 +var warning = require('warning');
17 +
18 +/**
19 + * We used to allow keyed objects to serve as a collection of ReactElements,
20 + * or nested sets. This allowed us a way to explicitly key a set a fragment of
21 + * components. This is now being replaced with an opaque data structure.
22 + * The upgrade path is to call React.addons.createFragment({ key: value }) to
23 + * create a keyed fragment. The resulting data structure is opaque, for now.
24 + */
25 +
26 +if (__DEV__) {
27 + var fragmentKey = '_reactFragment';
28 + var didWarnKey = '_reactDidWarn';
29 + var canWarnForReactFragment = false;
30 +
31 + try {
32 + // Feature test. Don't even try to issue this warning if we can't use
33 + // enumerable: false.
34 +
35 + var dummy = function() {
36 + return 1;
37 + };
38 +
39 + Object.defineProperty(
40 + {},
41 + fragmentKey,
42 + { enumerable: false, value: true }
43 + );
44 +
45 + Object.defineProperty(
46 + {},
47 + 'key',
48 + { enumerable: true, get: dummy }
49 + );
50 +
51 + canWarnForReactFragment = true;
52 + } catch (x) { }
53 +
54 + var proxyPropertyAccessWithWarning = function(obj, key) {
55 + Object.defineProperty(obj, key, {
56 + enumerable: true,
57 + get: function() {
58 + warning(
59 + this[didWarnKey],
60 + 'A ReactFragment is an opaque type. Accessing any of its ' +
61 + 'properties is deprecated. Pass it to one of the React.Children ' +
62 + 'helpers.'
63 + );
64 + this[didWarnKey] = true;
65 + return this[fragmentKey][key];
66 + },
67 + set: function(value) {
68 + warning(
69 + this[didWarnKey],
70 + 'A ReactFragment is an immutable opaque type. Mutating its ' +
71 + 'properties is deprecated.'
72 + );
73 + this[didWarnKey] = true;
74 + this[fragmentKey][key] = value;
75 + }
76 + });
77 + };
78 +
79 + var issuedWarnings = {};
80 +
81 + var didWarnForFragment = function(fragment) {
82 + // We use the keys and the type of the value as a heuristic to dedupe the
83 + // warning to avoid spamming too much.
84 + var fragmentCacheKey = '';
85 + for (var key in fragment) {
86 + fragmentCacheKey += key + ':' + (typeof fragment[key]) + ',';
87 + }
88 + var alreadyWarnedOnce = !!issuedWarnings[fragmentCacheKey];
89 + issuedWarnings[fragmentCacheKey] = true;
90 + return alreadyWarnedOnce;
91 + };
92 +}
93 +
94 +var ReactFragment = {
95 + // Wrap a keyed object in an opaque proxy that warns you if you access any
96 + // of its properties.
97 + create: function(object) {
98 + if (__DEV__) {
99 + if (canWarnForReactFragment) {
100 + var proxy = {};
101 + Object.defineProperty(proxy, fragmentKey, {
102 + enumerable: false,
103 + value: object
104 + });
105 + Object.defineProperty(proxy, didWarnKey, {
106 + writable: true,
107 + enumerable: false,
108 + value: false
109 + });
110 + for (var key in object) {
111 + proxyPropertyAccessWithWarning(proxy, key);
112 + }
113 + Object.preventExtensions(proxy);
114 + return proxy;
115 + }
116 + }
117 + return object;
118 + },
119 + // Extract the original keyed object from the fragment opaque type. Warn if
120 + // a plain object is passed here.
121 + extract: function(fragment) {
122 + if (__DEV__) {
123 + if (canWarnForReactFragment) {
124 + if (!fragment[fragmentKey]) {
125 + warning(
126 + didWarnForFragment(fragment),
127 + 'Any use of a keyed object should be wrapped in ' +
128 + 'React.addons.createFragment(object) before passed as a child.'
129 + );
130 + return fragment;
131 + }
132 + return fragment[fragmentKey];
133 + }
134 + }
135 + return fragment;
136 + },
137 + // Check if this is a fragment and if so, extract the keyed object. If it
138 + // is a fragment-like object, warn that it should be wrapped. Ignore if we
139 + // can't determine what kind of object this is.
140 + extractIfFragment: function(fragment) {
141 + if (__DEV__) {
142 + if (canWarnForReactFragment) {
143 + // If it is the opaque type, return the keyed object.
144 + if (fragment[fragmentKey]) {
145 + return fragment[fragmentKey];
146 + }
147 + // Otherwise, check each property if it has an element, if it does
148 + // it is probably meant as a fragment, so we can warn early. Defer,
149 + // the warning to extract.
150 + for (var key in fragment) {
151 + if (fragment.hasOwnProperty(key) &&
152 + ReactElement.isValidElement(fragment[key])) {
153 + // This looks like a fragment object, we should provide an
154 + // early warning.
155 + return ReactFragment.extract(fragment);
156 + }
157 + }
158 + }
159 + }
160 + return fragment;
161 + }
162 +};
163 +
164 +module.exports = ReactFragment;
src/addons/__tests__/ReactFragment-test.js new
+76
@@ -0,0 +1,76 @@
1 +/**
2 + * Copyright 2015, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +var React;
15 +var ReactFragment;
16 +
17 +describe('ReactFragment', function() {
18 +
19 + beforeEach(function() {
20 + React = require('React');
21 + ReactFragment = require('ReactFragment');
22 + });
23 +
24 + it('should warn if a plain object is used as a child', function() {
25 + spyOn(console, 'warn');
26 + var children = {
27 + x: <span />,
28 + y: <span />
29 + };
30 + <div>{children}</div>;
31 + expect(console.warn.calls.length).toBe(1);
32 + expect(console.warn.calls[0].args[0]).toContain(
33 + 'Any use of a keyed object'
34 + );
35 + // Only warn once for the same set of children
36 + var sameChildren = {
37 + x: <span />,
38 + y: <span />
39 + };
40 + <div>{sameChildren}</div>;
41 + expect(console.warn.calls.length).toBe(1);
42 + });
43 +
44 + it('should warn if a plain object even if it is deep', function() {
45 + spyOn(console, 'warn');
46 + var children = {
47 + x: <span />,
48 + y: <span />,
49 + z: <span />
50 + };
51 + var element = <div>{[children]}</div>;
52 + expect(console.warn.calls.length).toBe(0);
53 + var container = document.createElement('div');
54 + React.render(element, container);
55 + expect(console.warn.calls.length).toBe(1);
56 + expect(console.warn.calls[0].args[0]).toContain(
57 + 'Any use of a keyed object'
58 + );
59 + });
60 +
61 + it('should warn if accessing any property on a fragment', function() {
62 + spyOn(console, 'warn');
63 + var children = {
64 + x: <span />,
65 + y: <span />
66 + };
67 + var frag = ReactFragment.create(children);
68 + frag.x;
69 + frag.y = 10;
70 + expect(console.warn.calls.length).toBe(1);
71 + expect(console.warn.calls[0].args[0]).toContain(
72 + 'A ReactFragment is an opaque type'
73 + );
74 + });
75 +
76 +});
src/addons/transitions/ReactTransitionChildMapping.js
+6 -2
@@ -13,6 +13,7 @@
13 'use strict';
14
15 var ReactChildren = require('ReactChildren');
16 +var ReactFragment = require('ReactFragment');
17
18 var ReactTransitionChildMapping = {
19 /**
@@ -23,9 +24,12 @@ var ReactTransitionChildMapping = {
24 * @return {object} Mapping of key to child
25 */
26 getChildMapping: function(children) {
26 - return ReactChildren.map(children, function(child) {
27 + if (!children) {
28 + return children;
29 + }
30 + return ReactFragment.extract(ReactChildren.map(children, function(child) {
31 return child;
28 - });
32 + }));
33 },
34
35 /**
src/addons/transitions/ReactTransitionGroup.js
+4 -4
@@ -202,7 +202,7 @@ var ReactTransitionGroup = React.createClass({
202 render: function() {
203 // TODO: we could get rid of the need for the wrapper node
204 // by cloning a single child
205 - var childrenToRender = {};
205 + var childrenToRender = [];
206 for (var key in this.state.children) {
207 var child = this.state.children[key];
208 if (child) {
@@ -211,10 +211,10 @@ var ReactTransitionGroup = React.createClass({
211 // already been removed. In case you need this behavior you can provide
212 // a childFactory function to wrap every child, even the ones that are
213 // leaving.
214 - childrenToRender[key] = cloneWithProps(
214 + childrenToRender.push(cloneWithProps(
215 this.props.childFactory(child),
216 - {ref: key}
217 - );
216 + {ref: key, key: key}
217 + ));
218 }
219 }
220 return React.createElement(
src/browser/ReactWithAddons.js
+2
@@ -23,6 +23,7 @@ var React = require('React');
23 var ReactComponentWithPureRenderMixin =
24 require('ReactComponentWithPureRenderMixin');
25 var ReactCSSTransitionGroup = require('ReactCSSTransitionGroup');
26 +var ReactFragment = require('ReactFragment');
27 var ReactTransitionGroup = require('ReactTransitionGroup');
28 var ReactUpdates = require('ReactUpdates');
29
@@ -39,6 +40,7 @@ React.addons = {
40 batchedUpdates: ReactUpdates.batchedUpdates,
41 classSet: cx,
42 cloneWithProps: cloneWithProps,
43 + createFragment: ReactFragment.create,
44 update: update
45 };
46
src/browser/__tests__/ReactDOM-test.js
+19 -19
@@ -75,37 +75,37 @@ describe('ReactDOM', function() {
75 */
76 it("should purge the DOM cache when removing nodes", function() {
77 var myDiv = ReactTestUtils.renderIntoDocument(
78 - <div>{{
79 - theDog: <div className="dog" />,
80 - theBird: <div className="bird" />
81 - }}</div>
78 + <div>
79 + <div key="theDog" className="dog" />,
80 + <div key="theBird" className="bird" />
81 + </div>
82 );
83 // Warm the cache with theDog
84 myDiv.setProps({
85 - children: {
86 - theDog: <div className="dogbeforedelete" />,
87 - theBird: <div className="bird" />
88 - }
85 + children: [
86 + <div key="theDog" className="dogbeforedelete" />,
87 + <div key="theBird" className="bird" />
88 + ]
89 });
90 // Remove theDog - this should purge the cache
91 myDiv.setProps({
92 - children: {
93 - theBird: <div className="bird" />
94 - }
92 + children: [
93 + <div key="theBird" className="bird" />
94 + ]
95 });
96 // Now, put theDog back. It's now a different DOM node.
97 myDiv.setProps({
98 - children: {
99 - theDog: <div className="dog" />,
100 - theBird: <div className="bird" />
101 - }
98 + children: [
99 + <div key="theDog" className="dog" />,
100 + <div key="theBird" className="bird" />
101 + ]
102 });
103 // Change the className of theDog. It will use the same element
104 myDiv.setProps({
105 - children: {
106 - theDog: <div className="bigdog" />,
107 - theBird: <div className="bird" />
108 - }
105 + children: [
106 + <div key="theDog" className="bigdog" />,
107 + <div key="theBird" className="bird" />
108 + ]
109 });
110 var root = myDiv.getDOMNode();
111 var dog = root.childNodes[0];
src/classic/element/ReactElementValidator.js
+13 -25
@@ -19,6 +19,7 @@
19 'use strict';
20
21 var ReactElement = require('ReactElement');
22 +var ReactFragment = require('ReactFragment');
23 var ReactPropTypeLocations = require('ReactPropTypeLocations');
24 var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
25 var ReactCurrentOwner = require('ReactCurrentOwner');
@@ -175,21 +176,6 @@ function warnAndMonitorForKeyUse(warningID, message, element, parentType) {
176 console.warn(message);
177 }
178
178 -/**
179 - * Log that we're using an object map. We're considering deprecating this
180 - * feature and replace it with proper Map and ImmutableMap data structures.
181 - *
182 - * @internal
183 - */
184 -function monitorUseOfObjectMap() {
185 - var currentName = getCurrentOwnerDisplayName() || '';
186 - if (ownerHasMonitoredObjectMap.hasOwnProperty(currentName)) {
187 - return;
188 - }
189 - ownerHasMonitoredObjectMap[currentName] = true;
190 - monitorCodeUse('react_object_map_children');
191 -}
192 -
179 /**
180 * Ensure that every element either is passed in a static location, in an
181 * array with an explicit keys property defined, or in an object literal
@@ -213,19 +199,21 @@ function validateChildKeys(node, parentType) {
199 } else if (node) {
200 var iteratorFn = getIteratorFn(node);
201 // Entry iterators provide implicit keys.
216 - if (iteratorFn && iteratorFn !== node.entries) {
217 - var iterator = iteratorFn.call(node);
218 - var step;
219 - while (!(step = iterator.next()).done) {
220 - if (ReactElement.isValidElement(step.value)) {
221 - validateExplicitKey(step.value, parentType);
202 + if (iteratorFn) {
203 + if (iteratorFn !== node.entries) {
204 + var iterator = iteratorFn.call(node);
205 + var step;
206 + while (!(step = iterator.next()).done) {
207 + if (ReactElement.isValidElement(step.value)) {
208 + validateExplicitKey(step.value, parentType);
209 + }
210 }
211 }
212 } else if (typeof node === 'object') {
225 - monitorUseOfObjectMap();
226 - for (var key in node) {
227 - if (node.hasOwnProperty(key)) {
228 - validatePropertyKey(key, node[key], parentType);
213 + var fragment = ReactFragment.extractIfFragment(node);
214 + for (var key in fragment) {
215 + if (fragment.hasOwnProperty(key)) {
216 + validatePropertyKey(key, fragment[key], parentType);
217 }
218 }
219 }
src/classic/element/__tests__/ReactElementValidator-test.js
+15 -1
@@ -15,6 +15,7 @@
15 // classic JS without JSX.
16
17 var React;
18 +var ReactFragment;
19 var ReactTestUtils;
20
21 describe('ReactElementValidator', function() {
@@ -24,12 +25,17 @@ describe('ReactElementValidator', function() {
25 require('mock-modules').dumpCache();
26
27 React = require('React');
28 + ReactFragment = require('ReactFragment');
29 ReactTestUtils = require('ReactTestUtils');
30 ComponentClass = React.createClass({
31 render: function() { return React.createElement('div'); }
32 });
33 });
34
35 + function frag(obj) {
36 + return ReactFragment.create(obj);
37 + }
38 +
39 it('warns for keys for arrays of elements in rest args', function() {
40 spyOn(console, 'warn');
41 var Component = React.createFactory(ComponentClass);
@@ -135,7 +141,7 @@ describe('ReactElementValidator', function() {
141 spyOn(console, 'warn');
142 var Component = React.createFactory(ComponentClass);
143
138 - Component(null, { 1: Component(), 2: Component() });
144 + Component(null, frag({ 1: Component(), 2: Component() }));
145
146 expect(console.warn.argsForCall.length).toBe(1);
147 expect(console.warn.argsForCall[0][0]).toContain(
@@ -316,4 +322,12 @@ describe('ReactElementValidator', function() {
322 expect(console.warn.calls.length).toBe(2);
323 });
324
325 + it('should warn if a fragment is used without the wrapper', function () {
326 + spyOn(console, 'warn');
327 + var child = React.createElement('span');
328 + React.createElement('div', null, { a: child, b: child });
329 + expect(console.warn.calls.length).toBe(1);
330 + expect(console.warn.calls[0].args[0]).toContain('use of a keyed object');
331 + });
332 +
333 });
src/core/__tests__/ReactIdentity-test.js
+9 -3
@@ -12,6 +12,7 @@
12 'use strict';
13
14 var React;
15 +var ReactFragment;
16 var ReactTestUtils;
17 var reactComponentExpect;
18 var ReactMount;
@@ -21,6 +22,7 @@ describe('ReactIdentity', function() {
22 beforeEach(function() {
23 require('mock-modules').dumpCache();
24 React = require('React');
25 + ReactFragment = require('ReactFragment');
26 ReactTestUtils = require('ReactTestUtils');
27 reactComponentExpect = require('reactComponentExpect');
28 ReactMount = require('ReactMount');
@@ -35,13 +37,17 @@ describe('ReactIdentity', function() {
37 expect(actual[1]).toEqual(expected[1]);
38 }
39
40 + function frag(obj) {
41 + return ReactFragment.create(obj);
42 + }
43 +
44 it('should allow keyed objects to express identity', function() {
45 var instance =
46 <div>
41 - {{
47 + {frag({
48 first: <div />,
49 second: <div />
44 - }}
50 + })}
51 </div>;
52
53 instance = React.render(instance, document.createElement('div'));
@@ -106,7 +112,7 @@ describe('ReactIdentity', function() {
112
113 var map = {};
114 map[key] = span2;
109 - return <div>{[span1, map]}</div>;
115 + return <div>{[span1, frag(map)]}</div>;
116 }
117
118 });
src/core/__tests__/ReactMultiChildReconcile-test.js
+6 -5
@@ -39,7 +39,7 @@ var stripEmptyValues = function(obj) {
39 * here. This relies on an implementation detail of the rendering system.
40 */
41 var getOriginalKey = function(childName) {
42 - var match = childName.match(/^\.\$([^.]+)\:0$/);
42 + var match = childName.match(/^\.\$([^.]+)$/);
43 expect(match).not.toBeNull();
44 return match[1];
45 };
@@ -107,13 +107,14 @@ var FriendsStatusDisplay = React.createClass({
107 return res;
108 },
109 render: function() {
110 - var children = null;
110 + var children = [];
111 var key;
112 for (key in this.props.usernameToStatus) {
113 var status = this.props.usernameToStatus[key];
114 - children = children || {};
115 - children[key] = !status ? null :
116 - <StatusDisplay ref={key} status={status} />;
114 + children.push(
115 + !status ? null :
116 + <StatusDisplay key={key} ref={key} status={status} />
117 + );
118 }
119 return (
120 <div>
src/core/__tests__/ReactMultiChildText-test.js
+15 -12
@@ -16,10 +16,13 @@
16 require('mock-modules');
17
18 var React = require('React');
19 +var ReactFragment = require('ReactFragment');
20 var ReactTestUtils = require('ReactTestUtils');
21
22 var reactComponentExpect = require('reactComponentExpect');
23
24 +var frag = ReactFragment.create;
25 +
26 // Helpers
27 var testAllPermutations = function(testCases) {
28 for (var i = 0; i < testCases.length; i += 2) {
@@ -182,18 +185,18 @@ describe('ReactMultiChildText', function() {
185 ['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
186
187 // values inside objects
185 - [{a: true}, {a: true}], [],
186 - [{a: 1.2}, {a: 1.2}], ['1.2', '1.2'],
187 - [{a: ''}, {a: ''}], ['', ''],
188 - [{a: 'foo'}, {a: 'foo'}], ['foo', 'foo'],
189 - [{a: <div />}, {a: <div />}], [<div />, <div />],
190 -
191 - [{a: true, b: 1.2, c: <div />}, '', 'foo'], ['1.2', <div />, '', 'foo'],
192 - [1.2, '', {a: <div />, b: 'foo', c: true}], ['1.2', '', <div />, 'foo'],
193 - ['', {a: 'foo', b: <div />, c: true}, 1.2], ['', 'foo', <div />, '1.2'],
194 -
195 - [true, {a: 1.2, b: '', c: <div />, d: 'foo'}, true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
196 - ['', 'foo', {a: true, b: <div />, c: 1.2, d: ''}, 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
188 + [frag({a: true}), frag({a: true})], [],
189 + [frag({a: 1.2}), frag({a: 1.2})], ['1.2', '1.2'],
190 + [frag({a: ''}), frag({a: ''})], ['', ''],
191 + [frag({a: 'foo'}), frag({a: 'foo'})], ['foo', 'foo'],
192 + [frag({a: <div />}), frag({a: <div />})], [<div />, <div />],
193 +
194 + [frag({a: true, b: 1.2, c: <div />}), '', 'foo'], ['1.2', <div />, '', 'foo'],
195 + [1.2, '', frag({a: <div />, b: 'foo', c: true})], ['1.2', '', <div />, 'foo'],
196 + ['', frag({a: 'foo', b: <div />, c: true}), 1.2], ['', 'foo', <div />, '1.2'],
197 +
198 + [true, frag({a: 1.2, b: '', c: <div />, d: 'foo'}), true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
199 + ['', 'foo', frag({a: true, b: <div />, c: 1.2, d: ''}), 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
200
201 // values inside elements
202 [<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
src/modern/element/__tests__/ReactJSXElementValidator-test.js
+7 -1
@@ -15,6 +15,7 @@
15 // of dynamic errors when using JSX with Flow.
16
17 var React;
18 +var ReactFragment;
19 var ReactTestUtils;
20
21 describe('ReactJSXElementValidator', function() {
@@ -24,6 +25,7 @@ describe('ReactJSXElementValidator', function() {
25 require('mock-modules').dumpCache();
26
27 React = require('React');
28 + ReactFragment = require('ReactFragment');
29 ReactTestUtils = require('ReactTestUtils');
30
31 Component = class {
@@ -31,6 +33,10 @@ describe('ReactJSXElementValidator', function() {
33 };
34 });
35
36 + function frag(obj) {
37 + return ReactFragment.create(obj);
38 + }
39 +
40 it('warns for keys for arrays of elements in children position', function() {
41 spyOn(console, 'warn');
42
@@ -128,7 +134,7 @@ describe('ReactJSXElementValidator', function() {
134 it('warns for numeric keys on objects as children', function() {
135 spyOn(console, 'warn');
136
131 - <Component>{{ 1: <Component />, 2: <Component /> }}</Component>;
137 + <Component>{frag({ 1: <Component />, 2: <Component /> })}</Component>;
138
139 expect(console.warn.argsForCall.length).toBe(1);
140 expect(console.warn.argsForCall[0][0]).toContain(
src/utils/ReactChildren.js
+2 -1
@@ -12,6 +12,7 @@
12 'use strict';
13
14 var PooledClass = require('PooledClass');
15 +var ReactFragment = require('ReactFragment');
16
17 var traverseAllChildren = require('traverseAllChildren');
18 var warning = require('warning');
@@ -121,7 +122,7 @@ function mapChildren(children, func, context) {
122 var traverseContext = MapBookKeeping.getPooled(mapResult, func, context);
123 traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
124 MapBookKeeping.release(traverseContext);
124 - return mapResult;
125 + return ReactFragment.create(mapResult);
126 }
127
128 function forEachSingleChildDummy(traverseContext, child, name, i) {
src/utils/__tests__/ReactChildren-test.js
+99 -34
@@ -14,12 +14,31 @@
14 describe('ReactChildren', function() {
15 var ReactChildren;
16 var React;
17 + var ReactFragment;
18
19 beforeEach(function() {
20 ReactChildren = require('ReactChildren');
21 React = require('React');
22 + ReactFragment = require('ReactFragment');
23 });
24
25 + function frag(obj) {
26 + return ReactFragment.create(obj);
27 + }
28 +
29 + function nthChild(mappedChildren, n) {
30 + var result = null;
31 + ReactChildren.forEach(mappedChildren, function(child, index) {
32 + if (index === n) {
33 + result = child;
34 + }
35 + });
36 + return result;
37 + }
38 +
39 + function keyOfNthChild(mappedChildren, n) {
40 + return Object.keys(ReactFragment.extract(mappedChildren))[n];
41 + }
42
43 it('should support identity for simple', function() {
44 var callback = jasmine.createSpy().andCallFake(function(kid, index) {
@@ -37,7 +56,7 @@ describe('ReactChildren', function() {
56 callback.reset();
57 var mappedChildren = ReactChildren.map(instance.props.children, callback);
58 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
40 - expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
59 + expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
60 });
61
62 it('should treat single arrayless child as being in array', function() {
@@ -52,7 +71,7 @@ describe('ReactChildren', function() {
71 callback.reset();
72 var mappedChildren = ReactChildren.map(instance.props.children, callback);
73 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
55 - expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
74 + expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
75 });
76
77 it('should treat single child in array as expected', function() {
@@ -67,7 +86,7 @@ describe('ReactChildren', function() {
86 callback.reset();
87 var mappedChildren = ReactChildren.map(instance.props.children, callback);
88 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
70 - expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
89 + expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
90 });
91
92 it('should pass key to returned component', function() {
@@ -80,11 +99,10 @@ describe('ReactChildren', function() {
99 var instance = <div>{simpleKid}</div>;
100 var mappedChildren = ReactChildren.map(instance.props.children, mapFn);
101
83 - var mappedKeys = Object.keys(mappedChildren);
84 - expect(mappedKeys.length).toBe(1);
85 - expect(mappedChildren[mappedKeys[0]]).not.toBe(simpleKid);
86 - expect(mappedChildren[mappedKeys[0]].props.children).toBe(simpleKid);
87 - expect(mappedKeys[0]).toBe('.$simple');
102 + expect(ReactChildren.count(mappedChildren)).toBe(1);
103 + expect(nthChild(mappedChildren, 0)).not.toBe(simpleKid);
104 + expect(nthChild(mappedChildren, 0).props.children).toBe(simpleKid);
105 + expect(keyOfNthChild(mappedChildren, 0)).toBe('.$simple');
106 });
107
108 it('should invoke callback with the right context', function() {
@@ -94,7 +112,8 @@ describe('ReactChildren', function() {
112 return this;
113 };
114
97 - var scopeTester = {};
115 + // TODO: Use an object to test, after non-object fragments has fully landed.
116 + var scopeTester = 'scope tester';
117
118 var simpleKid = <span key="simple" />;
119 var instance = <div>{simpleKid}</div>;
@@ -104,9 +123,8 @@ describe('ReactChildren', function() {
123 var mappedChildren =
124 ReactChildren.map(instance.props.children, callback, scopeTester);
125
107 - var mappedKeys = Object.keys(mappedChildren);
108 - expect(mappedKeys.length).toBe(1);
109 - expect(mappedChildren[mappedKeys[0]]).toBe(scopeTester);
126 + expect(ReactChildren.count(mappedChildren)).toBe(1);
127 + expect(nthChild(mappedChildren, 0)).toBe(scopeTester);
128 });
129
130 it('should be called for each child', function() {
@@ -149,28 +167,33 @@ describe('ReactChildren', function() {
167
168 var mappedChildren =
169 ReactChildren.map(instance.props.children, callback);
152 - var mappedKeys = Object.keys(mappedChildren);
170 expect(callback.calls.length).toBe(5);
154 - expect(mappedKeys.length).toBe(5);
171 + expect(ReactChildren.count(mappedChildren)).toBe(5);
172 // Keys default to indices.
156 - expect(mappedKeys).toEqual(
173 + expect([
174 + keyOfNthChild(mappedChildren, 0),
175 + keyOfNthChild(mappedChildren, 1),
176 + keyOfNthChild(mappedChildren, 2),
177 + keyOfNthChild(mappedChildren, 3),
178 + keyOfNthChild(mappedChildren, 4)
179 + ]).toEqual(
180 ['.$keyZero', '.1', '.$keyTwo', '.3', '.$keyFour']
181 );
182
183 expect(callback).toHaveBeenCalledWith(zero, 0);
161 - expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
184 + expect(nthChild(mappedChildren, 0)).toBe(zeroMapped);
185
186 expect(callback).toHaveBeenCalledWith(one, 1);
164 - expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
187 + expect(nthChild(mappedChildren, 1)).toBe(oneMapped);
188
189 expect(callback).toHaveBeenCalledWith(two, 2);
167 - expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
190 + expect(nthChild(mappedChildren, 2)).toBe(twoMapped);
191
192 expect(callback).toHaveBeenCalledWith(three, 3);
170 - expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
193 + expect(nthChild(mappedChildren, 3)).toBe(threeMapped);
194
195 expect(callback).toHaveBeenCalledWith(four, 4);
173 - expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
196 + expect(nthChild(mappedChildren, 4)).toBe(fourMapped);
197 });
198
199
@@ -204,11 +227,11 @@ describe('ReactChildren', function() {
227
228 var instance = (
229 <div>{
207 - [{
230 + [frag({
231 firstHalfKey: [zero, one, two],
232 secondHalfKey: [three, four],
233 keyFive: five
211 - }]
234 + })]
235 }</div>
236 );
237
@@ -222,11 +245,17 @@ describe('ReactChildren', function() {
245 callback.reset();
246
247 var mappedChildren = ReactChildren.map(instance.props.children, callback);
225 - var mappedKeys = Object.keys(mappedChildren);
248 expect(callback.calls.length).toBe(6);
227 - expect(mappedKeys.length).toBe(6);
249 + expect(ReactChildren.count(mappedChildren)).toBe(6);
250 // Keys default to indices.
229 - expect(mappedKeys).toEqual([
251 + expect([
252 + keyOfNthChild(mappedChildren, 0),
253 + keyOfNthChild(mappedChildren, 1),
254 + keyOfNthChild(mappedChildren, 2),
255 + keyOfNthChild(mappedChildren, 3),
256 + keyOfNthChild(mappedChildren, 4),
257 + keyOfNthChild(mappedChildren, 5)
258 + ]).toEqual([
259 '.0:$firstHalfKey:0:$keyZero',
260 '.0:$firstHalfKey:0:1',
261 '.0:$firstHalfKey:0:$keyTwo',
@@ -236,22 +265,22 @@ describe('ReactChildren', function() {
265 ]);
266
267 expect(callback).toHaveBeenCalledWith(zero, 0);
239 - expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
268 + expect(nthChild(mappedChildren, 0)).toBe(zeroMapped);
269
270 expect(callback).toHaveBeenCalledWith(one, 1);
242 - expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
271 + expect(nthChild(mappedChildren, 1)).toBe(oneMapped);
272
273 expect(callback).toHaveBeenCalledWith(two, 2);
245 - expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
274 + expect(nthChild(mappedChildren, 2)).toBe(twoMapped);
275
276 expect(callback).toHaveBeenCalledWith(three, 3);
248 - expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
277 + expect(nthChild(mappedChildren, 3)).toBe(threeMapped);
278
279 expect(callback).toHaveBeenCalledWith(four, 4);
251 - expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
280 + expect(nthChild(mappedChildren, 4)).toBe(fourMapped);
281
282 expect(callback).toHaveBeenCalledWith(five, 5);
254 - expect(mappedChildren[mappedKeys[5]]).toBe(fiveMapped);
283 + expect(nthChild(mappedChildren, 5)).toBe(fiveMapped);
284 });
285
286 it('should retain key across two mappings', function() {
@@ -325,7 +354,8 @@ describe('ReactChildren', function() {
354 var mapped = ReactChildren.map(instance.props.children, mapFn);
355
356 expect(console.warn.calls.length).toEqual(1);
328 - expect(mapped).toEqual({'.$something': zero});
357 + expect(nthChild(mapped, 0)).toBe(zero);
358 + expect(keyOfNthChild(mapped, 0)).toBe('.$something');
359 });
360
361 it('should return 0 for null children', function() {
@@ -380,14 +410,49 @@ describe('ReactChildren', function() {
410
411 var instance = (
412 <div>{
383 - [{
413 + [frag({
414 firstHalfKey: [zero, one, two],
415 secondHalfKey: [three, four],
416 keyFive: five
387 - }]
417 + })]
418 }</div>
419 );
420 var numberOfChildren = ReactChildren.count(instance.props.children);
421 expect(numberOfChildren).toBe(6);
422 });
423 +
424 + it('should warn if a fragment is used without the wrapper', function () {
425 + spyOn(console, 'warn');
426 + var child = React.createElement('span');
427 + ReactChildren.forEach({ a: child, b: child}, function(c) {
428 + expect(c).toBe(child);
429 + });
430 + expect(console.warn.calls.length).toBe(1);
431 + expect(console.warn.calls[0].args[0]).toContain('use of a keyed object');
432 + });
433 +
434 + it('should warn if a fragment is accessed', function () {
435 + spyOn(console, 'warn');
436 + var child = React.createElement('span');
437 + var frag = ReactChildren.map([ child, child ], function(c) {
438 + return c;
439 + });
440 + for (var key in frag) {
441 + frag[key];
442 + break;
443 + }
444 + expect(console.warn.calls.length).toBe(1);
445 + expect(console.warn.calls[0].args[0]).toContain('is an opaque type');
446 +
447 + var frag2 = ReactChildren.map([ child, child ], function(c) {
448 + return c;
449 + });
450 + for (var key in frag2) {
451 + frag2[key] = 123;
452 + break;
453 + }
454 + expect(console.warn.calls.length).toBe(2);
455 + expect(console.warn.calls[1].args[0]).toContain('is an immutable opaque');
456 + });
457 +
458 });
src/utils/__tests__/onlyChild-test.js
+3 -1
@@ -14,11 +14,13 @@
14 describe('onlyChild', function() {
15
16 var React;
17 + var ReactFragment;
18 var onlyChild;
19 var WrapComponent;
20
21 beforeEach(function() {
22 React = require('React');
23 + ReactFragment = require('ReactFragment');
24 onlyChild = require('onlyChild');
25 WrapComponent = React.createClass({
26 render: function() {
@@ -64,7 +66,7 @@ describe('onlyChild', function() {
66 expect(function() {
67 var instance =
68 <WrapComponent>
67 - {{oneThing: <span />}}
69 + {ReactFragment.create({oneThing: <span />})}
70 </WrapComponent>;
71 onlyChild(instance.props.children);
72 }).toThrow();
src/utils/__tests__/sliceChildren-test.js
+11 -4
@@ -14,6 +14,7 @@
14 describe('sliceChildren', function() {
15
16 var React;
17 + var ReactFragment;
18 var ReactTestUtils;
19
20 var sliceChildren;
@@ -23,6 +24,7 @@ describe('sliceChildren', function() {
24
25 beforeEach(function() {
26 React = require('React');
27 + ReactFragment = require('ReactFragment');
28 ReactTestUtils = require('ReactTestUtils');
29
30 sliceChildren = require('sliceChildren');
@@ -52,6 +54,11 @@ describe('sliceChildren', function() {
54 return rendered.props.children;
55 }
56
57 + function testKeyValuePairs(children, expectedPairs) {
58 + var obj = ReactFragment.extract(children);
59 + expect(obj).toEqual(expectedPairs);
60 + }
61 +
62 it('should render the whole set if start zero is supplied', function() {
63 var fullSet = [
64 <div key="A" />,
@@ -59,7 +66,7 @@ describe('sliceChildren', function() {
66 <div key="C" />
67 ];
68 var children = renderAndSlice(fullSet, 0);
62 - expect(children).toEqual({
69 + testKeyValuePairs(children, {
70 '.$A': fullSet[0],
71 '.$B': fullSet[1],
72 '.$C': fullSet[2]
@@ -73,7 +80,7 @@ describe('sliceChildren', function() {
80 <div key="C" />
81 ];
82 var children = renderAndSlice(fullSet, 1);
76 - expect(children).toEqual({
83 + testKeyValuePairs(children, {
84 '.$B': fullSet[1],
85 '.$C': fullSet[2]
86 });
@@ -87,7 +94,7 @@ describe('sliceChildren', function() {
94 <div key="D" />
95 ];
96 var children = renderAndSlice(fullSet, 1, 2);
90 - expect(children).toEqual({
97 + testKeyValuePairs(children, {
98 '.$B': fullSet[1]
99 });
100 });
@@ -103,7 +110,7 @@ describe('sliceChildren', function() {
110 .expectRenderedChild()
111 .instance();
112
106 - expect(rendered.props.children).toEqual({
113 + testKeyValuePairs(rendered.props.children, {
114 '.1': b
115 });
116 });
src/utils/__tests__/traverseAllChildren-test.js
+9 -4
@@ -14,11 +14,16 @@
14 describe('traverseAllChildren', function() {
15 var traverseAllChildren;
16 var React;
17 + var ReactFragment;
18 beforeEach(function() {
19 traverseAllChildren = require('traverseAllChildren');
20 React = require('React');
21 + ReactFragment = require('ReactFragment');
22 });
23
24 + function frag(obj) {
25 + return ReactFragment.create(obj);
26 + }
27
28 it('should support identity for simple', function() {
29 var traverseContext = [];
@@ -139,8 +144,8 @@ describe('traverseAllChildren', function() {
144 var instance = (
145 <div>
146 {div}
142 - {[{span}]}
143 - {{a: a}}
147 + {[frag({span})]}
148 + {frag({a: a})}
149 {'string'}
150 {1234}
151 {true}
@@ -206,11 +211,11 @@ describe('traverseAllChildren', function() {
211
212 var instance = (
213 <div>{
209 - [{
214 + [frag({
215 firstHalfKey: [zero, one, two],
216 secondHalfKey: [three, four],
217 keyFive: five
213 - }]
218 + })]
219 }</div>
220 );
221
src/utils/sliceChildren.js
+3 -1
@@ -11,6 +11,8 @@
11
12 'use strict';
13
14 +var ReactFragment = require('ReactFragment');
15 +
16 var flattenChildren = require('flattenChildren');
17
18 /**
@@ -43,7 +45,7 @@ function sliceChildren(children, start, end) {
45 break;
46 }
47 }
46 - return slicedChildren;
48 + return ReactFragment.create(slicedChildren);
49 }
50
51 module.exports = sliceChildren;
src/utils/traverseAllChildren.js
+5 -3
@@ -12,6 +12,7 @@
12 'use strict';
13
14 var ReactElement = require('ReactElement');
15 +var ReactFragment = require('ReactFragment');
16 var ReactInstanceHandles = require('ReactInstanceHandles');
17
18 var getIteratorFn = require('getIteratorFn');
@@ -184,9 +185,10 @@ function traverseAllChildrenImpl(
185 'traverseAllChildren(...): Encountered an invalid child; DOM ' +
186 'elements are not valid children of React components.'
187 );
187 - for (var key in children) {
188 - if (children.hasOwnProperty(key)) {
189 - child = children[key];
188 + var fragment = ReactFragment.extract(children);
189 + for (var key in fragment) {
190 + if (fragment.hasOwnProperty(key)) {
191 + child = fragment[key];
192 nextName = (
193 (nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) +
194 wrapUserProvidedKey(key) + SUBSEPARATOR +