Merge pull request #3266 from sebmarkbage/cloneelement

Add cloneElement Implementation
This commit is contained in:
Sebastian Markbåge
2015-03-02 12:15:20 -08:00
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`.'
);
});
});