Skip to content
This repository was archived by the owner on Jul 29, 2025. It is now read-only.

Commit 36469f7

Browse files
author
Matt Goo
authored
fix(text-field): remove foundation.setValue call during onChange (#350)
1 parent e96ca10 commit 36469f7

File tree

5 files changed

+124
-43
lines changed

5 files changed

+124
-43
lines changed

packages/text-field/Input.js

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,10 @@ import classnames from 'classnames';
2727
import {VALIDATION_ATTR_WHITELIST} from '@material/textfield/constants';
2828

2929
export default class Input extends React.Component {
30-
constructor(props) {
31-
super(props);
32-
this.inputElement = React.createRef();
33-
}
30+
inputElement = React.createRef();
31+
state = {wasUserTriggeredChange: false};
3432

3533
componentDidMount() {
36-
this.props.handleValueChange(this.props.value);
3734
if (this.props.id) {
3835
this.props.setInputId(this.props.id);
3936
}
@@ -43,20 +40,46 @@ export default class Input extends React.Component {
4340
}
4441

4542
componentDidUpdate(prevProps) {
43+
const {
44+
id,
45+
handleValueChange,
46+
setInputId,
47+
setDisabled,
48+
foundation,
49+
value,
50+
disabled,
51+
} = this.props;
52+
4653
this.handleValidationAttributeUpdate(prevProps);
4754

48-
if (this.props.disabled !== prevProps.disabled) {
49-
const {disabled} = this.props;
50-
this.props.setDisabled(disabled);
51-
this.props.foundation.setDisabled(disabled);
55+
if (disabled !== prevProps.disabled) {
56+
setDisabled(disabled);
57+
foundation.setDisabled(disabled);
5258
}
5359

54-
if (this.props.id !== prevProps.id) {
55-
this.props.setInputId(this.props.id);
60+
if (id !== prevProps.id) {
61+
setInputId(id);
62+
}
63+
64+
// this should be in the componentDidMount, but foundation is not created
65+
// at that time.
66+
// Will fix this in
67+
// https://github.com/material-components/material-components-web-react/pull/353/files
68+
if (value && foundation !== prevProps.foundation) {
69+
handleValueChange(value, () => foundation.setValue(value));
70+
return;
5671
}
5772

58-
if (this.props.value !== prevProps.value) {
59-
this.props.handleValueChange(this.props.value);
73+
if (value !== prevProps.value) {
74+
handleValueChange(value, () => {
75+
// only call #foundation.setValue on programatic changes;
76+
// not changes by the user.
77+
if (this.state.wasUserTriggeredChange) {
78+
this.setState({wasUserTriggeredChange: false});
79+
} else {
80+
foundation.setValue(value);
81+
}
82+
});
6083
}
6184
}
6285

@@ -96,7 +119,11 @@ export default class Input extends React.Component {
96119
// value of the input is.
97120
handleChange = (e) => {
98121
const {foundation, onChange} = this.props;
122+
// autoCompleteFocus runs on `input` event in MDC Web. In React, onChange and
123+
// onInput are the same event
124+
// https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput
99125
foundation.autoCompleteFocus();
126+
this.setState({wasUserTriggeredChange: true});
100127
onChange(e);
101128
}
102129

packages/text-field/index.js

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class TextField extends React.Component {
3939
constructor(props) {
4040
super(props);
4141
this.floatingLabelElement = React.createRef();
42-
this.inputElement = React.createRef();
42+
this.inputElement_ = React.createRef();
4343

4444
this.state = {
4545
// root state
@@ -76,12 +76,6 @@ class TextField extends React.Component {
7676
this.foundation_.init();
7777
}
7878

79-
componentDidUpdate(prevProps, prevState) {
80-
if (this.state.value !== prevState.value) {
81-
this.foundation_.setValue(this.state.value);
82-
}
83-
}
84-
8579
componentWillUnmount() {
8680
this.foundation_.destroy();
8781
}
@@ -167,9 +161,9 @@ class TextField extends React.Component {
167161
getNativeInput: () => {
168162
let badInput;
169163
let valid;
170-
if (this.inputElement && this.inputElement.current) {
171-
badInput = this.inputElement.current.isBadInput();
172-
valid = this.inputElement.current.isValid();
164+
if (this.inputElement_ && this.inputElement_.current) {
165+
badInput = this.inputElement_.current.isBadInput();
166+
valid = this.inputElement_.current.isValid();
173167
}
174168
const input = {
175169
validity: {badInput, valid},
@@ -231,10 +225,10 @@ class TextField extends React.Component {
231225
return Object.assign({}, props, {
232226
foundation: this.foundation_,
233227
handleFocusChange: (isFocused) => this.setState({isFocused}),
234-
handleValueChange: (value) => this.setState({value}),
228+
handleValueChange: (value, cb) => this.setState({value}, cb),
235229
setDisabled: (disabled) => this.setState({disabled}),
236230
setInputId: (id) => this.setState({inputId: id}),
237-
ref: this.inputElement,
231+
ref: this.inputElement_,
238232
inputType: this.props.textarea ? 'textarea' : 'input',
239233
});
240234
}

test/screenshot/golden.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
"text-field/helper-text": "59604d0f39e0846fc97aae7573d317dded215282a677e4641c5e33426e3a2a1e",
2222
"text-field/icon": "c34dae5444deec222533b3f43448c0393b0c8543a5af4e50cc12d71611f366a7",
2323
"text-field/textArea": "871b32d2fd1982e191e9d5f6ac32e8eb4d82f9fbb1a83359bce8e8f2e9edd027",
24-
"text-field/standard": "1eebb15502b41c43a32af4a799eec507ad87523dfb25b0dc417aded9448cddcd",
25-
"text-field/fullWidth": "b374f46466e92756de433ee34b5f178dccb3b6b7ce25fe43f363e211ed4d6e4e",
26-
"text-field/outlined": "34833e20fadf130cfa5cb6ec697c89f350fb9eb777a394adf604c5f10c259c4c",
24+
"text-field/standard": "be2ea75813583dac8d3ad988282cfa19fa7266a29b095cbd60a34912a1900251",
25+
"text-field/fullWidth": "7c854723b1b4ce7e6df614f44f7b7845bef6098ac30714da5c5128bbd57eb51f",
26+
"text-field/outlined": "5d7fd01cfe503a0651daeb7acdf8163dd39a3b3f0ce3d872613bb15db30400ec",
2727
"top-app-bar/fixed": "7a2dd6318d62ac2eabd66f1b28100db7c15840ccb753660065fa9524db6435d6",
2828
"top-app-bar/prominent": "5a63148610f315001fbf80bd3f4b8ceb37691bd1a7ec81a33228bb3c2b364dae",
2929
"top-app-bar/short": "0e0e8a6c812e93910a540689bc6a962a1c8097c9f4e8b9ca65f35994bb380cfc",

test/unit/text-field/Input.test.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ test('#isValid returns false if input is invalid', () => {
4040
assert.isFalse(isValidInput);
4141
});
4242

43-
test('#componentDidMount should call props.handleValueChange', () => {
44-
const handleValueChange = td.func();
45-
shallow(<Input handleValueChange={handleValueChange} value='woof'/>);
46-
td.verify(handleValueChange('woof'), {times: 1});
47-
});
48-
4943
test('#componentDidMount should call props.setInputId if props.id exists', () => {
5044
const setInputId = td.func();
5145
shallow(<Input setInputId={setInputId} id='best-id'/>);
@@ -141,11 +135,45 @@ test('#componentDidUpdate does nothing if an unrelated property is ' +
141135
td.verify(setInputId(td.matchers.anything), {times: 0});
142136
});
143137

138+
test('#componentDidUpdate calls handleValueChange when the foundation initializes with a value', () => {
139+
const setValue = td.func();
140+
const handleValueChange = td.func();
141+
const wrapper = shallow(<Input value='test value' handleValueChange={handleValueChange} />);
142+
143+
wrapper.setProps({foundation: {setValue}});
144+
td.verify(handleValueChange('test value', td.matchers.isA(Function)), {times: 1});
145+
});
146+
147+
test('#componentDidUpdate calls setValue when the foundation initializes with a value', () => {
148+
const setValue = td.func();
149+
const handleValueChange = (value, cb) => {
150+
cb(value);
151+
};
152+
const wrapper = shallow(<Input value='test value' handleValueChange={handleValueChange} />);
153+
154+
wrapper.setProps({foundation: {setValue}});
155+
td.verify(setValue('test value'), {times: 1});
156+
});
157+
144158
test('props.handleValueChange() is called if this.props.value updates', () => {
145159
const handleValueChange = td.func();
146160
const wrapper = shallow(<Input handleValueChange={handleValueChange} />);
147161
wrapper.setProps({value: 'meow'});
148-
td.verify(handleValueChange('meow'), {times: 1});
162+
td.verify(handleValueChange('meow', td.matchers.isA(Function)), {times: 1});
163+
});
164+
165+
test('foundation.setValue() is called if this.props.value updates', () => {
166+
const setValue = td.func();
167+
const foundation = {setValue};
168+
const handleValueChange = (value, cb) => {
169+
cb(value);
170+
};
171+
const wrapper = shallow(<Input
172+
value='test value'
173+
foundation={foundation}
174+
handleValueChange={handleValueChange} />);
175+
wrapper.setProps({value: 'meow'});
176+
td.verify(setValue('meow'), {times: 1});
149177
});
150178

151179
test('#event.onFocus calls props.handleFocusChange(true)', () => {

test/unit/text-field/index.test.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,6 @@ test('#componentDidMount creates foundation', () => {
7070
assert.exists(wrapper.instance().foundation_);
7171
});
7272

73-
test('#componentDidUpdate calls setValue if state.value updates', () => {
74-
const wrapper = shallow(<TextField label='my label'><Input /></TextField>);
75-
wrapper.instance().foundation_.setValue = td.func();
76-
wrapper.setState({value: 'value'});
77-
td.verify(wrapper.instance().foundation_.setValue('value'), {times: 1});
78-
});
79-
8073
test('#componentDidUpdate does not call setValue if another property updates', () => {
8174
const wrapper = shallow(<TextField label='my label'><Input /></TextField>);
8275
wrapper.instance().foundation_.setValue = td.func();
@@ -143,6 +136,38 @@ test('#adapter.input.getNativeInput.validity.valid returns false for invalid inp
143136
assert.isFalse(valid);
144137
});
145138

139+
test('#adapter.input.getNativeInput.validity.valid returns false for invalid input with email pattern', () => {
140+
const wrapper = mount(<TextField label='my label'>
141+
<Input value='123' pattern='[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$'/>
142+
</TextField>);
143+
const valid = wrapper.instance().foundation_.adapter_.getNativeInput().validity.valid;
144+
assert.isFalse(valid);
145+
});
146+
147+
test('#adapter.input.getNativeInput.validity.valid returns false for required field with no value', () => {
148+
const wrapper = mount(<TextField label='my label'>
149+
<Input value='' required/>
150+
</TextField>);
151+
const valid = wrapper.instance().foundation_.adapter_.getNativeInput().validity.valid;
152+
assert.isFalse(valid);
153+
});
154+
155+
test('#adapter.input.getNativeInput.validity.valid returns true for required field with value', () => {
156+
const wrapper = mount(<TextField label='my label'>
157+
<Input value='value' required/>
158+
</TextField>);
159+
const valid = wrapper.instance().foundation_.adapter_.getNativeInput().validity.valid;
160+
assert.isTrue(valid);
161+
});
162+
163+
test('#adapter.input.getNativeInput.validity.valid returns true for valid email', () => {
164+
const wrapper = mount(<TextField label='my label'>
165+
<Input value='chevy@gmail.com' pattern='[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$'/>
166+
</TextField>);
167+
const valid = wrapper.instance().foundation_.adapter_.getNativeInput().validity.valid;
168+
assert.isTrue(valid);
169+
});
170+
146171
test('#get adapter.input.value returns state.value', () => {
147172
const wrapper = shallow(<TextField label='my label'><Input /></TextField>);
148173
wrapper.setState({value: '123'});
@@ -353,10 +378,17 @@ test('#inputProps.handleFocusChange updates state.isFocused', () => {
353378

354379
test('#inputProps.handleValueChange updates state.value', () => {
355380
const wrapper = shallow(<TextField label='my label'><Input /></TextField>);
356-
wrapper.instance().inputProps({}).handleValueChange('meow');
381+
wrapper.instance().inputProps({}).handleValueChange('meow', td.func());
357382
assert.equal(wrapper.state().value, 'meow');
358383
});
359384

385+
test('#inputProps.handleValueChange calls cb after state is set', () => {
386+
const wrapper = shallow(<TextField label='my label'><Input /></TextField>);
387+
const callback = td.func();
388+
wrapper.instance().inputProps({}).handleValueChange('meow', callback);
389+
td.verify(callback(), {times: 1});
390+
});
391+
360392
test('#inputProps.setDisabled updates state.disabled', () => {
361393
const wrapper = shallow(<TextField label='my label'><Input /></TextField>);
362394
wrapper.instance().inputProps({}).setDisabled(true);

0 commit comments

Comments
 (0)