mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #3030 from sebmarkbage/fragments
Make keyed object fragments an opaque type
This commit is contained in:
commit
0fdd2a74e2
19 files changed
+466
-118
No files matched your search
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright 2015, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule ReactFragment
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var ReactElement = require('ReactElement');
|
||||
|
||||
var warning = require('warning');
|
||||
|
||||
/**
|
||||
* We used to allow keyed objects to serve as a collection of ReactElements,
|
||||
* or nested sets. This allowed us a way to explicitly key a set a fragment of
|
||||
* components. This is now being replaced with an opaque data structure.
|
||||
* The upgrade path is to call React.addons.createFragment({ key: value }) to
|
||||
* create a keyed fragment. The resulting data structure is opaque, for now.
|
||||
*/
|
||||
|
||||
if (__DEV__) {
|
||||
var fragmentKey = '_reactFragment';
|
||||
var didWarnKey = '_reactDidWarn';
|
||||
var canWarnForReactFragment = false;
|
||||
|
||||
try {
|
||||
// Feature test. Don't even try to issue this warning if we can't use
|
||||
// enumerable: false.
|
||||
|
||||
var dummy = function() {
|
||||
return 1;
|
||||
};
|
||||
|
||||
Object.defineProperty(
|
||||
{},
|
||||
fragmentKey,
|
||||
{ enumerable: false, value: true }
|
||||
);
|
||||
|
||||
Object.defineProperty(
|
||||
{},
|
||||
'key',
|
||||
{ enumerable: true, get: dummy }
|
||||
);
|
||||
|
||||
canWarnForReactFragment = true;
|
||||
} catch (x) { }
|
||||
|
||||
var proxyPropertyAccessWithWarning = function(obj, key) {
|
||||
Object.defineProperty(obj, key, {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
warning(
|
||||
this[didWarnKey],
|
||||
'A ReactFragment is an opaque type. Accessing any of its ' +
|
||||
'properties is deprecated. Pass it to one of the React.Children ' +
|
||||
'helpers.'
|
||||
);
|
||||
this[didWarnKey] = true;
|
||||
return this[fragmentKey][key];
|
||||
},
|
||||
set: function(value) {
|
||||
warning(
|
||||
this[didWarnKey],
|
||||
'A ReactFragment is an immutable opaque type. Mutating its ' +
|
||||
'properties is deprecated.'
|
||||
);
|
||||
this[didWarnKey] = true;
|
||||
this[fragmentKey][key] = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var issuedWarnings = {};
|
||||
|
||||
var didWarnForFragment = function(fragment) {
|
||||
// We use the keys and the type of the value as a heuristic to dedupe the
|
||||
// warning to avoid spamming too much.
|
||||
var fragmentCacheKey = '';
|
||||
for (var key in fragment) {
|
||||
fragmentCacheKey += key + ':' + (typeof fragment[key]) + ',';
|
||||
}
|
||||
var alreadyWarnedOnce = !!issuedWarnings[fragmentCacheKey];
|
||||
issuedWarnings[fragmentCacheKey] = true;
|
||||
return alreadyWarnedOnce;
|
||||
};
|
||||
}
|
||||
|
||||
var ReactFragment = {
|
||||
// Wrap a keyed object in an opaque proxy that warns you if you access any
|
||||
// of its properties.
|
||||
create: function(object) {
|
||||
if (__DEV__) {
|
||||
if (canWarnForReactFragment) {
|
||||
var proxy = {};
|
||||
Object.defineProperty(proxy, fragmentKey, {
|
||||
enumerable: false,
|
||||
value: object
|
||||
});
|
||||
Object.defineProperty(proxy, didWarnKey, {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
value: false
|
||||
});
|
||||
for (var key in object) {
|
||||
proxyPropertyAccessWithWarning(proxy, key);
|
||||
}
|
||||
Object.preventExtensions(proxy);
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
return object;
|
||||
},
|
||||
// Extract the original keyed object from the fragment opaque type. Warn if
|
||||
// a plain object is passed here.
|
||||
extract: function(fragment) {
|
||||
if (__DEV__) {
|
||||
if (canWarnForReactFragment) {
|
||||
if (!fragment[fragmentKey]) {
|
||||
warning(
|
||||
didWarnForFragment(fragment),
|
||||
'Any use of a keyed object should be wrapped in ' +
|
||||
'React.addons.createFragment(object) before passed as a child.'
|
||||
);
|
||||
return fragment;
|
||||
}
|
||||
return fragment[fragmentKey];
|
||||
}
|
||||
}
|
||||
return fragment;
|
||||
},
|
||||
// Check if this is a fragment and if so, extract the keyed object. If it
|
||||
// is a fragment-like object, warn that it should be wrapped. Ignore if we
|
||||
// can't determine what kind of object this is.
|
||||
extractIfFragment: function(fragment) {
|
||||
if (__DEV__) {
|
||||
if (canWarnForReactFragment) {
|
||||
// If it is the opaque type, return the keyed object.
|
||||
if (fragment[fragmentKey]) {
|
||||
return fragment[fragmentKey];
|
||||
}
|
||||
// Otherwise, check each property if it has an element, if it does
|
||||
// it is probably meant as a fragment, so we can warn early. Defer,
|
||||
// the warning to extract.
|
||||
for (var key in fragment) {
|
||||
if (fragment.hasOwnProperty(key) &&
|
||||
ReactElement.isValidElement(fragment[key])) {
|
||||
// This looks like a fragment object, we should provide an
|
||||
// early warning.
|
||||
return ReactFragment.extract(fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ReactFragment;
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright 2015, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React;
|
||||
var ReactFragment;
|
||||
|
||||
describe('ReactFragment', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
});
|
||||
|
||||
it('should warn if a plain object is used as a child', function() {
|
||||
spyOn(console, 'warn');
|
||||
var children = {
|
||||
x: <span />,
|
||||
y: <span />
|
||||
};
|
||||
<div>{children}</div>;
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
expect(console.warn.calls[0].args[0]).toContain(
|
||||
'Any use of a keyed object'
|
||||
);
|
||||
// Only warn once for the same set of children
|
||||
var sameChildren = {
|
||||
x: <span />,
|
||||
y: <span />
|
||||
};
|
||||
<div>{sameChildren}</div>;
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should warn if a plain object even if it is deep', function() {
|
||||
spyOn(console, 'warn');
|
||||
var children = {
|
||||
x: <span />,
|
||||
y: <span />,
|
||||
z: <span />
|
||||
};
|
||||
var element = <div>{[children]}</div>;
|
||||
expect(console.warn.calls.length).toBe(0);
|
||||
var container = document.createElement('div');
|
||||
React.render(element, container);
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
expect(console.warn.calls[0].args[0]).toContain(
|
||||
'Any use of a keyed object'
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn if accessing any property on a fragment', function() {
|
||||
spyOn(console, 'warn');
|
||||
var children = {
|
||||
x: <span />,
|
||||
y: <span />
|
||||
};
|
||||
var frag = ReactFragment.create(children);
|
||||
frag.x;
|
||||
frag.y = 10;
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
expect(console.warn.calls[0].args[0]).toContain(
|
||||
'A ReactFragment is an opaque type'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -13,6 +13,7 @@
|
||||
'use strict';
|
||||
|
||||
var ReactChildren = require('ReactChildren');
|
||||
var ReactFragment = require('ReactFragment');
|
||||
|
||||
var ReactTransitionChildMapping = {
|
||||
/**
|
||||
@@ -23,9 +24,12 @@ var ReactTransitionChildMapping = {
|
||||
* @return {object} Mapping of key to child
|
||||
*/
|
||||
getChildMapping: function(children) {
|
||||
return ReactChildren.map(children, function(child) {
|
||||
if (!children) {
|
||||
return children;
|
||||
}
|
||||
return ReactFragment.extract(ReactChildren.map(children, function(child) {
|
||||
return child;
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -202,7 +202,7 @@ var ReactTransitionGroup = React.createClass({
|
||||
render: function() {
|
||||
// TODO: we could get rid of the need for the wrapper node
|
||||
// by cloning a single child
|
||||
var childrenToRender = {};
|
||||
var childrenToRender = [];
|
||||
for (var key in this.state.children) {
|
||||
var child = this.state.children[key];
|
||||
if (child) {
|
||||
@@ -211,10 +211,10 @@ var ReactTransitionGroup = React.createClass({
|
||||
// already been removed. In case you need this behavior you can provide
|
||||
// a childFactory function to wrap every child, even the ones that are
|
||||
// leaving.
|
||||
childrenToRender[key] = cloneWithProps(
|
||||
childrenToRender.push(cloneWithProps(
|
||||
this.props.childFactory(child),
|
||||
{ref: key}
|
||||
);
|
||||
{ref: key, key: key}
|
||||
));
|
||||
}
|
||||
}
|
||||
return React.createElement(
|
||||
|
||||
@@ -23,6 +23,7 @@ var React = require('React');
|
||||
var ReactComponentWithPureRenderMixin =
|
||||
require('ReactComponentWithPureRenderMixin');
|
||||
var ReactCSSTransitionGroup = require('ReactCSSTransitionGroup');
|
||||
var ReactFragment = require('ReactFragment');
|
||||
var ReactTransitionGroup = require('ReactTransitionGroup');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
|
||||
@@ -39,6 +40,7 @@ React.addons = {
|
||||
batchedUpdates: ReactUpdates.batchedUpdates,
|
||||
classSet: cx,
|
||||
cloneWithProps: cloneWithProps,
|
||||
createFragment: ReactFragment.create,
|
||||
update: update
|
||||
};
|
||||
|
||||
|
||||
@@ -75,37 +75,37 @@ describe('ReactDOM', function() {
|
||||
*/
|
||||
it("should purge the DOM cache when removing nodes", function() {
|
||||
var myDiv = ReactTestUtils.renderIntoDocument(
|
||||
<div>{{
|
||||
theDog: <div className="dog" />,
|
||||
theBird: <div className="bird" />
|
||||
}}</div>
|
||||
<div>
|
||||
<div key="theDog" className="dog" />,
|
||||
<div key="theBird" className="bird" />
|
||||
</div>
|
||||
);
|
||||
// Warm the cache with theDog
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: <div className="dogbeforedelete" />,
|
||||
theBird: <div className="bird" />
|
||||
}
|
||||
children: [
|
||||
<div key="theDog" className="dogbeforedelete" />,
|
||||
<div key="theBird" className="bird" />
|
||||
]
|
||||
});
|
||||
// Remove theDog - this should purge the cache
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theBird: <div className="bird" />
|
||||
}
|
||||
children: [
|
||||
<div key="theBird" className="bird" />
|
||||
]
|
||||
});
|
||||
// Now, put theDog back. It's now a different DOM node.
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: <div className="dog" />,
|
||||
theBird: <div className="bird" />
|
||||
}
|
||||
children: [
|
||||
<div key="theDog" className="dog" />,
|
||||
<div key="theBird" className="bird" />
|
||||
]
|
||||
});
|
||||
// Change the className of theDog. It will use the same element
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: <div className="bigdog" />,
|
||||
theBird: <div className="bird" />
|
||||
}
|
||||
children: [
|
||||
<div key="theDog" className="bigdog" />,
|
||||
<div key="theBird" className="bird" />
|
||||
]
|
||||
});
|
||||
var root = myDiv.getDOMNode();
|
||||
var dog = root.childNodes[0];
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
'use strict';
|
||||
|
||||
var ReactElement = require('ReactElement');
|
||||
var ReactFragment = require('ReactFragment');
|
||||
var ReactPropTypeLocations = require('ReactPropTypeLocations');
|
||||
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
|
||||
var ReactCurrentOwner = require('ReactCurrentOwner');
|
||||
@@ -175,21 +176,6 @@ function warnAndMonitorForKeyUse(warningID, message, element, parentType) {
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log that we're using an object map. We're considering deprecating this
|
||||
* feature and replace it with proper Map and ImmutableMap data structures.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function monitorUseOfObjectMap() {
|
||||
var currentName = getCurrentOwnerDisplayName() || '';
|
||||
if (ownerHasMonitoredObjectMap.hasOwnProperty(currentName)) {
|
||||
return;
|
||||
}
|
||||
ownerHasMonitoredObjectMap[currentName] = true;
|
||||
monitorCodeUse('react_object_map_children');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that every element either is passed in a static location, in an
|
||||
* array with an explicit keys property defined, or in an object literal
|
||||
@@ -213,19 +199,21 @@ function validateChildKeys(node, parentType) {
|
||||
} else if (node) {
|
||||
var iteratorFn = getIteratorFn(node);
|
||||
// Entry iterators provide implicit keys.
|
||||
if (iteratorFn && iteratorFn !== node.entries) {
|
||||
var iterator = iteratorFn.call(node);
|
||||
var step;
|
||||
while (!(step = iterator.next()).done) {
|
||||
if (ReactElement.isValidElement(step.value)) {
|
||||
validateExplicitKey(step.value, parentType);
|
||||
if (iteratorFn) {
|
||||
if (iteratorFn !== node.entries) {
|
||||
var iterator = iteratorFn.call(node);
|
||||
var step;
|
||||
while (!(step = iterator.next()).done) {
|
||||
if (ReactElement.isValidElement(step.value)) {
|
||||
validateExplicitKey(step.value, parentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof node === 'object') {
|
||||
monitorUseOfObjectMap();
|
||||
for (var key in node) {
|
||||
if (node.hasOwnProperty(key)) {
|
||||
validatePropertyKey(key, node[key], parentType);
|
||||
var fragment = ReactFragment.extractIfFragment(node);
|
||||
for (var key in fragment) {
|
||||
if (fragment.hasOwnProperty(key)) {
|
||||
validatePropertyKey(key, fragment[key], parentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// classic JS without JSX.
|
||||
|
||||
var React;
|
||||
var ReactFragment;
|
||||
var ReactTestUtils;
|
||||
|
||||
describe('ReactElementValidator', function() {
|
||||
@@ -24,12 +25,17 @@ describe('ReactElementValidator', function() {
|
||||
require('mock-modules').dumpCache();
|
||||
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
ComponentClass = React.createClass({
|
||||
render: function() { return React.createElement('div'); }
|
||||
});
|
||||
});
|
||||
|
||||
function frag(obj) {
|
||||
return ReactFragment.create(obj);
|
||||
}
|
||||
|
||||
it('warns for keys for arrays of elements in rest args', function() {
|
||||
spyOn(console, 'warn');
|
||||
var Component = React.createFactory(ComponentClass);
|
||||
@@ -135,7 +141,7 @@ describe('ReactElementValidator', function() {
|
||||
spyOn(console, 'warn');
|
||||
var Component = React.createFactory(ComponentClass);
|
||||
|
||||
Component(null, { 1: Component(), 2: Component() });
|
||||
Component(null, frag({ 1: Component(), 2: Component() }));
|
||||
|
||||
expect(console.warn.argsForCall.length).toBe(1);
|
||||
expect(console.warn.argsForCall[0][0]).toContain(
|
||||
@@ -316,4 +322,12 @@ describe('ReactElementValidator', function() {
|
||||
expect(console.warn.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should warn if a fragment is used without the wrapper', function () {
|
||||
spyOn(console, 'warn');
|
||||
var child = React.createElement('span');
|
||||
React.createElement('div', null, { a: child, b: child });
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
expect(console.warn.calls[0].args[0]).toContain('use of a keyed object');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
'use strict';
|
||||
|
||||
var React;
|
||||
var ReactFragment;
|
||||
var ReactTestUtils;
|
||||
var reactComponentExpect;
|
||||
var ReactMount;
|
||||
@@ -21,6 +22,7 @@ describe('ReactIdentity', function() {
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
reactComponentExpect = require('reactComponentExpect');
|
||||
ReactMount = require('ReactMount');
|
||||
@@ -35,13 +37,17 @@ describe('ReactIdentity', function() {
|
||||
expect(actual[1]).toEqual(expected[1]);
|
||||
}
|
||||
|
||||
function frag(obj) {
|
||||
return ReactFragment.create(obj);
|
||||
}
|
||||
|
||||
it('should allow keyed objects to express identity', function() {
|
||||
var instance =
|
||||
<div>
|
||||
{{
|
||||
{frag({
|
||||
first: <div />,
|
||||
second: <div />
|
||||
}}
|
||||
})}
|
||||
</div>;
|
||||
|
||||
instance = React.render(instance, document.createElement('div'));
|
||||
@@ -106,7 +112,7 @@ describe('ReactIdentity', function() {
|
||||
|
||||
var map = {};
|
||||
map[key] = span2;
|
||||
return <div>{[span1, map]}</div>;
|
||||
return <div>{[span1, frag(map)]}</div>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ var stripEmptyValues = function(obj) {
|
||||
* here. This relies on an implementation detail of the rendering system.
|
||||
*/
|
||||
var getOriginalKey = function(childName) {
|
||||
var match = childName.match(/^\.\$([^.]+)\:0$/);
|
||||
var match = childName.match(/^\.\$([^.]+)$/);
|
||||
expect(match).not.toBeNull();
|
||||
return match[1];
|
||||
};
|
||||
@@ -107,13 +107,14 @@ var FriendsStatusDisplay = React.createClass({
|
||||
return res;
|
||||
},
|
||||
render: function() {
|
||||
var children = null;
|
||||
var children = [];
|
||||
var key;
|
||||
for (key in this.props.usernameToStatus) {
|
||||
var status = this.props.usernameToStatus[key];
|
||||
children = children || {};
|
||||
children[key] = !status ? null :
|
||||
<StatusDisplay ref={key} status={status} />;
|
||||
children.push(
|
||||
!status ? null :
|
||||
<StatusDisplay key={key} ref={key} status={status} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
require('mock-modules');
|
||||
|
||||
var React = require('React');
|
||||
var ReactFragment = require('ReactFragment');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
|
||||
var reactComponentExpect = require('reactComponentExpect');
|
||||
|
||||
var frag = ReactFragment.create;
|
||||
|
||||
// Helpers
|
||||
var testAllPermutations = function(testCases) {
|
||||
for (var i = 0; i < testCases.length; i += 2) {
|
||||
@@ -182,18 +185,18 @@ describe('ReactMultiChildText', function() {
|
||||
['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
|
||||
|
||||
// values inside objects
|
||||
[{a: true}, {a: true}], [],
|
||||
[{a: 1.2}, {a: 1.2}], ['1.2', '1.2'],
|
||||
[{a: ''}, {a: ''}], ['', ''],
|
||||
[{a: 'foo'}, {a: 'foo'}], ['foo', 'foo'],
|
||||
[{a: <div />}, {a: <div />}], [<div />, <div />],
|
||||
[frag({a: true}), frag({a: true})], [],
|
||||
[frag({a: 1.2}), frag({a: 1.2})], ['1.2', '1.2'],
|
||||
[frag({a: ''}), frag({a: ''})], ['', ''],
|
||||
[frag({a: 'foo'}), frag({a: 'foo'})], ['foo', 'foo'],
|
||||
[frag({a: <div />}), frag({a: <div />})], [<div />, <div />],
|
||||
|
||||
[{a: true, b: 1.2, c: <div />}, '', 'foo'], ['1.2', <div />, '', 'foo'],
|
||||
[1.2, '', {a: <div />, b: 'foo', c: true}], ['1.2', '', <div />, 'foo'],
|
||||
['', {a: 'foo', b: <div />, c: true}, 1.2], ['', 'foo', <div />, '1.2'],
|
||||
[frag({a: true, b: 1.2, c: <div />}), '', 'foo'], ['1.2', <div />, '', 'foo'],
|
||||
[1.2, '', frag({a: <div />, b: 'foo', c: true})], ['1.2', '', <div />, 'foo'],
|
||||
['', frag({a: 'foo', b: <div />, c: true}), 1.2], ['', 'foo', <div />, '1.2'],
|
||||
|
||||
[true, {a: 1.2, b: '', c: <div />, d: 'foo'}, true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
|
||||
['', 'foo', {a: true, b: <div />, c: 1.2, d: ''}, 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
|
||||
[true, frag({a: 1.2, b: '', c: <div />, d: 'foo'}), true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
|
||||
['', 'foo', frag({a: true, b: <div />, c: 1.2, d: ''}), 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
|
||||
|
||||
// values inside elements
|
||||
[<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// of dynamic errors when using JSX with Flow.
|
||||
|
||||
var React;
|
||||
var ReactFragment;
|
||||
var ReactTestUtils;
|
||||
|
||||
describe('ReactJSXElementValidator', function() {
|
||||
@@ -24,6 +25,7 @@ describe('ReactJSXElementValidator', function() {
|
||||
require('mock-modules').dumpCache();
|
||||
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
|
||||
Component = class {
|
||||
@@ -31,6 +33,10 @@ describe('ReactJSXElementValidator', function() {
|
||||
};
|
||||
});
|
||||
|
||||
function frag(obj) {
|
||||
return ReactFragment.create(obj);
|
||||
}
|
||||
|
||||
it('warns for keys for arrays of elements in children position', function() {
|
||||
spyOn(console, 'warn');
|
||||
|
||||
@@ -128,7 +134,7 @@ describe('ReactJSXElementValidator', function() {
|
||||
it('warns for numeric keys on objects as children', function() {
|
||||
spyOn(console, 'warn');
|
||||
|
||||
<Component>{{ 1: <Component />, 2: <Component /> }}</Component>;
|
||||
<Component>{frag({ 1: <Component />, 2: <Component /> })}</Component>;
|
||||
|
||||
expect(console.warn.argsForCall.length).toBe(1);
|
||||
expect(console.warn.argsForCall[0][0]).toContain(
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
'use strict';
|
||||
|
||||
var PooledClass = require('PooledClass');
|
||||
var ReactFragment = require('ReactFragment');
|
||||
|
||||
var traverseAllChildren = require('traverseAllChildren');
|
||||
var warning = require('warning');
|
||||
@@ -121,7 +122,7 @@ function mapChildren(children, func, context) {
|
||||
var traverseContext = MapBookKeeping.getPooled(mapResult, func, context);
|
||||
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
|
||||
MapBookKeeping.release(traverseContext);
|
||||
return mapResult;
|
||||
return ReactFragment.create(mapResult);
|
||||
}
|
||||
|
||||
function forEachSingleChildDummy(traverseContext, child, name, i) {
|
||||
|
||||
@@ -14,12 +14,31 @@
|
||||
describe('ReactChildren', function() {
|
||||
var ReactChildren;
|
||||
var React;
|
||||
var ReactFragment;
|
||||
|
||||
beforeEach(function() {
|
||||
ReactChildren = require('ReactChildren');
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
});
|
||||
|
||||
function frag(obj) {
|
||||
return ReactFragment.create(obj);
|
||||
}
|
||||
|
||||
function nthChild(mappedChildren, n) {
|
||||
var result = null;
|
||||
ReactChildren.forEach(mappedChildren, function(child, index) {
|
||||
if (index === n) {
|
||||
result = child;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function keyOfNthChild(mappedChildren, n) {
|
||||
return Object.keys(ReactFragment.extract(mappedChildren))[n];
|
||||
}
|
||||
|
||||
it('should support identity for simple', function() {
|
||||
var callback = jasmine.createSpy().andCallFake(function(kid, index) {
|
||||
@@ -37,7 +56,7 @@ describe('ReactChildren', function() {
|
||||
callback.reset();
|
||||
var mappedChildren = ReactChildren.map(instance.props.children, callback);
|
||||
expect(callback).toHaveBeenCalledWith(simpleKid, 0);
|
||||
expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
|
||||
expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
|
||||
});
|
||||
|
||||
it('should treat single arrayless child as being in array', function() {
|
||||
@@ -52,7 +71,7 @@ describe('ReactChildren', function() {
|
||||
callback.reset();
|
||||
var mappedChildren = ReactChildren.map(instance.props.children, callback);
|
||||
expect(callback).toHaveBeenCalledWith(simpleKid, 0);
|
||||
expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
|
||||
expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
|
||||
});
|
||||
|
||||
it('should treat single child in array as expected', function() {
|
||||
@@ -67,7 +86,7 @@ describe('ReactChildren', function() {
|
||||
callback.reset();
|
||||
var mappedChildren = ReactChildren.map(instance.props.children, callback);
|
||||
expect(callback).toHaveBeenCalledWith(simpleKid, 0);
|
||||
expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
|
||||
expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
|
||||
});
|
||||
|
||||
it('should pass key to returned component', function() {
|
||||
@@ -80,11 +99,10 @@ describe('ReactChildren', function() {
|
||||
var instance = <div>{simpleKid}</div>;
|
||||
var mappedChildren = ReactChildren.map(instance.props.children, mapFn);
|
||||
|
||||
var mappedKeys = Object.keys(mappedChildren);
|
||||
expect(mappedKeys.length).toBe(1);
|
||||
expect(mappedChildren[mappedKeys[0]]).not.toBe(simpleKid);
|
||||
expect(mappedChildren[mappedKeys[0]].props.children).toBe(simpleKid);
|
||||
expect(mappedKeys[0]).toBe('.$simple');
|
||||
expect(ReactChildren.count(mappedChildren)).toBe(1);
|
||||
expect(nthChild(mappedChildren, 0)).not.toBe(simpleKid);
|
||||
expect(nthChild(mappedChildren, 0).props.children).toBe(simpleKid);
|
||||
expect(keyOfNthChild(mappedChildren, 0)).toBe('.$simple');
|
||||
});
|
||||
|
||||
it('should invoke callback with the right context', function() {
|
||||
@@ -94,7 +112,8 @@ describe('ReactChildren', function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
var scopeTester = {};
|
||||
// TODO: Use an object to test, after non-object fragments has fully landed.
|
||||
var scopeTester = 'scope tester';
|
||||
|
||||
var simpleKid = <span key="simple" />;
|
||||
var instance = <div>{simpleKid}</div>;
|
||||
@@ -104,9 +123,8 @@ describe('ReactChildren', function() {
|
||||
var mappedChildren =
|
||||
ReactChildren.map(instance.props.children, callback, scopeTester);
|
||||
|
||||
var mappedKeys = Object.keys(mappedChildren);
|
||||
expect(mappedKeys.length).toBe(1);
|
||||
expect(mappedChildren[mappedKeys[0]]).toBe(scopeTester);
|
||||
expect(ReactChildren.count(mappedChildren)).toBe(1);
|
||||
expect(nthChild(mappedChildren, 0)).toBe(scopeTester);
|
||||
});
|
||||
|
||||
it('should be called for each child', function() {
|
||||
@@ -149,28 +167,33 @@ describe('ReactChildren', function() {
|
||||
|
||||
var mappedChildren =
|
||||
ReactChildren.map(instance.props.children, callback);
|
||||
var mappedKeys = Object.keys(mappedChildren);
|
||||
expect(callback.calls.length).toBe(5);
|
||||
expect(mappedKeys.length).toBe(5);
|
||||
expect(ReactChildren.count(mappedChildren)).toBe(5);
|
||||
// Keys default to indices.
|
||||
expect(mappedKeys).toEqual(
|
||||
expect([
|
||||
keyOfNthChild(mappedChildren, 0),
|
||||
keyOfNthChild(mappedChildren, 1),
|
||||
keyOfNthChild(mappedChildren, 2),
|
||||
keyOfNthChild(mappedChildren, 3),
|
||||
keyOfNthChild(mappedChildren, 4)
|
||||
]).toEqual(
|
||||
['.$keyZero', '.1', '.$keyTwo', '.3', '.$keyFour']
|
||||
);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(zero, 0);
|
||||
expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
|
||||
expect(nthChild(mappedChildren, 0)).toBe(zeroMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(one, 1);
|
||||
expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
|
||||
expect(nthChild(mappedChildren, 1)).toBe(oneMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(two, 2);
|
||||
expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
|
||||
expect(nthChild(mappedChildren, 2)).toBe(twoMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(three, 3);
|
||||
expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
|
||||
expect(nthChild(mappedChildren, 3)).toBe(threeMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(four, 4);
|
||||
expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
|
||||
expect(nthChild(mappedChildren, 4)).toBe(fourMapped);
|
||||
});
|
||||
|
||||
|
||||
@@ -204,11 +227,11 @@ describe('ReactChildren', function() {
|
||||
|
||||
var instance = (
|
||||
<div>{
|
||||
[{
|
||||
[frag({
|
||||
firstHalfKey: [zero, one, two],
|
||||
secondHalfKey: [three, four],
|
||||
keyFive: five
|
||||
}]
|
||||
})]
|
||||
}</div>
|
||||
);
|
||||
|
||||
@@ -222,11 +245,17 @@ describe('ReactChildren', function() {
|
||||
callback.reset();
|
||||
|
||||
var mappedChildren = ReactChildren.map(instance.props.children, callback);
|
||||
var mappedKeys = Object.keys(mappedChildren);
|
||||
expect(callback.calls.length).toBe(6);
|
||||
expect(mappedKeys.length).toBe(6);
|
||||
expect(ReactChildren.count(mappedChildren)).toBe(6);
|
||||
// Keys default to indices.
|
||||
expect(mappedKeys).toEqual([
|
||||
expect([
|
||||
keyOfNthChild(mappedChildren, 0),
|
||||
keyOfNthChild(mappedChildren, 1),
|
||||
keyOfNthChild(mappedChildren, 2),
|
||||
keyOfNthChild(mappedChildren, 3),
|
||||
keyOfNthChild(mappedChildren, 4),
|
||||
keyOfNthChild(mappedChildren, 5)
|
||||
]).toEqual([
|
||||
'.0:$firstHalfKey:0:$keyZero',
|
||||
'.0:$firstHalfKey:0:1',
|
||||
'.0:$firstHalfKey:0:$keyTwo',
|
||||
@@ -236,22 +265,22 @@ describe('ReactChildren', function() {
|
||||
]);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(zero, 0);
|
||||
expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
|
||||
expect(nthChild(mappedChildren, 0)).toBe(zeroMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(one, 1);
|
||||
expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
|
||||
expect(nthChild(mappedChildren, 1)).toBe(oneMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(two, 2);
|
||||
expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
|
||||
expect(nthChild(mappedChildren, 2)).toBe(twoMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(three, 3);
|
||||
expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
|
||||
expect(nthChild(mappedChildren, 3)).toBe(threeMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(four, 4);
|
||||
expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
|
||||
expect(nthChild(mappedChildren, 4)).toBe(fourMapped);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(five, 5);
|
||||
expect(mappedChildren[mappedKeys[5]]).toBe(fiveMapped);
|
||||
expect(nthChild(mappedChildren, 5)).toBe(fiveMapped);
|
||||
});
|
||||
|
||||
it('should retain key across two mappings', function() {
|
||||
@@ -325,7 +354,8 @@ describe('ReactChildren', function() {
|
||||
var mapped = ReactChildren.map(instance.props.children, mapFn);
|
||||
|
||||
expect(console.warn.calls.length).toEqual(1);
|
||||
expect(mapped).toEqual({'.$something': zero});
|
||||
expect(nthChild(mapped, 0)).toBe(zero);
|
||||
expect(keyOfNthChild(mapped, 0)).toBe('.$something');
|
||||
});
|
||||
|
||||
it('should return 0 for null children', function() {
|
||||
@@ -380,14 +410,49 @@ describe('ReactChildren', function() {
|
||||
|
||||
var instance = (
|
||||
<div>{
|
||||
[{
|
||||
[frag({
|
||||
firstHalfKey: [zero, one, two],
|
||||
secondHalfKey: [three, four],
|
||||
keyFive: five
|
||||
}]
|
||||
})]
|
||||
}</div>
|
||||
);
|
||||
var numberOfChildren = ReactChildren.count(instance.props.children);
|
||||
expect(numberOfChildren).toBe(6);
|
||||
});
|
||||
|
||||
it('should warn if a fragment is used without the wrapper', function () {
|
||||
spyOn(console, 'warn');
|
||||
var child = React.createElement('span');
|
||||
ReactChildren.forEach({ a: child, b: child}, function(c) {
|
||||
expect(c).toBe(child);
|
||||
});
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
expect(console.warn.calls[0].args[0]).toContain('use of a keyed object');
|
||||
});
|
||||
|
||||
it('should warn if a fragment is accessed', function () {
|
||||
spyOn(console, 'warn');
|
||||
var child = React.createElement('span');
|
||||
var frag = ReactChildren.map([ child, child ], function(c) {
|
||||
return c;
|
||||
});
|
||||
for (var key in frag) {
|
||||
frag[key];
|
||||
break;
|
||||
}
|
||||
expect(console.warn.calls.length).toBe(1);
|
||||
expect(console.warn.calls[0].args[0]).toContain('is an opaque type');
|
||||
|
||||
var frag2 = ReactChildren.map([ child, child ], function(c) {
|
||||
return c;
|
||||
});
|
||||
for (var key in frag2) {
|
||||
frag2[key] = 123;
|
||||
break;
|
||||
}
|
||||
expect(console.warn.calls.length).toBe(2);
|
||||
expect(console.warn.calls[1].args[0]).toContain('is an immutable opaque');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -14,11 +14,13 @@
|
||||
describe('onlyChild', function() {
|
||||
|
||||
var React;
|
||||
var ReactFragment;
|
||||
var onlyChild;
|
||||
var WrapComponent;
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
onlyChild = require('onlyChild');
|
||||
WrapComponent = React.createClass({
|
||||
render: function() {
|
||||
@@ -64,7 +66,7 @@ describe('onlyChild', function() {
|
||||
expect(function() {
|
||||
var instance =
|
||||
<WrapComponent>
|
||||
{{oneThing: <span />}}
|
||||
{ReactFragment.create({oneThing: <span />})}
|
||||
</WrapComponent>;
|
||||
onlyChild(instance.props.children);
|
||||
}).toThrow();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
describe('sliceChildren', function() {
|
||||
|
||||
var React;
|
||||
var ReactFragment;
|
||||
var ReactTestUtils;
|
||||
|
||||
var sliceChildren;
|
||||
@@ -23,6 +24,7 @@ describe('sliceChildren', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
|
||||
sliceChildren = require('sliceChildren');
|
||||
@@ -52,6 +54,11 @@ describe('sliceChildren', function() {
|
||||
return rendered.props.children;
|
||||
}
|
||||
|
||||
function testKeyValuePairs(children, expectedPairs) {
|
||||
var obj = ReactFragment.extract(children);
|
||||
expect(obj).toEqual(expectedPairs);
|
||||
}
|
||||
|
||||
it('should render the whole set if start zero is supplied', function() {
|
||||
var fullSet = [
|
||||
<div key="A" />,
|
||||
@@ -59,7 +66,7 @@ describe('sliceChildren', function() {
|
||||
<div key="C" />
|
||||
];
|
||||
var children = renderAndSlice(fullSet, 0);
|
||||
expect(children).toEqual({
|
||||
testKeyValuePairs(children, {
|
||||
'.$A': fullSet[0],
|
||||
'.$B': fullSet[1],
|
||||
'.$C': fullSet[2]
|
||||
@@ -73,7 +80,7 @@ describe('sliceChildren', function() {
|
||||
<div key="C" />
|
||||
];
|
||||
var children = renderAndSlice(fullSet, 1);
|
||||
expect(children).toEqual({
|
||||
testKeyValuePairs(children, {
|
||||
'.$B': fullSet[1],
|
||||
'.$C': fullSet[2]
|
||||
});
|
||||
@@ -87,7 +94,7 @@ describe('sliceChildren', function() {
|
||||
<div key="D" />
|
||||
];
|
||||
var children = renderAndSlice(fullSet, 1, 2);
|
||||
expect(children).toEqual({
|
||||
testKeyValuePairs(children, {
|
||||
'.$B': fullSet[1]
|
||||
});
|
||||
});
|
||||
@@ -103,7 +110,7 @@ describe('sliceChildren', function() {
|
||||
.expectRenderedChild()
|
||||
.instance();
|
||||
|
||||
expect(rendered.props.children).toEqual({
|
||||
testKeyValuePairs(rendered.props.children, {
|
||||
'.1': b
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
describe('traverseAllChildren', function() {
|
||||
var traverseAllChildren;
|
||||
var React;
|
||||
var ReactFragment;
|
||||
beforeEach(function() {
|
||||
traverseAllChildren = require('traverseAllChildren');
|
||||
React = require('React');
|
||||
ReactFragment = require('ReactFragment');
|
||||
});
|
||||
|
||||
function frag(obj) {
|
||||
return ReactFragment.create(obj);
|
||||
}
|
||||
|
||||
it('should support identity for simple', function() {
|
||||
var traverseContext = [];
|
||||
@@ -139,8 +144,8 @@ describe('traverseAllChildren', function() {
|
||||
var instance = (
|
||||
<div>
|
||||
{div}
|
||||
{[{span}]}
|
||||
{{a: a}}
|
||||
{[frag({span})]}
|
||||
{frag({a: a})}
|
||||
{'string'}
|
||||
{1234}
|
||||
{true}
|
||||
@@ -206,11 +211,11 @@ describe('traverseAllChildren', function() {
|
||||
|
||||
var instance = (
|
||||
<div>{
|
||||
[{
|
||||
[frag({
|
||||
firstHalfKey: [zero, one, two],
|
||||
secondHalfKey: [three, four],
|
||||
keyFive: five
|
||||
}]
|
||||
})]
|
||||
}</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var ReactFragment = require('ReactFragment');
|
||||
|
||||
var flattenChildren = require('flattenChildren');
|
||||
|
||||
/**
|
||||
@@ -43,7 +45,7 @@ function sliceChildren(children, start, end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return slicedChildren;
|
||||
return ReactFragment.create(slicedChildren);
|
||||
}
|
||||
|
||||
module.exports = sliceChildren;
|
||||
@@ -12,6 +12,7 @@
|
||||
'use strict';
|
||||
|
||||
var ReactElement = require('ReactElement');
|
||||
var ReactFragment = require('ReactFragment');
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
|
||||
var getIteratorFn = require('getIteratorFn');
|
||||
@@ -184,9 +185,10 @@ function traverseAllChildrenImpl(
|
||||
'traverseAllChildren(...): Encountered an invalid child; DOM ' +
|
||||
'elements are not valid children of React components.'
|
||||
);
|
||||
for (var key in children) {
|
||||
if (children.hasOwnProperty(key)) {
|
||||
child = children[key];
|
||||
var fragment = ReactFragment.extract(children);
|
||||
for (var key in fragment) {
|
||||
if (fragment.hasOwnProperty(key)) {
|
||||
child = fragment[key];
|
||||
nextName = (
|
||||
(nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) +
|
||||
wrapUserProvidedKey(key) + SUBSEPARATOR +
|
||||
|
||||
Reference in new issue
Block a user