diff --git a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js index d9ffa5ba3e9b2..d61b5cca2be0d 100644 --- a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js +++ b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js @@ -40,6 +40,32 @@ describe('ReactDOMInput', () => { spyOn(console, 'error'); }); + it('should properly control a value even if no event listener exists', () => { + var container = document.createElement('div'); + var stub = ReactDOM.render( + , + container + ); + + document.body.appendChild(container); + + var node = ReactDOM.findDOMNode(stub); + expect(console.error.calls.count()).toBe(1); + + // Simulate a native change event + setUntrackedValue(node, 'giraffe'); + + // This must use the native event dispatching. If we simulate, we will + // bypass the lazy event attachment system so we won't actually test this. + var nativeEvent = document.createEvent('Event'); + nativeEvent.initEvent('change', true, true); + node.dispatchEvent(nativeEvent); + + expect(node.value).toBe('lion'); + + document.body.removeChild(container); + }); + it('should display `defaultValue` of number 0', () => { var stub = ; stub = ReactTestUtils.renderIntoDocument(stub); @@ -455,6 +481,7 @@ describe('ReactDOMInput', () => { expect(console.error.calls.count()).toBe(1); }); + it('should have a this value of undefined if bind is not used', () => { var unboundInputOnChange = function() { expect(this).toBe(undefined); diff --git a/src/renderers/dom/stack/client/ReactDOMComponent.js b/src/renderers/dom/stack/client/ReactDOMComponent.js index bc23bc144fddf..e0fd44e83da37 100644 --- a/src/renderers/dom/stack/client/ReactDOMComponent.js +++ b/src/renderers/dom/stack/client/ReactDOMComponent.js @@ -228,6 +228,25 @@ function enqueuePutListener(inst, registrationName, listener, transaction) { }); } +// TODO: This is coming from future #8192. Dedupe this and enqueuePutListener. +function ensureListeningTo(inst, registrationName, transaction) { + if (transaction instanceof ReactServerRenderingTransaction) { + return; + } + if (__DEV__) { + // IE8 has no API for event capturing and the `onScroll` event doesn't + // bubble. + warning( + registrationName !== 'onScroll' || isEventSupported('scroll', true), + 'This browser doesn\'t support the `onScroll` event' + ); + } + var containerInfo = inst._hostContainerInfo; + var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE; + var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; + listenTo(registrationName, doc); +} + function putListener() { var listenerToPut = this; EventPluginHub.putListener( @@ -561,6 +580,9 @@ ReactDOMComponent.Mixin = { props = ReactDOMInput.getHostProps(this, props); transaction.getReactMountReady().enqueue(trackInputValue, this); transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this); + // For controlled components we always need to ensure we're listening + // to onChange. Even if there is no listener. + ensureListeningTo(this, 'onChange', transaction); break; case 'option': ReactDOMOption.mountWrapper(this, props, hostParent); @@ -570,12 +592,18 @@ ReactDOMComponent.Mixin = { ReactDOMSelect.mountWrapper(this, props, hostParent); props = ReactDOMSelect.getHostProps(this, props); transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this); + // For controlled components we always need to ensure we're listening + // to onChange. Even if there is no listener. + ensureListeningTo(this, 'onChange', transaction); break; case 'textarea': ReactDOMTextarea.mountWrapper(this, props, hostParent); props = ReactDOMTextarea.getHostProps(this, props); transaction.getReactMountReady().enqueue(trackInputValue, this); transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this); + // For controlled components we always need to ensure we're listening + // to onChange. Even if there is no listener. + ensureListeningTo(this, 'onChange', transaction); break; }