From 52a229f168ea10c342d0e69e1ddc63d425579656 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 28 May 2015 16:06:28 -0700 Subject: [PATCH] Make DOM wrapper component using lower-level primitives Introducing: a really lame version of composite components, right inside of ReactDOMComponent! Now ReactDOMInput isn't an actual component. This brings us closer to exposing DOM nodes as refs. --- .../{AutoFocusMixin.js => AutoFocusUtils.js} | 16 +- .../dom/client/wrappers/LinkedValueUtils.js | 111 +++++----- .../dom/client/wrappers/ReactDOMButton.js | 4 +- .../dom/client/wrappers/ReactDOMInput.js | 209 +++++++++--------- .../dom/client/wrappers/ReactDOMSelect.js | 25 ++- .../dom/client/wrappers/ReactDOMTextarea.js | 19 +- .../dom/shared/HTMLDOMPropertyConfig.js | 2 +- src/renderers/dom/shared/ReactDOMComponent.js | 67 ++++-- .../dom/shared/ReactDefaultInjection.js | 2 - .../__tests__/ReactDOMComponent-test.js | 9 +- 10 files changed, 268 insertions(+), 196 deletions(-) rename src/renderers/dom/client/wrappers/{AutoFocusMixin.js => AutoFocusUtils.js} (67%) diff --git a/src/renderers/dom/client/wrappers/AutoFocusMixin.js b/src/renderers/dom/client/wrappers/AutoFocusUtils.js similarity index 67% rename from src/renderers/dom/client/wrappers/AutoFocusMixin.js rename to src/renderers/dom/client/wrappers/AutoFocusUtils.js index f401fa60ba..f7197e2084 100644 --- a/src/renderers/dom/client/wrappers/AutoFocusMixin.js +++ b/src/renderers/dom/client/wrappers/AutoFocusUtils.js @@ -6,16 +6,18 @@ * 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. * - * @providesModule AutoFocusMixin + * @providesModule AutoFocusUtils * @typechecks static-only */ 'use strict'; +var ReactMount = require('ReactMount'); + var findDOMNode = require('findDOMNode'); var focusNode = require('focusNode'); -var AutoFocusMixin = { +var Mixin = { componentDidMount: function() { if (this.props.autoFocus) { focusNode(findDOMNode(this)); @@ -23,4 +25,12 @@ var AutoFocusMixin = { } }; -module.exports = AutoFocusMixin; +var AutoFocusUtils = { + Mixin: Mixin, + + focusDOMComponent: function() { + focusNode(ReactMount.getNode(this._rootNodeID)); + } +}; + +module.exports = AutoFocusUtils; diff --git a/src/renderers/dom/client/wrappers/LinkedValueUtils.js b/src/renderers/dom/client/wrappers/LinkedValueUtils.js index 84d3f8a9ae..71999bfa05 100644 --- a/src/renderers/dom/client/wrappers/LinkedValueUtils.js +++ b/src/renderers/dom/client/wrappers/LinkedValueUtils.js @@ -15,6 +15,7 @@ var ReactPropTypes = require('ReactPropTypes'); var invariant = require('invariant'); +var warning = require('warning'); var hasReadOnlyValue = { 'button': true, @@ -52,20 +53,48 @@ function _assertCheckedLink(inputProps) { ); } -/** - * @param {SyntheticEvent} e change event to handle - */ -function _handleLinkedValueChange(e) { - /*jshint validthis:true */ - this.props.valueLink.requestChange(e.target.value); -} +var propTypes = { + value: function(props, propName, componentName) { + if (!props[propName] || + hasReadOnlyValue[props.type] || + props.onChange || + props.readOnly || + props.disabled) { + return null; + } + return new Error( + 'You provided a `value` prop to a form field without an ' + + '`onChange` handler. This will render a read-only field. If ' + + 'the field should be mutable use `defaultValue`. Otherwise, ' + + 'set either `onChange` or `readOnly`.' + ); + }, + checked: function(props, propName, componentName) { + if (!props[propName] || + props.onChange || + props.readOnly || + props.disabled) { + return null; + } + return new Error( + 'You provided a `checked` prop to a form field without an ' + + '`onChange` handler. This will render a read-only field. If ' + + 'the field should be mutable use `defaultChecked`. Otherwise, ' + + 'set either `onChange` or `readOnly`.' + ); + }, + onChange: ReactPropTypes.func +}; -/** - * @param {SyntheticEvent} e change event to handle - */ -function _handleLinkedCheckChange(e) { - /*jshint validthis:true */ - this.props.checkedLink.requestChange(e.target.checked); +var loggedTypeFailures = {}; +function getDeclarationErrorAddendum(owner) { + if (owner) { + var name = owner.getName(); + if (name) { + return ' Check the render method of `' + name + '`.'; + } + } + return ''; } /** @@ -73,38 +102,19 @@ function _handleLinkedCheckChange(e) { * this outside of the ReactDOM controlled form components. */ var LinkedValueUtils = { - Mixin: { - propTypes: { - value: function(props, propName, componentName) { - if (!props[propName] || - hasReadOnlyValue[props.type] || - props.onChange || - props.readOnly || - props.disabled) { - return null; - } - return new Error( - 'You provided a `value` prop to a form field without an ' + - '`onChange` handler. This will render a read-only field. If ' + - 'the field should be mutable use `defaultValue`. Otherwise, ' + - 'set either `onChange` or `readOnly`.' - ); - }, - checked: function(props, propName, componentName) { - if (!props[propName] || - props.onChange || - props.readOnly || - props.disabled) { - return null; - } - return new Error( - 'You provided a `checked` prop to a form field without an ' + - '`onChange` handler. This will render a read-only field. If ' + - 'the field should be mutable use `defaultChecked`. Otherwise, ' + - 'set either `onChange` or `readOnly`.' - ); - }, - onChange: ReactPropTypes.func + checkPropTypes: function (tagName, props, owner) { + for (var propName in propTypes) { + if (propTypes.hasOwnProperty(propName)) { + var error = propTypes[propName](props, propName, tagName, location); + } + if (error instanceof Error && !(error.message in loggedTypeFailures)) { + // Only monitor this failure once because there tends to be a lot of the + // same error. + loggedTypeFailures[error.message] = true; + + var addendum = getDeclarationErrorAddendum(owner); + warning(false, 'Failed form propType: %s%s', error.message, addendum); + } } }, @@ -135,17 +145,18 @@ var LinkedValueUtils = { /** * @param {object} inputProps Props for form component - * @return {function} change callback either from onChange prop or link. + * @param {SyntheticEvent} event change event to handle */ - getOnChange: function(inputProps) { + executeOnChange: function(inputProps, event) { if (inputProps.valueLink) { _assertValueLink(inputProps); - return _handleLinkedValueChange; + return inputProps.valueLink.requestChange(event.target.value); } else if (inputProps.checkedLink) { _assertCheckedLink(inputProps); - return _handleLinkedCheckChange; + return inputProps.checkedLink.requestChange(event.target.checked); + } else if (inputProps.onChange) { + return inputProps.onChange.call(undefined, event); } - return inputProps.onChange; } }; diff --git a/src/renderers/dom/client/wrappers/ReactDOMButton.js b/src/renderers/dom/client/wrappers/ReactDOMButton.js index ab5276fb78..9d1e90b56d 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMButton.js +++ b/src/renderers/dom/client/wrappers/ReactDOMButton.js @@ -11,7 +11,7 @@ 'use strict'; -var AutoFocusMixin = require('AutoFocusMixin'); +var AutoFocusUtils = require('AutoFocusUtils'); var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactClass = require('ReactClass'); var ReactElement = require('ReactElement'); @@ -41,7 +41,7 @@ var ReactDOMButton = ReactClass.createClass({ displayName: 'ReactDOMButton', tagName: 'BUTTON', - mixins: [AutoFocusMixin, ReactBrowserComponentMixin], + mixins: [AutoFocusUtils.Mixin, ReactBrowserComponentMixin], render: function() { var props = {}; diff --git a/src/renderers/dom/client/wrappers/ReactDOMInput.js b/src/renderers/dom/client/wrappers/ReactDOMInput.js index 49f783c1e1..488cdb6137 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMInput.js +++ b/src/renderers/dom/client/wrappers/ReactDOMInput.js @@ -11,27 +11,22 @@ 'use strict'; -var AutoFocusMixin = require('AutoFocusMixin'); -var DOMPropertyOperations = require('DOMPropertyOperations'); +var AutoFocusUtils = require('AutoFocusUtils'); +var ReactDOMIDOperations = require('ReactDOMIDOperations'); var LinkedValueUtils = require('LinkedValueUtils'); -var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); -var ReactClass = require('ReactClass'); -var ReactElement = require('ReactElement'); var ReactMount = require('ReactMount'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); -var findDOMNode = require('findDOMNode'); var invariant = require('invariant'); -var input = ReactElement.createFactory('input'); - var instancesByReactID = {}; function forceUpdateIfMounted() { /*jshint validthis:true */ - if (this.isMounted()) { - this.forceUpdate(); + if (this._rootNodeID) { + // DOM component is still mounted; update + ReactDOMInput.updateWrapper(this); } } @@ -51,124 +46,132 @@ function forceUpdateIfMounted() { * * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html */ -var ReactDOMInput = ReactClass.createClass({ - displayName: 'ReactDOMInput', - tagName: 'INPUT', +var ReactDOMInput = { + getNativeProps: function(inst, props, context) { + var value = LinkedValueUtils.getValue(props); + var checked = LinkedValueUtils.getChecked(props); - mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin], + var nativeProps = assign({}, props, { + defaultChecked: undefined, + defaultValue: undefined, + value: value != null ? value : inst._wrapperState.initialValue, + checked: checked != null ? checked : inst._wrapperState.initialChecked, + onChange: inst._wrapperState.onChange + }); - getInitialState: function() { - var defaultValue = this.props.defaultValue; - return { - initialChecked: this.props.defaultChecked || false, - initialValue: defaultValue != null ? defaultValue : null + return nativeProps; + }, + + mountWrapper: function(inst, props) { + LinkedValueUtils.checkPropTypes( + 'input', + props, + inst._currentElement._owner + ); + + var defaultValue = props.defaultValue; + inst._wrapperState = { + initialChecked: props.defaultChecked || false, + initialValue: defaultValue != null ? defaultValue : null, + onChange: _handleChange.bind(inst) }; + + instancesByReactID[inst._rootNodeID] = inst; }, - render: function() { - // Clone `this.props` so we don't mutate the input. - var props = assign({}, this.props); - - props.defaultChecked = null; - props.defaultValue = null; - - var value = LinkedValueUtils.getValue(this.props); - props.value = value != null ? value : this.state.initialValue; - - var checked = LinkedValueUtils.getChecked(this.props); - props.checked = checked != null ? checked : this.state.initialChecked; - - props.onChange = this._handleChange; - - return input(props, this.props.children); + postMountWrapper: function(inst, transaction, props) { + if (props.autoFocus) { + transaction.getReactMountReady().enqueue( + AutoFocusUtils.focusDOMComponent, + inst + ); + } }, - componentDidMount: function() { - var id = ReactMount.getID(findDOMNode(this)); - instancesByReactID[id] = this; + unmountWrapper: function(inst) { + delete instancesByReactID[inst._rootNodeID]; }, - componentWillUnmount: function() { - var rootNode = findDOMNode(this); - var id = ReactMount.getID(rootNode); - delete instancesByReactID[id]; - }, + updateWrapper: function(inst) { + var props = inst._currentElement.props; - componentDidUpdate: function(prevProps, prevState, prevContext) { - var rootNode = findDOMNode(this); - if (this.props.checked != null) { - DOMPropertyOperations.setValueForProperty( - rootNode, + // TODO: Shouldn't this be getChecked(props)? + var checked = props.checked; + if (checked != null) { + ReactDOMIDOperations.updatePropertyByID( + inst._rootNodeID, 'checked', - this.props.checked || false + checked || false ); } - var value = LinkedValueUtils.getValue(this.props); + var value = LinkedValueUtils.getValue(props); if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - DOMPropertyOperations.setValueForProperty(rootNode, 'value', '' + value); + ReactDOMIDOperations.updatePropertyByID( + inst._rootNodeID, + 'value', + '' + value + ); } - }, + } +}; - _handleChange: function(event) { - var returnValue; - var onChange = LinkedValueUtils.getOnChange(this.props); - if (onChange) { - returnValue = onChange.call(this, event); +function _handleChange(event) { + var props = this._currentElement.props; + + var returnValue = LinkedValueUtils.executeOnChange(props, event); + + // Here we use asap to wait until all updates have propagated, which + // is important when using controlled components within layers: + // https://github.com/facebook/react/issues/1698 + ReactUpdates.asap(forceUpdateIfMounted, this); + + var name = props.name; + if (props.type === 'radio' && name != null) { + var rootNode = ReactMount.getNode(this._rootNodeID); + var queryRoot = rootNode; + + while (queryRoot.parentNode) { + queryRoot = queryRoot.parentNode; } - // Here we use asap to wait until all updates have propagated, which - // is important when using controlled components within layers: - // https://github.com/facebook/react/issues/1698 - ReactUpdates.asap(forceUpdateIfMounted, this); - var name = this.props.name; - if (this.props.type === 'radio' && name != null) { - var rootNode = findDOMNode(this); - var queryRoot = rootNode; + // If `rootNode.form` was non-null, then we could try `form.elements`, + // but that sometimes behaves strangely in IE8. We could also try using + // `form.getElementsByName`, but that will only return direct children + // and won't include inputs that use the HTML5 `form=` attribute. Since + // the input might not even be in a form, let's just use the global + // `querySelectorAll` to ensure we don't miss anything. + var group = queryRoot.querySelectorAll( + 'input[name=' + JSON.stringify('' + name) + '][type="radio"]'); - while (queryRoot.parentNode) { - queryRoot = queryRoot.parentNode; - } - - // If `rootNode.form` was non-null, then we could try `form.elements`, - // but that sometimes behaves strangely in IE8. We could also try using - // `form.getElementsByName`, but that will only return direct children - // and won't include inputs that use the HTML5 `form=` attribute. Since - // the input might not even be in a form, let's just use the global - // `querySelectorAll` to ensure we don't miss anything. - var group = queryRoot.querySelectorAll( - 'input[name=' + JSON.stringify('' + name) + '][type="radio"]'); - - for (var i = 0; i < group.length; i++) { - var otherNode = group[i]; - if (otherNode === rootNode || - otherNode.form !== rootNode.form) { - continue; - } - var otherID = ReactMount.getID(otherNode); - invariant( - otherID, - 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + - 'same `name` is not supported.' - ); - var otherInstance = instancesByReactID[otherID]; - invariant( - otherInstance, - 'ReactDOMInput: Unknown radio button ID %s.', - otherID - ); - // If this is a controlled radio button group, forcing the input that - // was previously checked to update will cause it to be come re-checked - // as appropriate. - ReactUpdates.asap(forceUpdateIfMounted, otherInstance); + for (var i = 0; i < group.length; i++) { + var otherNode = group[i]; + if (otherNode === rootNode || + otherNode.form !== rootNode.form) { + continue; } + var otherID = ReactMount.getID(otherNode); + invariant( + otherID, + 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + + 'same `name` is not supported.' + ); + var otherInstance = instancesByReactID[otherID]; + invariant( + otherInstance, + 'ReactDOMInput: Unknown radio button ID %s.', + otherID + ); + // If this is a controlled radio button group, forcing the input that + // was previously checked to update will cause it to be come re-checked + // as appropriate. + ReactUpdates.asap(forceUpdateIfMounted, otherInstance); } - - return returnValue; } -}); + return returnValue; +} module.exports = ReactDOMInput; diff --git a/src/renderers/dom/client/wrappers/ReactDOMSelect.js b/src/renderers/dom/client/wrappers/ReactDOMSelect.js index 1573d64a2b..4a89070df1 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMSelect.js +++ b/src/renderers/dom/client/wrappers/ReactDOMSelect.js @@ -11,11 +11,12 @@ 'use strict'; -var AutoFocusMixin = require('AutoFocusMixin'); +var AutoFocusUtils = require('AutoFocusUtils'); var LinkedValueUtils = require('LinkedValueUtils'); var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactClass = require('ReactClass'); var ReactElement = require('ReactElement'); +var ReactInstanceMap = require('ReactInstanceMap'); var ReactUpdates = require('ReactUpdates'); var ReactPropTypes = require('ReactPropTypes'); @@ -118,7 +119,7 @@ var ReactDOMSelect = ReactClass.createClass({ displayName: 'ReactDOMSelect', tagName: 'SELECT', - mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin], + mixins: [AutoFocusUtils.Mixin, ReactBrowserComponentMixin], statics: { valueContextKey: valueContextKey @@ -129,6 +130,16 @@ var ReactDOMSelect = ReactClass.createClass({ value: selectValueType }, + componentWillMount: function() { + LinkedValueUtils.checkPropTypes( + 'select', + this.props, + ReactInstanceMap.get(this)._currentElement._owner + ); + + this._pendingUpdate = false; + }, + getInitialState: function() { // Pass down initial value so initial generated markup has correct // `selected` attributes @@ -162,10 +173,6 @@ var ReactDOMSelect = ReactClass.createClass({ return select(props, this.props.children); }, - componentWillMount: function() { - this._pendingUpdate = false; - }, - componentWillReceiveProps: function(nextProps) { // After the initial mount, we control selected-ness manually so don't pass // the context value down @@ -189,11 +196,7 @@ var ReactDOMSelect = ReactClass.createClass({ }, _handleChange: function(event) { - var returnValue; - var onChange = LinkedValueUtils.getOnChange(this.props); - if (onChange) { - returnValue = onChange.call(this, event); - } + var returnValue = LinkedValueUtils.executeOnChange(this.props, event); this._pendingUpdate = true; ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this); diff --git a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js index 0261005c40..d75e4263ef 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js +++ b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js @@ -11,12 +11,13 @@ 'use strict'; -var AutoFocusMixin = require('AutoFocusMixin'); +var AutoFocusUtils = require('AutoFocusUtils'); var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueUtils = require('LinkedValueUtils'); var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactClass = require('ReactClass'); var ReactElement = require('ReactElement'); +var ReactInstanceMap = require('ReactInstanceMap'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); @@ -53,7 +54,15 @@ var ReactDOMTextarea = ReactClass.createClass({ displayName: 'ReactDOMTextarea', tagName: 'TEXTAREA', - mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin], + mixins: [AutoFocusUtils.Mixin, ReactBrowserComponentMixin], + + componentWillMount: function() { + LinkedValueUtils.checkPropTypes( + 'textarea', + this.props, + ReactInstanceMap.get(this)._currentElement._owner + ); + }, getInitialState: function() { var defaultValue = this.props.defaultValue; @@ -123,11 +132,7 @@ var ReactDOMTextarea = ReactClass.createClass({ }, _handleChange: function(event) { - var returnValue; - var onChange = LinkedValueUtils.getOnChange(this.props); - if (onChange) { - returnValue = onChange.call(this, event); - } + var returnValue = LinkedValueUtils.executeOnChange(this.props, event); ReactUpdates.asap(forceUpdateIfMounted, this); return returnValue; } diff --git a/src/renderers/dom/shared/HTMLDOMPropertyConfig.js b/src/renderers/dom/shared/HTMLDOMPropertyConfig.js index 9df98bda67..25c6c39481 100644 --- a/src/renderers/dom/shared/HTMLDOMPropertyConfig.js +++ b/src/renderers/dom/shared/HTMLDOMPropertyConfig.js @@ -54,7 +54,7 @@ var HTMLDOMPropertyConfig = { alt: null, async: HAS_BOOLEAN_VALUE, autoComplete: null, - // autoFocus is polyfilled/normalized by AutoFocusMixin + // autoFocus is polyfilled/normalized by AutoFocusUtils // autoFocus: HAS_BOOLEAN_VALUE, autoPlay: HAS_BOOLEAN_VALUE, capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index a8742274f7..49adf3d989 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -20,6 +20,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); +var ReactDOMInput = require('ReactDOMInput'); var ReactMount = require('ReactMount'); var ReactMultiChild = require('ReactMultiChild'); var ReactPerf = require('ReactPerf'); @@ -254,6 +255,7 @@ function ReactDOMComponent(tag) { this._previousStyle = null; this._previousStyleCopy = null; this._rootNodeID = null; + this._wrapperState = null; } ReactDOMComponent.displayName = 'ReactDOMComponent'; @@ -277,7 +279,16 @@ ReactDOMComponent.Mixin = { mountComponent: function(rootID, transaction, context) { this._rootNodeID = rootID; - assertValidProps(this, this._currentElement.props); + var props = this._currentElement.props; + + switch (this._tag) { + case 'input': + ReactDOMInput.mountWrapper(this, props); + props = ReactDOMInput.getNativeProps(this, props, context); + break; + } + + assertValidProps(this, props); if (__DEV__) { if (context[validateDOMNesting.ancestorInfoContextKey]) { validateDOMNesting( @@ -288,8 +299,15 @@ ReactDOMComponent.Mixin = { } } - var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction); - var tagContent = this._createContentMarkup(transaction, context); + var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props); + var tagContent = this._createContentMarkup(transaction, props, context); + + switch (this._tag) { + case 'input': + ReactDOMInput.postMountWrapper(this, transaction, props); + break; + } + if (!tagContent && omittedCloseTags[this._tag]) { return tagOpen + '/>'; } @@ -306,10 +324,10 @@ ReactDOMComponent.Mixin = { * * @private * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction + * @param {object} props * @return {string} Markup of opening tag. */ - _createOpenTagMarkupAndPutListeners: function(transaction) { - var props = this._currentElement.props; + _createOpenTagMarkupAndPutListeners: function(transaction, props) { var ret = '<' + this._tag; for (var propKey in props) { @@ -356,12 +374,12 @@ ReactDOMComponent.Mixin = { * * @private * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction + * @param {object} props * @param {object} context * @return {string} Content markup. */ - _createContentMarkup: function(transaction, context) { + _createContentMarkup: function(transaction, props, context) { var ret = ''; - var props = this._currentElement.props; // Intentional use of != to avoid catching zero/false. var innerHTML = props.dangerouslySetInnerHTML; @@ -427,10 +445,22 @@ ReactDOMComponent.Mixin = { * @overridable */ updateComponent: function(transaction, prevElement, nextElement, context) { - assertValidProps(this, this._currentElement.props); - this._updateDOMProperties(prevElement.props, transaction); + var lastProps = prevElement.props; + var nextProps = this._currentElement.props; + + switch (this._tag) { + case 'input': + ReactDOMInput.updateWrapper(this); + lastProps = ReactDOMInput.getNativeProps(this, lastProps); + nextProps = ReactDOMInput.getNativeProps(this, nextProps); + break; + } + + assertValidProps(this, nextProps); + this._updateDOMProperties(lastProps, nextProps, transaction); this._updateDOMChildren( - prevElement.props, + lastProps, + nextProps, transaction, processChildContext(context, this) ); @@ -449,10 +479,10 @@ ReactDOMComponent.Mixin = { * * @private * @param {object} lastProps + * @param {object} nextProps * @param {ReactReconcileTransaction} transaction */ - _updateDOMProperties: function(lastProps, transaction) { - var nextProps = this._currentElement.props; + _updateDOMProperties: function(lastProps, nextProps, transaction) { var propKey; var styleName; var styleUpdates; @@ -558,11 +588,11 @@ ReactDOMComponent.Mixin = { * children content. * * @param {object} lastProps + * @param {object} nextProps * @param {ReactReconcileTransaction} transaction + * @param {object} context */ - _updateDOMChildren: function(lastProps, transaction, context) { - var nextProps = this._currentElement.props; - + _updateDOMChildren: function(lastProps, nextProps, transaction, context) { var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null; var nextContent = @@ -612,10 +642,17 @@ ReactDOMComponent.Mixin = { * @internal */ unmountComponent: function() { + switch (this._tag) { + case 'input': + ReactDOMInput.unmountWrapper(this); + break; + } + this.unmountChildren(); ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID); ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); this._rootNodeID = null; + this._wrapperState = null; } }; diff --git a/src/renderers/dom/shared/ReactDefaultInjection.js b/src/renderers/dom/shared/ReactDefaultInjection.js index 0e18f6f8f3..3046078c3c 100644 --- a/src/renderers/dom/shared/ReactDefaultInjection.js +++ b/src/renderers/dom/shared/ReactDefaultInjection.js @@ -29,7 +29,6 @@ var ReactDOMForm = require('ReactDOMForm'); var ReactDOMImg = require('ReactDOMImg'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); var ReactDOMIframe = require('ReactDOMIframe'); -var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); var ReactDOMSelect = require('ReactDOMSelect'); var ReactDOMTextarea = require('ReactDOMTextarea'); @@ -120,7 +119,6 @@ function inject() { 'form': ReactDOMForm, 'iframe': ReactDOMIframe, 'img': ReactDOMImg, - 'input': ReactDOMInput, 'option': ReactDOMOption, 'select': ReactDOMSelect, 'textarea': ReactDOMTextarea, diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index dda078a441..0c56d96656 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -308,7 +308,8 @@ describe('ReactDOMComponent', function() { genMarkup = function(props) { var transaction = new ReactReconcileTransaction(); return (new NodeStub(props))._createOpenTagMarkupAndPutListeners( - transaction + transaction, + props ); }; @@ -357,7 +358,11 @@ describe('ReactDOMComponent', function() { genMarkup = function(props) { var transaction = new ReactReconcileTransaction(); - return (new NodeStub(props))._createContentMarkup(transaction, {}); + return (new NodeStub(props))._createContentMarkup( + transaction, + props, + {} + ); }; this.addMatchers({