From cc005668b56028ab35353bc2d671e9fb59d208f8 Mon Sep 17 00:00:00 2001 From: Pete Hunt Date: Thu, 2 Jan 2014 15:41:10 -0800 Subject: [PATCH] cloneWithProps() what if you want to change the props of a child? This is my first attempt which lets you clone a child and transfer some custom props to it. There is a fundamental issue with refs here. Since the component is cloned the ref will be broken. And since we can clone multiple times, it might not make sense to support repairing refs. --- src/core/ReactDOM.js | 2 + src/core/ReactPropTransferer.js | 51 ++++++++----- src/utils/__tests__/cloneWithProps-test.js | 89 ++++++++++++++++++++++ src/utils/cloneWithProps.js | 49 ++++++++++++ 4 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 src/utils/__tests__/cloneWithProps-test.js create mode 100644 src/utils/cloneWithProps.js diff --git a/src/core/ReactDOM.js b/src/core/ReactDOM.js index 412a6f4704..c1de36d9ab 100644 --- a/src/core/ReactDOM.js +++ b/src/core/ReactDOM.js @@ -50,6 +50,8 @@ function createDOMComponentClass(tag, omitClose) { instance.construct.apply(instance, arguments); return instance; }; + + Constructor.ConvenienceConstructor = ConvenienceConstructor; ConvenienceConstructor.componentConstructor = Constructor; return ConvenienceConstructor; } diff --git a/src/core/ReactPropTransferer.js b/src/core/ReactPropTransferer.js index 1242887d5d..ed721a3c9b 100644 --- a/src/core/ReactPropTransferer.js +++ b/src/core/ReactPropTransferer.js @@ -76,6 +76,33 @@ var ReactPropTransferer = { TransferStrategies: TransferStrategies, + /** + * Merge two props objects using TransferStrategies. + * + * @param {object} oldProps original props (they take precedence) + * @param {object} newProps new props to merge in + * @return {object} a new object containing both sets of props merged. + */ + mergeProps: function(oldProps, newProps) { + var props = merge(oldProps); + + for (var thisKey in newProps) { + if (!newProps.hasOwnProperty(thisKey)) { + continue; + } + + var transferStrategy = TransferStrategies[thisKey]; + + if (transferStrategy) { + transferStrategy(props, thisKey, newProps[thisKey]); + } else if (!props.hasOwnProperty(thisKey)) { + props[thisKey] = newProps[thisKey]; + } + } + + return props; + }, + /** * @lends {ReactPropTransferer.prototype} */ @@ -104,29 +131,15 @@ var ReactPropTransferer = { component.constructor.displayName ); - var props = {}; - for (var thatKey in component.props) { - if (component.props.hasOwnProperty(thatKey)) { - props[thatKey] = component.props[thatKey]; - } - } - for (var thisKey in this.props) { - if (!this.props.hasOwnProperty(thisKey)) { - continue; - } - var transferStrategy = TransferStrategies[thisKey]; - if (transferStrategy) { - transferStrategy(props, thisKey, this.props[thisKey]); - } else if (!props.hasOwnProperty(thisKey)) { - props[thisKey] = this.props[thisKey]; - } - } - component.props = props; + component.props = ReactPropTransferer.mergeProps( + component.props, + this.props + ); + return component; } } - }; module.exports = ReactPropTransferer; diff --git a/src/utils/__tests__/cloneWithProps-test.js b/src/utils/__tests__/cloneWithProps-test.js new file mode 100644 index 0000000000..2e96bdcdda --- /dev/null +++ b/src/utils/__tests__/cloneWithProps-test.js @@ -0,0 +1,89 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @emails react-core + * @jsx React.DOM + */ + +"use strict"; + +require('mock-modules').dontMock('cloneWithProps'); + +var mocks = require('mocks'); + +var cloneWithProps = require('cloneWithProps'); + +var React; +var ReactTestUtils; + +var onlyChild; + +describe('cloneWithProps', function() { + + beforeEach(function() { + React = require('React'); + ReactTestUtils = require('ReactTestUtils'); + onlyChild = require('onlyChild'); + }); + + it('should clone an object with new props', function() { + var Grandparent = React.createClass({ + render: function() { + return
; + } + }); + var Parent = React.createClass({ + render: function() { + return ( +
+ {cloneWithProps(onlyChild(this.props.children), {className: 'xyz'})} +
+ ); + } + }); + var component = ReactTestUtils.renderIntoDocument(); + expect(component.getDOMNode().childNodes[0].className) + .toBe('child xyz'); + }); + + it('should warn when cloning with refs', function() { + var Grandparent = React.createClass({ + render: function() { + return
; + } + }); + var Parent = React.createClass({ + render: function() { + return ( +
+ {cloneWithProps(onlyChild(this.props.children), {className: 'xyz'})} +
+ ); + } + }); + + var _warn = console.warn; + + try { + console.warn = mocks.getMockFunction(); + + var component = ReactTestUtils.renderIntoDocument(); + expect(component.refs).toBe(undefined); + expect(console.warn.mock.calls.length).toBe(1); + } finally { + console.warn = _warn; + } + }); +}); diff --git a/src/utils/cloneWithProps.js b/src/utils/cloneWithProps.js new file mode 100644 index 0000000000..67d51636eb --- /dev/null +++ b/src/utils/cloneWithProps.js @@ -0,0 +1,49 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @typechecks + * @providesModule cloneWithProps + */ + +"use strict"; + +var ReactPropTransferer = require('ReactPropTransferer'); + +/** + * Sometimes you want to change the props of a child passed to you. Usually + * this is to add a CSS class. + * + * @param {object} child child component you'd like to clone + * @param {object} props props you'd like to modify. They will be merged + * as if you used `transferPropsTo()`. + * @return {object} a clone of child with props merged in. + */ +function cloneWithProps(child, props) { + if (__DEV__) { + if (child.props.ref) { + console.warn( + 'You are calling cloneWithProps() on a child with a ref. This is ' + + 'dangerous because you\'re creating a new child which will not be ' + + 'added as a ref to its parent.' + ); + } + } + + return child.constructor.ConvenienceConstructor( + ReactPropTransferer.mergeProps(child.props, props) + ); +} + +module.exports = cloneWithProps;