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;