Merge pull request #3030 from sebmarkbage/fragments

Make keyed object fragments an opaque type
This commit is contained in:
Sebastian Markbåge committed 2015-02-09 14:50:46 -08:00
commit 0fdd2a74e2
19 files changed
+466 -118

No files matched your search

+164
View File
@@ -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(
+2
View File
@@ -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
};
+19 -19
View File
@@ -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];
+13 -25
View File
@@ -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');
});
});
+9 -3
View File
@@ -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>
+13 -10
View File
@@ -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(
+2 -1
View File
@@ -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) {
+99 -34
View File
@@ -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');
});
});
+3 -1
View File
@@ -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();
+11 -4
View File
@@ -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>
);
+3 -1
View File
@@ -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;
+5 -3
View File
@@ -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 +