mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #3266 from sebmarkbage/cloneelement
Add cloneElement Implementation
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