mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user