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.
Sebastian Markbage committed
Feb 26, 2015 at 01:17 UTC
4adcee69a04058a4858837752e7e8f011bb652f9
4 files changed
+371
-27
src/browser/ui/React.js
+3
@@ -39,10 +39,12 @@ ReactDefaultInjection.inject();
39
40
var createElement = ReactElement.createElement;
41
var createFactory = ReactElement.createFactory;
42
+var cloneElement = ReactElement.cloneElement;
43
44
if (__DEV__) {
45
createElement = ReactElementValidator.createElement;
46
createFactory = ReactElementValidator.createFactory;
47
+ cloneElement = ReactElementValidator.cloneElement;
48
}
49
50
var render = ReactPerf.measure('React', 'render', ReactMount.render);
@@ -62,6 +64,7 @@ var React = {
64
},
65
createClass: ReactClass.createClass,
66
createElement: createElement,
67
+ cloneElement: cloneElement,
68
createFactory: createFactory,
69
createMixin: function(mixin) {
70
// Currently a noop. Will be used to validate and trace mixins.
src/classic/element/ReactElement.js
+54
@@ -228,6 +228,60 @@ ReactElement.cloneAndReplaceProps = function(oldElement, newProps) {
228
return newElement;
229
};
230
231
+ReactElement.cloneElement = function(element, config, children) {
232
+ var propName;
233
+
234
+ // Original props are copied
235
+ var props = assign({}, element.props);
236
+
237
+ // Reserved names are extracted
238
+ var key = element.key;
239
+ var ref = element.ref;
240
+
241
+ // Owner will be preserved, unless ref is overridden
242
+ var owner = element._owner;
243
+
244
+ if (config != null) {
245
+ if (config.ref !== undefined) {
246
+ // Silently steal the ref from the parent.
247
+ ref = config.ref;
248
+ owner = ReactCurrentOwner.current;
249
+ }
250
+ if (config.key !== undefined) {
251
+ key = '' + config.key;
252
+ }
253
+ // Remaining properties override existing props
254
+ for (propName in config) {
255
+ if (config.hasOwnProperty(propName) &&
256
+ !RESERVED_PROPS.hasOwnProperty(propName)) {
257
+ props[propName] = config[propName];
258
+ }
259
+ }
260
+ }
261
+
262
+ // Children can be more than one argument, and those are transferred onto
263
+ // the newly allocated props object.
264
+ var childrenLength = arguments.length - 2;
265
+ if (childrenLength === 1) {
266
+ props.children = children;
267
+ } else if (childrenLength > 1) {
268
+ var childArray = Array(childrenLength);
269
+ for (var i = 0; i < childrenLength; i++) {
270
+ childArray[i] = arguments[i + 2];
271
+ }
272
+ props.children = childArray;
273
+ }
274
+
275
+ return new ReactElement(
276
+ element.type,
277
+ key,
278
+ ref,
279
+ owner,
280
+ element._context,
281
+ props
282
+ );
283
+};
284
+
285
/**
286
* @param {?object} object
287
* @return {boolean} True if `object` is a valid component.
src/classic/element/ReactElementValidator.js
+46
-27
@@ -336,6 +336,42 @@ function checkAndWarnForMutatedProps(element) {
336
}
337
}
338
339
+/**
340
+ * Given an element, validate that its props follow the propTypes definition,
341
+ * provided by the type.
342
+ *
343
+ * @param {ReactElement} element
344
+ */
345
+function validatePropTypes(element) {
346
+ if (element.type == null) {
347
+ // This has already warned. Don't throw.
348
+ return;
349
+ }
350
+ // Extract the component class from the element. Converts string types
351
+ // to a composite class which may have propTypes.
352
+ // TODO: Validating a string's propTypes is not decoupled from the
353
+ // rendering target which is problematic.
354
+ var componentClass = ReactNativeComponent.getComponentClassForElement(
355
+ element
356
+ );
357
+ var name = componentClass.displayName || componentClass.name;
358
+ if (componentClass.propTypes) {
359
+ checkPropTypes(
360
+ name,
361
+ componentClass.propTypes,
362
+ element.props,
363
+ ReactPropTypeLocations.prop
364
+ );
365
+ }
366
+ if (typeof componentClass.getDefaultProps === 'function') {
367
+ warning(
368
+ componentClass.getDefaultProps.isReactClassApproved,
369
+ 'getDefaultProps is only used on classic React.createClass ' +
370
+ 'definitions. Use a static property named `defaultProps` instead.'
371
+ );
372
+ }
373
+}
374
+
375
var ReactElementValidator = {
376
377
checkAndWarnForMutatedProps: checkAndWarnForMutatedProps,
@@ -362,33 +398,7 @@ var ReactElementValidator = {
398
validateChildKeys(arguments[i], type);
399
}
400
365
- if (type) {
366
- // Extract the component class from the element. Converts string types
367
- // to a composite class which may have propTypes.
368
- // TODO: Validating a string's propTypes is not decoupled from the
369
- // rendering target which is problematic.
370
- var componentClass = ReactNativeComponent.getComponentClassForElement(
371
- element
372
- );
373
- var name = componentClass.displayName || componentClass.name;
374
- if (__DEV__) {
375
- if (componentClass.propTypes) {
376
- checkPropTypes(
377
- name,
378
- componentClass.propTypes,
379
- element.props,
380
- ReactPropTypeLocations.prop
381
- );
382
- }
383
- }
384
- if (typeof componentClass.getDefaultProps === 'function') {
385
- warning(
386
- componentClass.getDefaultProps.isReactClassApproved,
387
- 'getDefaultProps is only used on classic React.createClass ' +
388
- 'definitions. Use a static property named `defaultProps` instead.'
389
- );
390
- }
391
- }
401
+ validatePropTypes(element);
402
403
return element;
404
},
@@ -428,6 +438,15 @@ var ReactElementValidator = {
438
439
440
return validatedFactory;
441
+ },
442
+
443
+ cloneElement: function(element, props, children) {
444
+ var newElement = ReactElement.cloneElement.apply(this, arguments);
445
+ for (var i = 2; i < arguments.length; i++) {
446
+ validateChildKeys(arguments[i], newElement.type);
447
+ }
448
+ validatePropTypes(newElement);
449
+ return newElement;
450
}
451
452
};
src/classic/element/__tests__/ReactElementClone-test.js
new
+268
@@ -0,0 +1,268 @@
1
+/**
2
+ * Copyright 2013-2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+require('mock-modules');
15
+
16
+var mocks = require('mocks');
17
+
18
+var React;
19
+var ReactTestUtils;
20
+
21
+describe('ReactElementClone', function() {
22
+
23
+ beforeEach(function() {
24
+ React = require('React');
25
+ ReactTestUtils = require('ReactTestUtils');
26
+ });
27
+
28
+ it('should clone a DOM component with new props', function() {
29
+ var Grandparent = React.createClass({
30
+ render: function() {
31
+ return <Parent child={<div className="child" />} />;
32
+ }
33
+ });
34
+ var Parent = React.createClass({
35
+ render: function() {
36
+ return (
37
+ <div className="parent">
38
+ {React.cloneElement(this.props.child, { className: 'xyz' })}
39
+ </div>
40
+ );
41
+ }
42
+ });
43
+ var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
44
+ expect(component.getDOMNode().childNodes[0].className).toBe('xyz');
45
+ });
46
+
47
+ it('should clone a composite component with new props', function() {
48
+ var Child = React.createClass({
49
+ render: function() {
50
+ return <div className={this.props.className} />;
51
+ }
52
+ });
53
+ var Grandparent = React.createClass({
54
+ render: function() {
55
+ return <Parent child={<Child className="child" />} />;
56
+ }
57
+ });
58
+ var Parent = React.createClass({
59
+ render: function() {
60
+ return (
61
+ <div className="parent">
62
+ {React.cloneElement(this.props.child, { className: 'xyz' })}
63
+ </div>
64
+ );
65
+ }
66
+ });
67
+ var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
68
+ expect(component.getDOMNode().childNodes[0].className).toBe('xyz');
69
+ });
70
+
71
+ it('should keep the original ref if it is not overridden', function() {
72
+ var Grandparent = React.createClass({
73
+ render: function() {
74
+ return <Parent child={<div ref="yolo" />} />;
75
+ }
76
+ });
77
+
78
+ var Parent = React.createClass({
79
+ render: function() {
80
+ return (
81
+ <div>
82
+ {React.cloneElement(this.props.child, { className: 'xyz' })}
83
+ </div>
84
+ );
85
+ }
86
+ });
87
+
88
+ var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
89
+ expect(component.refs.yolo.tagName).toBe('DIV');
90
+ });
91
+
92
+ it('should transfer the key property', function() {
93
+ var Component = React.createClass({
94
+ render: function() {
95
+ return null;
96
+ }
97
+ });
98
+ var clone = React.cloneElement(<Component />, {key: 'xyz'});
99
+ expect(clone.key).toBe('xyz');
100
+ });
101
+
102
+ it('should transfer children', function() {
103
+ var Component = React.createClass({
104
+ render: function() {
105
+ expect(this.props.children).toBe('xyz');
106
+ return <div />;
107
+ }
108
+ });
109
+
110
+ ReactTestUtils.renderIntoDocument(
111
+ React.cloneElement(<Component />, {children: 'xyz'})
112
+ );
113
+ });
114
+
115
+ it('should shallow clone children', function() {
116
+ var Component = React.createClass({
117
+ render: function() {
118
+ expect(this.props.children).toBe('xyz');
119
+ return <div />;
120
+ }
121
+ });
122
+
123
+ ReactTestUtils.renderIntoDocument(
124
+ React.cloneElement(<Component>xyz</Component>, {})
125
+ );
126
+ });
127
+
128
+ it('should accept children as rest arguments', function() {
129
+ var Component = React.createClass({
130
+ render: function() {
131
+ return null;
132
+ }
133
+ });
134
+
135
+ var clone = React.cloneElement(
136
+ <Component>xyz</Component>,
137
+ { children: <Component /> },
138
+ <div />,
139
+ <span />
140
+ );
141
+
142
+ expect(clone.props.children).toEqual([
143
+ <div />,
144
+ <span />
145
+ ]);
146
+ });
147
+
148
+ it('should support keys and refs', function() {
149
+ var Parent = React.createClass({
150
+ render: function() {
151
+ var clone =
152
+ React.cloneElement(this.props.children, {key: 'xyz', ref: 'xyz'});
153
+ expect(clone.key).toBe('xyz');
154
+ expect(clone.ref).toBe('xyz');
155
+ return <div>{clone}</div>;
156
+ }
157
+ });
158
+
159
+ var Grandparent = React.createClass({
160
+ render: function() {
161
+ return <Parent ref="parent"><span key="abc" /></Parent>;
162
+ }
163
+ });
164
+
165
+ var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
166
+ expect(component.refs.parent.refs.xyz.tagName).toBe('SPAN');
167
+ });
168
+
169
+ it('should steal the ref if a new ref is specified', function() {
170
+ var Parent = React.createClass({
171
+ render: function() {
172
+ var clone = React.cloneElement(this.props.children, {ref: 'xyz'});
173
+ return <div>{clone}</div>;
174
+ }
175
+ });
176
+
177
+ var Grandparent = React.createClass({
178
+ render: function() {
179
+ return <Parent ref="parent"><span ref="child" /></Parent>;
180
+ }
181
+ });
182
+
183
+ var component = ReactTestUtils.renderIntoDocument(<Grandparent />);
184
+ expect(component.refs.child).toBeUndefined();
185
+ expect(component.refs.parent.refs.xyz.tagName).toBe('SPAN');
186
+ });
187
+
188
+ it('should overwrite props', function() {
189
+ var Component = React.createClass({
190
+ render: function() {
191
+ expect(this.props.myprop).toBe('xyz');
192
+ return <div />;
193
+ }
194
+ });
195
+
196
+ ReactTestUtils.renderIntoDocument(
197
+ React.cloneElement(<Component myprop="abc" />, {myprop: 'xyz'})
198
+ );
199
+ });
200
+
201
+ it('warns for keys for arrays of elements in rest args', function() {
202
+ spyOn(console, 'warn');
203
+
204
+ React.cloneElement(<div />, null, [<div />, <div />]);
205
+
206
+ expect(console.warn.argsForCall.length).toBe(1);
207
+ expect(console.warn.argsForCall[0][0]).toContain(
208
+ 'Each child in an array or iterator should have a unique "key" prop.'
209
+ );
210
+ });
211
+
212
+ it('does not warns for arrays of elements with keys', function() {
213
+ spyOn(console, 'warn');
214
+
215
+ React.cloneElement(<div />, null, [<div key="#1" />, <div key="#2" />]);
216
+
217
+ expect(console.warn.argsForCall.length).toBe(0);
218
+ });
219
+
220
+ it('does not warn when the element is directly in rest args', function() {
221
+ spyOn(console, 'warn');
222
+
223
+ React.cloneElement(<div />, null, <div />, <div />);
224
+
225
+ expect(console.warn.argsForCall.length).toBe(0);
226
+ });
227
+
228
+ it('does not warn when the array contains a non-element', function() {
229
+ spyOn(console, 'warn');
230
+
231
+ React.cloneElement(<div />, null, [{}, {}]);
232
+
233
+ expect(console.warn.argsForCall.length).toBe(0);
234
+ });
235
+
236
+ it('should check declared prop types after clone', function() {
237
+ spyOn(console, 'warn');
238
+ var Component = React.createClass({
239
+ propTypes: {
240
+ color: React.PropTypes.string.isRequired
241
+ },
242
+ render: function() {
243
+ return React.createElement('div', null, 'My color is ' + this.color);
244
+ }
245
+ });
246
+ var Parent = React.createClass({
247
+ render: function() {
248
+ return React.cloneElement(this.props.child, {color: 123});
249
+ }
250
+ });
251
+ var GrandParent = React.createClass({
252
+ render: function() {
253
+ return React.createElement(
254
+ Parent,
255
+ { child: React.createElement(Component, {color: 'red'}) }
256
+ );
257
+ }
258
+ });
259
+ ReactTestUtils.renderIntoDocument(React.createElement(GrandParent));
260
+ expect(console.warn.argsForCall.length).toBe(1);
261
+ expect(console.warn.calls[0].args[0]).toBe(
262
+ 'Warning: Failed propType: ' +
263
+ 'Invalid prop `color` of type `number` supplied to `Component`, ' +
264
+ 'expected `string`. Check the render method of `Parent`.'
265
+ );
266
+ });
267
+
268
+});