Skip to content

Commit 9a6a3dd

Browse files
authored
[Input] should accommodate number and string values (#7791)
* [Input] implement controlled tests for both string and numeric values * [Input] isControlled should accomodate number | string values as indicated in Props. * extensive testing of isDirty and hasValue to support both strings and numbers This is based on the previous implementation, still not sure that 0 !isDirty * [Input] ensure a value of `0` will mark the field as dirty as it’s a valid value * Refactor Input hasValue/isDirty using logic from master
1 parent 78f14df commit 9a6a3dd

File tree

2 files changed

+104
-42
lines changed

2 files changed

+104
-42
lines changed

src/Input/Input.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,33 @@ import classNames from 'classnames';
66
import withStyles from '../styles/withStyles';
77
import Textarea from './Textarea';
88

9+
/**
10+
* Supports determination of isControlled().
11+
* Controlled input accepts its current value as a prop.
12+
*
13+
* @see https://facebook.github.io/react/docs/forms.html#controlled-components
14+
* @param value
15+
* @returns {boolean} true if string (including '') or number (including zero)
16+
*/
17+
export function hasValue(value: ?(number | string)) {
18+
return value !== undefined && value !== null && !(Array.isArray(value) && value.length === 0);
19+
}
20+
21+
/**
22+
* Determine if field is dirty (a.k.a. filled).
23+
*
24+
* Response determines if label is presented above field or as placeholder.
25+
*
26+
* @param obj
27+
* @param SSR
28+
* @returns {boolean} False when not present or empty string.
29+
* True when any number or string with length.
30+
*/
931
export function isDirty(obj, SSR = false) {
1032
return (
1133
obj &&
12-
((obj.value && obj.value.toString().length) ||
13-
(SSR && obj.defaultValue && obj.defaultValue.toString().length)) > 0
34+
((hasValue(obj.value) && obj.value !== '') ||
35+
(SSR && hasValue(obj.defaultValue) && obj.defaultValue !== ''))
1436
);
1537
}
1638

@@ -369,8 +391,14 @@ class Input extends Component<DefaultProps, AllProps, State> {
369391
}
370392
};
371393

394+
/**
395+
* A controlled input accepts its current value as a prop.
396+
*
397+
* @see https://facebook.github.io/react/docs/forms.html#controlled-components
398+
* @returns {boolean} true if string (including '') or number (including zero)
399+
*/
372400
isControlled() {
373-
return typeof this.props.value === 'string';
401+
return hasValue(this.props.value);
374402
}
375403

376404
checkDirty(obj) {

src/Input/Input.spec.js

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { assert } from 'chai';
55
import { spy } from 'sinon';
66
import { createShallow, createMount, getClasses } from '../test-utils';
77
import Textarea from './Textarea';
8-
import Input, { isDirty } from './Input';
8+
import Input, { hasValue, isDirty } from './Input';
99

1010
describe('<Input />', () => {
1111
let shallow;
@@ -95,39 +95,50 @@ describe('<Input />', () => {
9595
});
9696

9797
describe('controlled', () => {
98-
let wrapper;
99-
let handleDirty;
100-
let handleClean;
101-
102-
before(() => {
103-
handleClean = spy();
104-
handleDirty = spy();
105-
wrapper = shallow(<Input value="" onDirty={handleDirty} onClean={handleClean} />);
106-
});
107-
108-
it('should check that the component is controlled', () => {
109-
const instance = wrapper.instance();
110-
assert.strictEqual(instance.isControlled(), true, 'isControlled() should return true');
111-
});
112-
113-
it('should have called the handleClean callback', () => {
114-
assert.strictEqual(handleClean.callCount, 1, 'should have called the onClean cb');
115-
});
98+
['', 0].forEach(value => {
99+
describe(`${typeof value} value`, () => {
100+
let wrapper;
101+
let handleDirty;
102+
let handleClean;
103+
104+
before(() => {
105+
handleClean = spy();
106+
handleDirty = spy();
107+
wrapper = shallow(<Input value={value} onDirty={handleDirty} onClean={handleClean} />);
108+
});
116109

117-
it('should fire the onDirty callback when dirtied', () => {
118-
assert.strictEqual(handleDirty.callCount, 0, 'should not have called the onDirty cb yet');
119-
wrapper.setProps({ value: 'hello' });
120-
assert.strictEqual(handleDirty.callCount, 1, 'should have called the onDirty cb');
121-
});
110+
it('should check that the component is controlled', () => {
111+
const instance = wrapper.instance();
112+
assert.strictEqual(instance.isControlled(), true, 'isControlled() should return true');
113+
});
122114

123-
it('should fire the onClean callback when dirtied', () => {
124-
assert.strictEqual(
125-
handleClean.callCount,
126-
1,
127-
'should have called the onClean cb once already',
128-
);
129-
wrapper.setProps({ value: '' });
130-
assert.strictEqual(handleClean.callCount, 2, 'should have called the onClean cb again');
115+
// don't test number because zero is a dirty state, whereas '' is not
116+
if (typeof value !== 'number') {
117+
it('should have called the handleClean callback', () => {
118+
assert.strictEqual(handleClean.callCount, 1, 'should have called the onClean cb');
119+
});
120+
121+
it('should fire the onDirty callback when dirtied', () => {
122+
assert.strictEqual(
123+
handleDirty.callCount,
124+
0,
125+
'should not have called the onDirty cb yet',
126+
);
127+
wrapper.setProps({ value: typeof value === 'number' ? 2 : 'hello' });
128+
assert.strictEqual(handleDirty.callCount, 1, 'should have called the onDirty cb');
129+
});
130+
131+
it('should fire the onClean callback when dirtied', () => {
132+
assert.strictEqual(
133+
handleClean.callCount,
134+
1,
135+
'should have called the onClean cb once already',
136+
);
137+
wrapper.setProps({ value });
138+
assert.strictEqual(handleClean.callCount, 2, 'should have called the onClean cb again');
139+
});
140+
}
141+
});
131142
});
132143
});
133144

@@ -345,14 +356,37 @@ describe('<Input />', () => {
345356
});
346357
});
347358

359+
describe('hasValue', () => {
360+
['', 0].forEach(value => {
361+
it(`is true for ${value}`, () => {
362+
assert.strictEqual(hasValue(value), true);
363+
});
364+
});
365+
366+
[null, undefined].forEach(value => {
367+
it(`is false for ${value}`, () => {
368+
assert.strictEqual(hasValue(value), false);
369+
});
370+
});
371+
});
372+
348373
describe('isDirty', () => {
349-
it('should support integer', () => {
350-
assert.strictEqual(
351-
isDirty({
352-
value: 3,
353-
}),
354-
true,
355-
);
374+
[' ', 0].forEach(value => {
375+
it(`is true for value ${value}`, () => {
376+
assert.strictEqual(isDirty({ value }), true);
377+
});
378+
379+
it(`is true for SSR defaultValue ${value}`, () => {
380+
assert.strictEqual(isDirty({ defaultValue: value }, true), true);
381+
});
382+
});
383+
[null, undefined, ''].forEach(value => {
384+
it(`is false for value ${value}`, () => {
385+
assert.strictEqual(isDirty({ value }), false);
386+
});
387+
it(`is false for SSR defaultValue ${value}`, () => {
388+
assert.strictEqual(isDirty({ defaultValue: value }, true), false);
389+
});
356390
});
357391
});
358392
});

0 commit comments

Comments
 (0)