@samitouri / QOS-React-2 / commits / fb6381fb35
Fork 0

The library for web and native user interfaces.

Upgrade TextChangeEventPlugin to ChangeEventPlugin and support more form elements

Upgrade `TextChangeEventPlugin` to be the `onChange` event that React fires. In React, `onChange` will now fire when `input` fires for form elements in modern browsers. Handle this for: input[type=text] input[type=password] input[type=checkbox] input[type=radio] textarea select Support: - OSX Chrome - OSX Safari - OSX Firefox - Win 7 / IE8 - Win 7 / IE9 - Win 7 / IE10 Everything works but caret selection / placement differs from browser to browser. For <select> elements, the event is fired with `change`. This is a conscious decision, even though in some browsers (OSX firefox, IE), it can be argued that the event should fire more due to how the UI looks. Builds on https://github.com/facebook/react/pull/75, which handled only text inputs.

CommitSyncScript committed Jun 25, 2013 at 13:56 UTC fb6381fb35c8af868b5134c3d7a91699a7d01090
5 files changed +179 -86
src/core/ReactDefaultInjection.js
+2 -2
@@ -23,7 +23,7 @@ var ReactDOMForm = require('ReactDOMForm');
23
24 var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
25 var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
26 -var TextChangeEventPlugin = require('TextChangeEventPlugin');
26 +var ChangeEventPlugin = require('ChangeEventPlugin');
27 var EventPluginHub = require('EventPluginHub');
28 var ReactInstanceHandles = require('ReactInstanceHandles');
29 var SimpleEventPlugin = require('SimpleEventPlugin');
@@ -42,7 +42,7 @@ function inject() {
42 EventPluginHub.injection.injectEventPluginsByName({
43 'SimpleEventPlugin': SimpleEventPlugin,
44 'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
45 - 'TextChangeEventPlugin': TextChangeEventPlugin
45 + 'ChangeEventPlugin': ChangeEventPlugin
46 });
47
48 /**
src/dom/components/ReactDOMInput.js
+1 -19
@@ -66,13 +66,7 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
66
67 props.checked = this.getChecked();
68 props.value = this.getValue();
69 -
70 - // TODO: Re-implement `onChange` with the implementation of `onTextChange`.
71 - if (props.type === 'text' || props.type === 'password') {
72 - props.onTextChange = this.handleTextChange;
73 - } else {
74 - props.onChange = this.handleChange;
75 - }
69 + props.onChange = this.handleChange;
70
71 return input(props, this.props.children);
72 },
@@ -94,18 +88,6 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
88 }
89 },
90
97 - handleTextChange: ReactCompositeComponent.autoBind(function(event) {
98 - var returnValue;
99 - if (this.props.onTextChange) {
100 - returnValue = this.props.onTextChange(event);
101 - }
102 - this.setState({
103 - checked: event.target.checked,
104 - value: event.target.value
105 - });
106 - return returnValue;
107 - }),
108 -
91 handleChange: ReactCompositeComponent.autoBind(function(event) {
92 var returnValue;
93 if (this.props.onChange) {
src/eventPlugins/ChangeEventPlugin.js renamed
+175 -56
@@ -13,7 +13,7 @@
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 - * @providesModule TextChangeEventPlugin
16 + * @providesModule ChangeEventPlugin
17 */
18
19 "use strict";
@@ -30,41 +30,125 @@ var keyOf = require('keyOf');
30 var topLevelTypes = EventConstants.topLevelTypes;
31
32 var eventTypes = {
33 - textChange: {
33 + change: {
34 phasedRegistrationNames: {
35 - bubbled: keyOf({onTextChange: null}),
36 - captured: keyOf({onTextChangeCapture: null})
35 + bubbled: keyOf({onChange: null}),
36 + captured: keyOf({onChangeCapture: null})
37 }
38 }
39 };
40
41 -var isInputSupported;
41 +/**
42 + * For IE shims
43 + */
44 +var activeElement = null;
45 +var activeElementID = null;
46 +var activeElementValue = null;
47 +var activeElementValueProp = null;
48 +
49 +
50 +/**
51 + * SECTION: handle `change` event
52 + */
53 +var shouldUseChangeEvent = function(elem) {
54 + return elem.nodeName === 'SELECT';
55 +};
56 +
57 +var doesChangeEventBubble = false;
58 +if (ExecutionEnvironment.canUseDOM) {
59 + // See `handleChange` comment below
60 + doesChangeEventBubble = isEventSupported('change') && (
61 + !('documentMode' in document) || document.documentMode > 8
62 + );
63 +}
64 +
65 +/**
66 + * (For IE8). The `change` event does not bubble in IE8 so we add a listener
67 + * when the element is focused (and remove when blurred).
68 + */
69 +var handleChange = function(nativeEvent) {
70 + var event = SyntheticEvent.getPooled(
71 + eventTypes.change,
72 + activeElementID,
73 + nativeEvent
74 + );
75 + EventPropagators.accumulateTwoPhaseDispatches(event);
76 +
77 + // If change bubbled, we'd just bind to it like all the other events
78 + // and have it go through ReactEventTopLevelCallback. Since it doesn't, we
79 + // manually listen for the change event and so we have to enqueue and
80 + // process the abstract event manually.
81 + EventPluginHub.enqueueEvents(event);
82 + EventPluginHub.processEventQueue();
83 +};
84 +
85 +var getTargetIDForChangeEvent;
86 +if (doesChangeEventBubble) {
87 + getTargetIDForChangeEvent = function(
88 + topLevelType,
89 + topLevelTarget,
90 + topLevelTargetID) {
91 + if (topLevelType === topLevelTypes.topChange) {
92 + return topLevelTargetID;
93 + }
94 + };
95 +} else {
96 + getTargetIDForChangeEvent = function(
97 + topLevelType,
98 + topLevelTarget,
99 + topLevelTargetID) {
100 + if (topLevelType === topLevelTypes.topFocus) {
101 + activeElementID = topLevelTargetID;
102 + topLevelTarget.attachEvent('onchange', handleChange);
103 + } else if (topLevelType === topLevelTypes.topBlur) {
104 + topLevelTarget.detachEvent('onchange', handleChange);
105 + activeElementID = null;
106 + }
107 + };
108 +}
109 +
110 +
111 +/**
112 + * SECTION: handle `input` event
113 + */
114 +var isInputEventSupported = false;
115 if (ExecutionEnvironment.canUseDOM) {
116 // IE9 claims to support the input event but fails to trigger it when
117 // deleting text, so we ignore its input events
45 - isInputSupported = isEventSupported('input') && (
46 - !("documentMode" in document) || document.documentMode > 9
118 + isInputEventSupported = isEventSupported('input') && (
119 + !('documentMode' in document) || document.documentMode > 9
120 );
121 }
122
50 -var hasInputCapabilities = function(elem) {
51 - // The HTML5 spec lists many more types than `text` and `password` on which
52 - // the input event is triggered but none of them exist in old IE, so we don't
53 - // check them here.
54 - // TODO: <textarea> should be supported too but IE seems to reset the
55 - // selection when changing textarea contents during a selectionchange event
56 - // so it's not listed here for now.
123 +
124 +/**
125 + * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
126 + */
127 +var supportedInputTypes = {
128 + 'color': true,
129 + 'date': true,
130 + 'datetime': true,
131 + 'datetime-local': true,
132 + 'email': true,
133 + 'month': true,
134 + 'number': true,
135 + 'password': true,
136 + 'range': true,
137 + 'search': true,
138 + 'tel': true,
139 + 'text': true,
140 + 'time': true,
141 + 'url': true,
142 + 'week': true
143 +};
144 +
145 +var shouldUseInputEvent = function(elem) {
146 return (
58 - elem.nodeName === 'INPUT' &&
59 - (elem.type === 'text' || elem.type === 'password')
147 + (elem.nodeName === 'INPUT' && supportedInputTypes[elem.type]) ||
148 + elem.nodeName === 'TEXTAREA'
149 );
150 };
151
63 -var activeElement = null;
64 -var activeElementID = null;
65 -var activeElementValue = null;
66 -var activeElementValueProp = null;
67 -
152 /**
153 * (For old IE.) Replacement getter/setter for the `value` property that gets
154 * set on the active element.
@@ -84,7 +168,7 @@ var newValueProp = {
168 * and override the value property so that we can distinguish user events from
169 * value changes in JS.
170 */
87 -var startWatching = function(target, targetID) {
171 +var startWatchingForValueChange = function(target, targetID) {
172 activeElement = target;
173 activeElementID = targetID;
174 activeElementValue = target.value;
@@ -101,7 +185,7 @@ var startWatching = function(target, targetID) {
185 * (For old IE.) Removes the event listeners from the currently-tracked element,
186 * if any exists.
187 */
104 -var stopWatching = function() {
188 +var stopWatchingForValueChange = function() {
189 if (!activeElement) {
190 return;
191 }
@@ -117,11 +201,11 @@ var stopWatching = function() {
201 };
202
203 /**
120 - * (For old IE.) Handles a propertychange event, sending a textChange event if
204 + * (For old IE.) Handles a propertychange event, sending a `change` event if
205 * the value of the active element has changed.
206 */
207 var handlePropertyChange = function(nativeEvent) {
124 - if (nativeEvent.propertyName !== "value") {
208 + if (nativeEvent.propertyName !== 'value') {
209 return;
210 }
211 var value = nativeEvent.srcElement.value;
@@ -131,42 +215,34 @@ var handlePropertyChange = function(nativeEvent) {
215 activeElementValue = value;
216
217 var event = SyntheticEvent.getPooled(
134 - eventTypes.textChange,
218 + eventTypes.change,
219 activeElementID,
220 nativeEvent
221 );
222 EventPropagators.accumulateTwoPhaseDispatches(event);
223
140 - // If propertychange bubbled, we'd just bind to it like all the other events
141 - // and have it go through ReactEventTopLevelCallback. Since it doesn't, we
142 - // manually listen for the propertychange event and so we have to enqueue and
143 - // process the abstract event manually.
224 + // See comment above in handleChange
225 EventPluginHub.enqueueEvents(event);
226 EventPluginHub.processEventQueue();
227 };
228
229 /**
149 - * If a textChange event should be fired, returns the target's ID.
230 + * If a `change` event should be fired, returns the target's ID.
231 */
151 -var targetIDForTextChangeEvent;
152 -if (isInputSupported) {
153 - targetIDForTextChangeEvent = function(
232 +var getTargetIDForValueChangeEvent;
233 +if (isInputEventSupported) {
234 + getTargetIDForValueChangeEvent = function(
235 topLevelType,
236 topLevelTarget,
237 topLevelTargetID) {
238 if (topLevelType === topLevelTypes.topInput) {
239 // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
159 - // what we want so fall through here and trigger an abstract event...
160 - if (topLevelTarget.nodeName === 'TEXTAREA') {
161 - // ...unless it's a textarea, in which case we don't fire an event (so
162 - // that we have consistency with our old-IE shim).
163 - return;
164 - }
240 + // what we want so fall through here and trigger an abstract event
241 return topLevelTargetID;
242 }
243 };
244 } else {
169 - targetIDForTextChangeEvent = function(
245 + getTargetIDForValueChangeEvent = function(
246 topLevelType,
247 topLevelTarget,
248 topLevelTargetID) {
@@ -181,14 +257,13 @@ if (isInputSupported) {
257 // In either case, we don't want to call the event handler if the value
258 // is changed from JS so we redefine a setter for `.value` that updates
259 // our activeElementValue variable, allowing us to ignore those changes
184 - if (hasInputCapabilities(topLevelTarget)) {
185 - // stopWatching() should be a noop here but we call it just in case we
186 - // missed a blur event somehow.
187 - stopWatching();
188 - startWatching(topLevelTarget, topLevelTargetID);
189 - }
260 + //
261 + // stopWatching() should be a noop here but we call it just in case we
262 + // missed a blur event somehow.
263 + stopWatchingForValueChange();
264 + startWatchingForValueChange(topLevelTarget, topLevelTargetID);
265 } else if (topLevelType === topLevelTypes.topBlur) {
191 - stopWatching();
266 + stopWatchingForValueChange();
267 } else if (
268 topLevelType === topLevelTypes.topSelectionChange ||
269 topLevelType === topLevelTypes.topKeyUp ||
@@ -211,7 +286,40 @@ if (isInputSupported) {
286 };
287 }
288
214 -var TextChangeEventPlugin = {
289 +
290 +/**
291 + * SECTION: handle `click` event
292 + */
293 +var shouldUseClickEvent = function(elem) {
294 + // Use the `click` event to detect changes to checkbox and radio inputs.
295 + // This approach works across all browsers, whereas `change` does not fire
296 + // until `blur` in IE8.
297 + return (
298 + elem.nodeName === 'INPUT' &&
299 + (elem.type === 'checkbox' || elem.type === 'radio')
300 + );
301 +};
302 +
303 +var getTargetIDForClickEvent = function(
304 + topLevelType,
305 + topLevelTarget,
306 + topLevelTargetID) {
307 + if (topLevelType === topLevelTypes.topClick) {
308 + return topLevelTargetID;
309 + }
310 +};
311 +
312 +/**
313 + * This plugin creates an `onChange` event that normalizes change events
314 + * across form elements. This event fires at a time when it's possible to
315 + * change the element's value without seeing a flicker.
316 + *
317 + * Supported elements are:
318 + * - input (see `supportedInputTypes`)
319 + * - textarea
320 + * - select
321 + */
322 +var ChangeEventPlugin = {
323
324 eventTypes: eventTypes,
325
@@ -228,15 +336,26 @@ var TextChangeEventPlugin = {
336 topLevelTarget,
337 topLevelTargetID,
338 nativeEvent) {
231 - var targetID = targetIDForTextChangeEvent(
232 - topLevelType,
233 - topLevelTarget,
234 - topLevelTargetID
235 - );
339 +
340 + var getTargetIDFunc, targetID;
341 + if (shouldUseChangeEvent(topLevelTarget)) {
342 + getTargetIDFunc = getTargetIDForChangeEvent;
343 + } else if (shouldUseInputEvent(topLevelTarget)) {
344 + getTargetIDFunc = getTargetIDForValueChangeEvent;
345 + } else if (shouldUseClickEvent(topLevelTarget)) {
346 + getTargetIDFunc = getTargetIDForClickEvent;
347 + }
348 + if (getTargetIDFunc) {
349 + targetID = getTargetIDFunc(
350 + topLevelType,
351 + topLevelTarget,
352 + topLevelTargetID
353 + );
354 + }
355
356 if (targetID) {
357 var event = SyntheticEvent.getPooled(
239 - eventTypes.textChange,
358 + eventTypes.change,
359 targetID,
360 nativeEvent
361 );
@@ -247,4 +366,4 @@ var TextChangeEventPlugin = {
366
367 };
368
250 -module.exports = TextChangeEventPlugin;
369 +module.exports = ChangeEventPlugin;
src/eventPlugins/DefaultEventPluginOrder.js
+1 -1
@@ -34,7 +34,7 @@ var DefaultEventPluginOrder = [
34 keyOf({SimpleEventPlugin: null}),
35 keyOf({TapEventPlugin: null}),
36 keyOf({EnterLeaveEventPlugin: null}),
37 - keyOf({TextChangeEventPlugin: null}),
37 + keyOf({ChangeEventPlugin: null}),
38 keyOf({AnalyticsEventPlugin: null})
39 ];
40
src/eventPlugins/SimpleEventPlugin.js
-8
@@ -41,12 +41,6 @@ var eventTypes = {
41 captured: keyOf({onBlurCapture: true})
42 }
43 },
44 - change: {
45 - phasedRegistrationNames: {
46 - bubbled: keyOf({onChange: true}),
47 - captured: keyOf({onChangeCapture: true})
48 - }
49 - },
44 click: {
45 phasedRegistrationNames: {
46 bubbled: keyOf({onClick: true}),
@@ -209,7 +203,6 @@ var eventTypes = {
203
204 var topLevelEventsToDispatchConfig = {
205 topBlur: eventTypes.blur,
212 - topChange: eventTypes.change,
206 topClick: eventTypes.click,
207 topDoubleClick: eventTypes.doubleClick,
208 topDOMCharacterDataModified: eventTypes.DOMCharacterDataModified,
@@ -277,7 +270,6 @@ var SimpleEventPlugin = {
270 }
271 var EventConstructor;
272 switch(topLevelType) {
280 - case topLevelTypes.topChange:
273 case topLevelTypes.topInput:
274 case topLevelTypes.topSubmit:
275 // HTML Events