-
Notifications
You must be signed in to change notification settings - Fork 51k
Move input valueTracker to DOM nodes #10208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a9d4104
c3b0eac
b15092e
12317ad
b4f3fd2
f012837
04ac179
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ var ReactTestUtils = require('react-dom/test-utils'); | |
| // TODO: can we express this test with only public API? | ||
| var inputValueTracking = require('inputValueTracking'); | ||
|
|
||
| var getTracker = inputValueTracking._getTrackerFromNode; | ||
|
|
||
| describe('inputValueTracking', () => { | ||
| var input, checkbox, mockComponent; | ||
|
|
||
|
|
@@ -28,33 +30,30 @@ describe('inputValueTracking', () => { | |
| mockComponent = {_hostNode: input, _wrapperState: {}}; | ||
| }); | ||
|
|
||
| it('should attach tracker to wrapper state', () => { | ||
| inputValueTracking.track(mockComponent); | ||
| it('should attach tracker to node', () => { | ||
| var node = ReactTestUtils.renderIntoDocument(<input type="text" />); | ||
|
|
||
| expect(mockComponent._wrapperState.hasOwnProperty('valueTracker')).toBe( | ||
| true, | ||
| ); | ||
| expect(node.hasOwnProperty('_valueTracker')).toBe(true); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can also use |
||
| }); | ||
|
|
||
| it('should define `value` on the instance node', () => { | ||
| inputValueTracking.track(mockComponent); | ||
| it('should define `value` on the node instance', () => { | ||
| var node = ReactTestUtils.renderIntoDocument(<input type="text" />); | ||
|
|
||
| expect(input.hasOwnProperty('value')).toBe(true); | ||
| expect(node.hasOwnProperty('value')).toBe(true); | ||
| }); | ||
|
|
||
| it('should define `checked` on the instance node', () => { | ||
| mockComponent._hostNode = checkbox; | ||
| inputValueTracking.track(mockComponent); | ||
| it('should define `checked` on the node instance', () => { | ||
| var node = ReactTestUtils.renderIntoDocument(<input type="checkbox" />); | ||
|
|
||
| expect(checkbox.hasOwnProperty('checked')).toBe(true); | ||
| expect(node.hasOwnProperty('checked')).toBe(true); | ||
| }); | ||
|
|
||
| it('should initialize with the current value', () => { | ||
| input.value = 'foo'; | ||
|
|
||
| inputValueTracking.track(mockComponent); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(input); | ||
|
|
||
| expect(tracker.getValue()).toEqual('foo'); | ||
| }); | ||
|
|
@@ -64,92 +63,95 @@ describe('inputValueTracking', () => { | |
| checkbox.checked = true; | ||
| inputValueTracking.track(mockComponent); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(checkbox); | ||
|
|
||
| expect(tracker.getValue()).toEqual('true'); | ||
| }); | ||
|
|
||
| it('should track value changes', () => { | ||
| input.value = 'foo'; | ||
|
|
||
| inputValueTracking.track(mockComponent); | ||
| var node = ReactTestUtils.renderIntoDocument( | ||
| <input type="text" defaultValue="foo" />, | ||
| ); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(node); | ||
|
|
||
| input.value = 'bar'; | ||
| node.value = 'bar'; | ||
| expect(tracker.getValue()).toEqual('bar'); | ||
| }); | ||
|
|
||
| it('should tracked`checked` changes', () => { | ||
| mockComponent._hostNode = checkbox; | ||
| checkbox.checked = true; | ||
| inputValueTracking.track(mockComponent); | ||
| var node = ReactTestUtils.renderIntoDocument( | ||
| <input type="checkbox" defaultChecked={true} />, | ||
| ); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(node); | ||
|
|
||
| checkbox.checked = false; | ||
| node.checked = false; | ||
| expect(tracker.getValue()).toEqual('false'); | ||
| }); | ||
|
|
||
| it('should update value manually', () => { | ||
| input.value = 'foo'; | ||
| inputValueTracking.track(mockComponent); | ||
| var node = ReactTestUtils.renderIntoDocument( | ||
| <input type="text" defaultValue="foo" />, | ||
| ); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(node); | ||
|
|
||
| tracker.setValue('bar'); | ||
| expect(tracker.getValue()).toEqual('bar'); | ||
| }); | ||
|
|
||
| it('should coerce value to a string', () => { | ||
| input.value = 'foo'; | ||
| inputValueTracking.track(mockComponent); | ||
| var node = ReactTestUtils.renderIntoDocument( | ||
| <input type="text" defaultValue="foo" />, | ||
| ); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(node); | ||
|
|
||
| tracker.setValue(500); | ||
| expect(tracker.getValue()).toEqual('500'); | ||
| }); | ||
|
|
||
| it('should update value if it changed and return result', () => { | ||
| inputValueTracking.track(mockComponent); | ||
| input.value = 'foo'; | ||
| var node = ReactTestUtils.renderIntoDocument( | ||
| <input type="text" defaultValue="foo" />, | ||
| ); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| var tracker = getTracker(node); | ||
|
|
||
| expect(inputValueTracking.updateValueIfChanged(mockComponent)).toBe(false); | ||
| expect(inputValueTracking.updateNodeValueIfChanged(node)).toBe(false); | ||
|
|
||
| tracker.setValue('bar'); | ||
|
|
||
| expect(inputValueTracking.updateValueIfChanged(mockComponent)).toBe(true); | ||
| expect(inputValueTracking.updateNodeValueIfChanged(node)).toBe(true); | ||
|
|
||
| expect(tracker.getValue()).toEqual('foo'); | ||
| }); | ||
|
|
||
| it('should track value and return true when updating untracked instance', () => { | ||
| it('should return true when updating untracked instance', () => { | ||
| input.value = 'foo'; | ||
|
|
||
| expect(inputValueTracking.updateValueIfChanged(mockComponent)).toBe(true); | ||
|
|
||
| var tracker = mockComponent._wrapperState.valueTracker; | ||
| expect(tracker.getValue()).toEqual('foo'); | ||
| expect(getTracker(input)).not.toBeDefined(); | ||
| }); | ||
|
|
||
| it('should return tracker from node', () => { | ||
| var div = document.createElement('div'); | ||
| var node = ReactDOM.render(<input type="text" defaultValue="foo" />, div); | ||
| var tracker = inputValueTracking._getTrackerFromNode(node); | ||
|
|
||
| var tracker = getTracker(node); | ||
| expect(tracker.getValue()).toEqual('foo'); | ||
| }); | ||
|
|
||
| it('should stop tracking', () => { | ||
| inputValueTracking.track(mockComponent); | ||
|
|
||
| expect(mockComponent._wrapperState.valueTracker).not.toEqual(null); | ||
| expect(getTracker(input)).not.toEqual(null); | ||
|
|
||
| inputValueTracking.stopTracking(mockComponent); | ||
|
|
||
| expect(mockComponent._wrapperState.valueTracker).toEqual(null); | ||
| expect(getTracker(input)).toEqual(null); | ||
|
|
||
| expect(input.hasOwnProperty('value')).toBe(false); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,6 @@ | |
|
|
||
| 'use strict'; | ||
|
|
||
| var {ELEMENT_NODE} = require('HTMLNodeType'); | ||
| import type {Fiber} from 'ReactFiber'; | ||
| import type {ReactInstance} from 'ReactInstanceType'; | ||
|
|
||
|
|
@@ -21,16 +20,12 @@ type ValueTracker = { | |
| setValue(value: string): void, | ||
| stopTracking(): void, | ||
| }; | ||
| type WrapperState = {_wrapperState: {valueTracker: ?ValueTracker}}; | ||
| type ElementWithWrapperState = Element & WrapperState; | ||
| type InstanceWithWrapperState = ReactInstance & WrapperState; | ||
| type SubjectWithWrapperState = | ||
| | InstanceWithWrapperState | ||
| | ElementWithWrapperState; | ||
| type WrapperState = {_valueTracker: ?ValueTracker}; | ||
| type ElementWithValueTracker = HTMLInputElement & WrapperState; | ||
|
|
||
| var ReactDOMComponentTree = require('ReactDOMComponentTree'); | ||
|
|
||
| function isCheckable(elem: any) { | ||
| function isCheckable(elem: HTMLInputElement) { | ||
| var type = elem.type; | ||
| var nodeName = elem.nodeName; | ||
| return ( | ||
|
|
@@ -40,26 +35,30 @@ function isCheckable(elem: any) { | |
| ); | ||
| } | ||
|
|
||
| function getTracker(inst: any) { | ||
| if (typeof inst.tag === 'number') { | ||
| inst = inst.stateNode; | ||
| } | ||
| return inst._wrapperState.valueTracker; | ||
| function getTracker(node: ElementWithValueTracker) { | ||
| return node._valueTracker; | ||
| } | ||
|
|
||
| function detachTracker(subject: SubjectWithWrapperState) { | ||
| subject._wrapperState.valueTracker = null; | ||
| function detachTracker(subject: ElementWithValueTracker) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. subject => node? |
||
| subject._valueTracker = null; | ||
| } | ||
|
|
||
| function getValueFromNode(node: any) { | ||
| var value; | ||
| if (node) { | ||
| value = isCheckable(node) ? '' + node.checked : node.value; | ||
| function getValueFromNode(node: HTMLInputElement): string { | ||
| var value = ''; | ||
| if (!node) { | ||
| return value; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It used to return
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gaearon I think it's fine,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah the value is only ever used to compare to itself...and actually |
||
| } | ||
|
|
||
| if (isCheckable(node)) { | ||
| value = node.checked ? 'true' : 'false'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this just a stylistic change or does this affect the semantics at all?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. was a fight between eslint and flow. flow wants
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. flow complains about an invalid coercion, its being a bit over protective
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. I don't mind it being explicit. |
||
| } else { | ||
| value = node.value; | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
|
|
||
| function trackValueOnNode(node: any, inst: any): ?ValueTracker { | ||
| function trackValueOnNode(node: any): ?ValueTracker { | ||
| var valueField = isCheckable(node) ? 'checked' : 'value'; | ||
| var descriptor = Object.getOwnPropertyDescriptor( | ||
| node.constructor.prototype, | ||
|
|
@@ -100,7 +99,7 @@ function trackValueOnNode(node: any, inst: any): ?ValueTracker { | |
| currentValue = '' + value; | ||
| }, | ||
| stopTracking() { | ||
| detachTracker(inst); | ||
| detachTracker(node); | ||
| delete node[valueField]; | ||
| }, | ||
| }; | ||
|
|
@@ -109,70 +108,61 @@ function trackValueOnNode(node: any, inst: any): ?ValueTracker { | |
|
|
||
| var inputValueTracking = { | ||
| // exposed for testing | ||
| _getTrackerFromNode(node: ElementWithWrapperState) { | ||
| return getTracker(ReactDOMComponentTree.getInstanceFromNode(node)); | ||
| _getTrackerFromNode(inst: ElementWithValueTracker) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like it should be |
||
| return getTracker(inst); | ||
| }, | ||
|
|
||
| trackNode(node: ElementWithWrapperState) { | ||
| _getTrackerFromInstance(inst: ReactInstance | Fiber) { | ||
| return getTracker(ReactDOMComponentTree.getNodeFromInstance(inst)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we not expose it? Since only tests use it and they already have access to DOM node.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure 👍 |
||
| }, | ||
|
|
||
| trackNode(node: ElementWithValueTracker) { | ||
| if (getTracker(node)) { | ||
| return; | ||
| } | ||
| node._wrapperState.valueTracker = trackValueOnNode(node, node); | ||
|
|
||
| // TODO: Once it's just Fiber we can move this to node._wrapperState | ||
| node._valueTracker = trackValueOnNode(node); | ||
| }, | ||
|
|
||
| track(inst: InstanceWithWrapperState) { | ||
| if (getTracker(inst)) { | ||
| return; | ||
| } | ||
| var node = ReactDOMComponentTree.getNodeFromInstance(inst); | ||
| inst._wrapperState.valueTracker = trackValueOnNode(node, inst); | ||
| track(inst: ReactInstance | Fiber) { | ||
| inputValueTracking.trackNode( | ||
| ReactDOMComponentTree.getNodeFromInstance(inst), | ||
| ); | ||
| }, | ||
|
|
||
| // TODO: in practice "subject" can currently be Stack instance, | ||
| // Fiber, or DOM node. This is hard to understand. We should either | ||
| // make it accept only DOM nodes, or only Fiber/Stack instances. | ||
| updateValueIfChanged(subject: SubjectWithWrapperState | Fiber) { | ||
| if (!subject) { | ||
| updateNodeValueIfChanged(node: ElementWithValueTracker) { | ||
| if (!node) { | ||
| return false; | ||
| } | ||
|
|
||
| var isNode = (subject: any).nodeType === ELEMENT_NODE; | ||
| var tracker = getTracker(subject); | ||
| var tracker = getTracker(node); | ||
| // if there is no tracker at this point it's unlikely | ||
| // that trying again will succeed | ||
| if (!tracker) { | ||
| if (isNode) { | ||
| // DOM node | ||
| inputValueTracking.trackNode((subject: any)); | ||
| } else if (typeof (subject: any).tag === 'number') { | ||
| // Fiber | ||
| inputValueTracking.trackNode((subject: any).stateNode); | ||
| } else { | ||
| // Stack | ||
| inputValueTracking.track((subject: any)); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| var node; | ||
| if (isNode) { | ||
| // DOM node | ||
| node = subject; | ||
| } else { | ||
| // Fiber and Stack | ||
| node = ReactDOMComponentTree.getNodeFromInstance(subject); | ||
| } | ||
|
|
||
| var lastValue = tracker.getValue(); | ||
| var nextValue = getValueFromNode(node); | ||
| if (nextValue !== lastValue) { | ||
| tracker.setValue(nextValue); | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }, | ||
|
|
||
| stopTracking(inst: InstanceWithWrapperState | Fiber) { | ||
| var tracker = getTracker(inst); | ||
| updateValueIfChanged(instOrFiber: ReactInstance | Fiber) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about always accepting nodes instead? Since we immediately get the node anyway.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fine with me, it just means we need to call getNode at the call sites: ReactDOMComponent and ChangeEventPlugin |
||
| if (!instOrFiber) { | ||
| return false; | ||
| } | ||
| return inputValueTracking.updateNodeValueIfChanged( | ||
| ReactDOMComponentTree.getNodeFromInstance(instOrFiber), | ||
| ); | ||
| }, | ||
|
|
||
| stopTracking(inst: ReactInstance) { | ||
| var tracker = getTracker(ReactDOMComponentTree.getNodeFromInstance(inst)); | ||
| if (tracker) { | ||
| tracker.stopTracking(); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove
mockComponentcompletely?