Add cloneElement Implementation

This is a new version of cloneWithProps but this one is moving out of
add-ons. Unlike cloneWithProps, this one doesn't have special logic for
style, className and children.

This one also preserves the original ref. This is critical when upgrading
from a mutative pattern where a child might have a ref on it.

It also preserves context, which is similar to how context would work when
it is parent based. It also ensures that we're compatible with the old
mutative pattern which makes updates easier.
This commit is contained in:
Sebastian Markbage
2015-02-26 01:17:05 -08:00
parent 618bbdc531
commit 4adcee69a0
4 changed files with 371 additions and 27 deletions
+3
View File
@@ -39,10 +39,12 @@ ReactDefaultInjection.inject();
var createElement = ReactElement.createElement;
var createFactory = ReactElement.createFactory;
var cloneElement = ReactElement.cloneElement;
if (__DEV__) {
createElement = ReactElementValidator.createElement;
createFactory = ReactElementValidator.createFactory;
cloneElement = ReactElementValidator.cloneElement;
}
var render = ReactPerf.measure('React', 'render', ReactMount.render);
@@ -62,6 +64,7 @@ var React = {
},
createClass: ReactClass.createClass,
createElement: createElement,
cloneElement: cloneElement,
createFactory: createFactory,
createMixin: function(mixin) {
// Currently a noop. Will be used to validate and trace mixins.
+54
View File
@@ -228,6 +228,60 @@ ReactElement.cloneAndReplaceProps = function(oldElement, newProps) {
return newElement;
};
ReactElement.cloneElement = function(element, config, children) {
var propName;
// Original props are copied
var props = assign({}, element.props);
// Reserved names are extracted
var key = element.key;
var ref = element.ref;
// Owner will be preserved, unless ref is overridden
var owner = element._owner;
if (config != null) {
if (config.ref !== undefined) {
// Silently steal the ref from the parent.
ref = config.ref;
owner = ReactCurrentOwner.current;
}
if (config.key !== undefined) {
key = '' + config.key;
}
// Remaining properties override existing props
for (propName in config) {
if (config.hasOwnProperty(propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return new ReactElement(
element.type,
key,
ref,
owner,
element._context,
props
);
};
/**
* @param {?object} object
* @return {boolean} True if `object` is a valid component.
+46 -27
View File
@@ -336,6 +336,42 @@ function checkAndWarnForMutatedProps(element) {
}
}
/**
* Given an element, validate that its props follow the propTypes definition,
* provided by the type.
*
* @param {ReactElement} element
*/
function validatePropTypes(element) {
if (element.type == null) {
// This has already warned. Don't throw.
return;
}
// Extract the component class from the element. Converts string types
// to a composite class which may have propTypes.
// TODO: Validating a string's propTypes is not decoupled from the
// rendering target which is problematic.
var componentClass = ReactNativeComponent.getComponentClassForElement(
element
);
var name = componentClass.displayName || componentClass.name;
if (componentClass.propTypes) {
checkPropTypes(
name,
componentClass.propTypes,
element.props,
ReactPropTypeLocations.prop
);
}
if (typeof componentClass.getDefaultProps === 'function') {
warning(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
}
}
var ReactElementValidator = {
checkAndWarnForMutatedProps: checkAndWarnForMutatedProps,
@@ -362,33 +398,7 @@ var ReactElementValidator = {
validateChildKeys(arguments[i], type);
}
if (type) {
// Extract the component class from the element. Converts string types
// to a composite class which may have propTypes.
// TODO: Validating a string's propTypes is not decoupled from the
// rendering target which is problematic.
var componentClass = ReactNativeComponent.getComponentClassForElement(
element
);
var name = componentClass.displayName || componentClass.name;
if (__DEV__) {
if (componentClass.propTypes) {
checkPropTypes(
name,
componentClass.propTypes,
element.props,
ReactPropTypeLocations.prop
);
}
}
if (typeof componentClass.getDefaultProps === 'function') {
warning(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
}
}
validatePropTypes(element);
return element;
},
@@ -428,6 +438,15 @@ var ReactElementValidator = {
return validatedFactory;
},
cloneElement: function(element, props, children) {
var newElement = ReactElement.cloneElement.apply(this, arguments);
for (var i = 2; i < arguments.length; i++) {
validateChildKeys(arguments[i], newElement.type);
}
validatePropTypes(newElement);
return newElement;
}
};
@@ -0,0 +1,268 @@
/**
* Copyright 2013-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';
require('mock-modules');
var mocks = require('mocks');
var React;
var ReactTestUtils;
describe('ReactElementClone', function() {
beforeEach(function() {
React = require('React');
ReactTestUtils = require('ReactTestUtils');
});
it('should clone a DOM component with new props', function() {
var Grandparent = React.createClass({
render: function() {
return <Parent child={<div className="child" />} />;
}
});
var Parent = React.createClass({
render: function() {
return (
<div className="parent">
{React.cloneElement(this.props.child, { className: 'xyz' })}
</div>
);
}
});
var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
expect(component.getDOMNode().childNodes[0].className).toBe('xyz');
});
it('should clone a composite component with new props', function() {
var Child = React.createClass({
render: function() {
return <div className={this.props.className} />;
}
});
var Grandparent = React.createClass({
render: function() {
return <Parent child={<Child className="child" />} />;
}
});
var Parent = React.createClass({
render: function() {
return (
<div className="parent">
{React.cloneElement(this.props.child, { className: 'xyz' })}
</div>
);
}
});
var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
expect(component.getDOMNode().childNodes[0].className).toBe('xyz');
});
it('should keep the original ref if it is not overridden', function() {
var Grandparent = React.createClass({
render: function() {
return <Parent child={<div ref="yolo" />} />;
}
});
var Parent = React.createClass({
render: function() {
return (
<div>
{React.cloneElement(this.props.child, { className: 'xyz' })}
</div>
);
}
});
var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
expect(component.refs.yolo.tagName).toBe('DIV');
});
it('should transfer the key property', function() {
var Component = React.createClass({
render: function() {
return null;
}
});
var clone = React.cloneElement(<Component />, {key: 'xyz'});
expect(clone.key).toBe('xyz');
});
it('should transfer children', function() {
var Component = React.createClass({
render: function() {
expect(this.props.children).toBe('xyz');
return <div />;
}
});
ReactTestUtils.renderIntoDocument(
React.cloneElement(<Component />, {children: 'xyz'})
);
});
it('should shallow clone children', function() {
var Component = React.createClass({
render: function() {
expect(this.props.children).toBe('xyz');
return <div />;
}
});
ReactTestUtils.renderIntoDocument(
React.cloneElement(<Component>xyz</Component>, {})
);
});
it('should accept children as rest arguments', function() {
var Component = React.createClass({
render: function() {
return null;
}
});
var clone = React.cloneElement(
<Component>xyz</Component>,
{ children: <Component /> },
<div />,
<span />
);
expect(clone.props.children).toEqual([
<div />,
<span />
]);
});
it('should support keys and refs', function() {
var Parent = React.createClass({
render: function() {
var clone =
React.cloneElement(this.props.children, {key: 'xyz', ref: 'xyz'});
expect(clone.key).toBe('xyz');
expect(clone.ref).toBe('xyz');
return <div>{clone}</div>;
}
});
var Grandparent = React.createClass({
render: function() {
return <Parent ref="parent"><span key="abc" /></Parent>;
}
});
var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
expect(component.refs.parent.refs.xyz.tagName).toBe('SPAN');
});
it('should steal the ref if a new ref is specified', function() {
var Parent = React.createClass({
render: function() {
var clone = React.cloneElement(this.props.children, {ref: 'xyz'});
return <div>{clone}</div>;
}
});
var Grandparent = React.createClass({
render: function() {
return <Parent ref="parent"><span ref="child" /></Parent>;
}
});
var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
expect(component.refs.child).toBeUndefined();
expect(component.refs.parent.refs.xyz.tagName).toBe('SPAN');
});
it('should overwrite props', function() {
var Component = React.createClass({
render: function() {
expect(this.props.myprop).toBe('xyz');
return <div />;
}
});
ReactTestUtils.renderIntoDocument(
React.cloneElement(<Component myprop="abc" />, {myprop: 'xyz'})
);
});
it('warns for keys for arrays of elements in rest args', function() {
spyOn(console, 'warn');
React.cloneElement(<div />, null, [<div />, <div />]);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain(
'Each child in an array or iterator should have a unique "key" prop.'
);
});
it('does not warns for arrays of elements with keys', function() {
spyOn(console, 'warn');
React.cloneElement(<div />, null, [<div key="#1" />, <div key="#2" />]);
expect(console.warn.argsForCall.length).toBe(0);
});
it('does not warn when the element is directly in rest args', function() {
spyOn(console, 'warn');
React.cloneElement(<div />, null, <div />, <div />);
expect(console.warn.argsForCall.length).toBe(0);
});
it('does not warn when the array contains a non-element', function() {
spyOn(console, 'warn');
React.cloneElement(<div />, null, [{}, {}]);
expect(console.warn.argsForCall.length).toBe(0);
});
it('should check declared prop types after clone', function() {
spyOn(console, 'warn');
var Component = React.createClass({
propTypes: {
color: React.PropTypes.string.isRequired
},
render: function() {
return React.createElement('div', null, 'My color is ' + this.color);
}
});
var Parent = React.createClass({
render: function() {
return React.cloneElement(this.props.child, {color: 123});
}
});
var GrandParent = React.createClass({
render: function() {
return React.createElement(
Parent,
{ child: React.createElement(Component, {color: 'red'}) }
);
}
});
ReactTestUtils.renderIntoDocument(React.createElement(GrandParent));
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.calls[0].args[0]).toBe(
'Warning: Failed propType: ' +
'Invalid prop `color` of type `number` supplied to `Component`, ' +
'expected `string`. Check the render method of `Parent`.'
);
});
});