Skip to content

Commit

Permalink
fix(text-field): custom validation (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
hvolschenk authored and Matt Goo committed Nov 20, 2018
1 parent 3a908ff commit f5c9c35
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 27 deletions.
23 changes: 22 additions & 1 deletion packages/text-field/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ export default class Input extends React.Component {

componentDidMount() {
const {
id, disabled, value, setInputId, setDisabled, handleValueChange, foundation,
id,
disabled,
value,
setInputId,
setDisabled,
handleValueChange,
foundation,
isValid,
} = this.props;

if (id) {
Expand All @@ -44,6 +51,10 @@ export default class Input extends React.Component {
if (value) {
handleValueChange(value, () => foundation.setValue(value));
}
if (isValid !== undefined) {
foundation.setUseNativeValidation(false);
foundation.setValid(isValid);
}
}

componentDidUpdate(prevProps) {
Expand All @@ -55,6 +66,7 @@ export default class Input extends React.Component {
foundation,
value,
disabled,
isValid,
} = this.props;

this.handleValidationAttributeUpdate(prevProps);
Expand All @@ -79,6 +91,15 @@ export default class Input extends React.Component {
}
});
}

if (isValid !== prevProps.isValid) {
if (isValid === undefined) {
foundation.setUseNativeValidation(true);
} else {
foundation.setUseNativeValidation(false);
foundation.setValid(isValid);
}
}
}

get classes() {
Expand Down
72 changes: 69 additions & 3 deletions packages/text-field/helper-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,22 @@ validation | Boolean | If true, alters the helper text to an error message.

## Input Validation

HelperText provides validity styling by setting the `validation` prop in HelperText. Validation can be checked through the appropriate `Input` validation properties (pattern, min, max, required, step, minLength, maxLength).
HelperText provides validity styling by setting the `validation` prop in HelperText.

### Native Validation

Validation can be checked through the appropriate `Input` validation properties (pattern, min, max, required, step, minLength, maxLength).

The following snippet is an example of how to use pattern regex and check for minimum length with HelperText:
``` js
import React from 'react';
import TextField, {HelperText, Input} from '@material/react-text-field';
import React from 'react';

class MyApp extends React.Component {
state = {username: ''};
constructor(props) {
super(props);
this.state = {username: ''};
}
render() {
return (
<div>
Expand All @@ -53,6 +61,7 @@ class MyApp extends React.Component {
<Input
pattern='^([a-zA-Z_]+[0-9]*)$'
minLength={8}
name='username'
value={this.state.username}
onChange={(e)=>this.setState({username:e.target.value})}
/>
Expand All @@ -63,6 +72,63 @@ class MyApp extends React.Component {
}
```

### Custom Validation

Validation could also be done manually by using the `isValid`, `isValidationMessage` and
`validation` props, alongside a custom validation method:

```js
import TextField, {HelperText, Input} from '@material/react-text-field';
import React from 'react';

class MyApp extends React.Component {
constructor(props) {
super(props);
this.state = {isValid: true, username: ''};
this.onChange = this.onChange.bind(this);
this.renderHelperText = this.renderHelperText.bind(this);
}
onChange(e) {
const {value} = e.target;
this.setState({isValid: value.includes('@'), username: value});
}
renderHelperText() {
const { isValid } = this.state;
if (isValid) {
return (<HelperText>Please enter your Username</HelperText>);
} else {
return (
<HelperText
isValid={isValid}
isValidationMessage
validation
>
Your Username must contain an @
</HelperText>
);
}
}
render() {
return (
<div>
<TextField
box
label='Username'
helperText={this.renderHelperText()}
>
<Input
isValid={this.state.isValid}
name='username'
onChange={this.onChange}
value={this.state.username}
/>
</TextField>
</div>
);
}
}
```

## Sass Mixins

Sass mixins may be available to customize various aspects of the Components. Please refer to the
Expand Down
81 changes: 58 additions & 23 deletions test/unit/text-field/Input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import {Input} from '../../../packages/text-field/index';

suite('Text Field Input');

const buildFoundation = (overrides = {}) => ({
activateFocus: () => {},
deactivateFocus: () => {},
handleValidationAttributeChange: () => {},
handleValidationAttributeMutation_: () => {},
setDisabled: () => {},
setUseNativeValidation: () => {},
setValid: () => {},
...overrides,
});

test('classNames adds classes', () => {
const wrapper = shallow(<Input className='test-class-name' />);
assert.isTrue(wrapper.hasClass('test-class-name'));
Expand Down Expand Up @@ -41,25 +52,25 @@ test('#isValid returns false if input is invalid', () => {
});

test('#isValid returns true if prop.isValid is set to true', () => {
const wrapper = mount(<Input value='m' pattern='[a-z]' isValid />);
const wrapper = mount(<Input foundation={buildFoundation()} value='m' pattern='[a-z]' isValid />);
const isValidInput = wrapper.instance().isValid();
assert.isTrue(isValidInput);
});

test('#isValid returns false if prop.isValid is set to false', () => {
const wrapper = mount(<Input value='m' pattern='[a-z]' isValid={false} />);
const wrapper = mount(<Input foundation={buildFoundation()} value='m' pattern='[a-z]' isValid={false} />);
const isValidInput = wrapper.instance().isValid();
assert.isFalse(isValidInput);
});

test('#isValid returns false if prop.isValid is set to false and input is invalid', () => {
const wrapper = mount(<Input value='meow' pattern='[a-z]' isValid={false}/>);
const wrapper = mount(<Input foundation={buildFoundation()} value='meow' pattern='[a-z]' isValid={false}/>);
const isValidInput = wrapper.instance().isValid();
assert.isFalse(isValidInput);
});

test('#isValid returns true if prop.isValid is set to true and input is invalid', () => {
const wrapper = mount(<Input value='meow' pattern='[a-z]' isValid/>);
const wrapper = mount(<Input foundation={buildFoundation()} value='meow' pattern='[a-z]' isValid/>);
const isValidInput = wrapper.instance().isValid();
assert.isTrue(isValidInput);
});
Expand Down Expand Up @@ -115,23 +126,23 @@ test('#componentDidMount does not call props.handleValueChange when there is no
});

test('change to minLength calls handleValidationAttributeChange', () => {
const handleValidationAttributeChange = td.func();
const wrapper = shallow(<Input foundation={{handleValidationAttributeChange}} />);
const foundation = buildFoundation({handleValidationAttributeChange: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
wrapper.setProps({minLength: 20});
td.verify(handleValidationAttributeChange(['minlength']), {times: 1});
td.verify(foundation.handleValidationAttributeChange(['minlength']), {times: 1});
});

test('#componentDidUpdate calls handleValidationAttributeChange when ' +
'a whitelisted attr updates', () => {
const handleValidationAttributeChange = td.func();
const wrapper = shallow(<Input foundation={{handleValidationAttributeChange}} />);
const foundation = buildFoundation({handleValidationAttributeChange: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
wrapper.setProps({required: true});
td.verify(handleValidationAttributeChange(['required']), {times: 1});
td.verify(foundation.handleValidationAttributeChange(['required']), {times: 1});
});

test('#componentDidUpdate calls setDisabled and foundation.setDisabled when ' +
'disabled changes to true', () => {
const foundation = {setDisabled: td.func()};
const foundation = buildFoundation({setDisabled: td.func()});
const setDisabled = td.func();
const wrapper = shallow(<Input foundation={foundation} setDisabled={setDisabled} />);
wrapper.setProps({disabled: true});
Expand All @@ -141,7 +152,7 @@ test('#componentDidUpdate calls setDisabled and foundation.setDisabled when ' +

test('#componentDidUpdate calls setDisabled and foundation.setDisabled when ' +
'disabled changes to false', () => {
const foundation = {setDisabled: td.func()};
const foundation = buildFoundation({setDisabled: td.func()});
const setDisabled = td.func();
const wrapper = shallow(<Input disabled foundation={foundation} setDisabled={setDisabled} />);
wrapper.setProps({disabled: false});
Expand All @@ -159,10 +170,10 @@ test('#componentDidUpdate calls setInputId if id updates', () => {
test('#componentDidUpdate does nothing if an unrelated property is ' +
'updated', () => {
const setDisabled = td.func();
const foundation = {
const foundation = buildFoundation({
handleValidationAttributeMutation_: td.func(),
setDisabled: td.func(),
};
});
const setInputId = td.func();
const wrapper = shallow(<Input
setDisabled={setDisabled}
Expand All @@ -182,10 +193,35 @@ test('#componentDidUpdate calls handleValueChange when the foundation initialize
const handleValueChange = td.func();
const wrapper = shallow(<Input value='test value' handleValueChange={handleValueChange} />);

wrapper.setProps({foundation: {setValue}});
wrapper.setProps({foundation: buildFoundation({setValue})});
td.verify(handleValueChange('test value', td.matchers.isA(Function)), {times: 1});
});

test('#componentDidUpdate calls setUseNativeValidation when isValid changes to undefined', () => {
const foundation = buildFoundation({setUseNativeValidation: td.func()});
const wrapper = shallow(<Input isValid={false} value='test value' foundation={foundation} />);

wrapper.setProps({isValid: undefined});
td.verify(foundation.setUseNativeValidation(false), {times: 1});
});

test('#componentDidUpdate calls setUseNativeValidation when isValid changes', () => {
const foundation = buildFoundation({setUseNativeValidation: td.func()});
const wrapper = shallow(<Input value='test value' foundation={foundation} />);

wrapper.setProps({isValid: true});
td.verify(foundation.setUseNativeValidation(false), {times: 1});
});

test('#componentDidUpdate calls setValid when isValid changes', () => {
const foundation = buildFoundation({setValid: td.func()});
const wrapper = shallow(<Input isValid={false} value='test value' foundation={foundation} />);

wrapper.setProps({isValid: true});
td.verify(foundation.setValid(false), {times: 1});
td.verify(foundation.setValid(true), {times: 1});
});

test('props.handleValueChange() is called if this.props.value updates', () => {
const handleValueChange = td.func();
const wrapper = shallow(<Input handleValueChange={handleValueChange} />);
Expand All @@ -194,8 +230,7 @@ test('props.handleValueChange() is called if this.props.value updates', () => {
});

test('foundation.setValue() is called if this.props.value updates', () => {
const setValue = td.func();
const foundation = {setValue};
const foundation = buildFoundation({setValue: td.func()});
const handleValueChange = (value, cb) => {
cb(value);
};
Expand All @@ -204,7 +239,7 @@ test('foundation.setValue() is called if this.props.value updates', () => {
foundation={foundation}
handleValueChange={handleValueChange} />);
wrapper.setProps({value: 'meow'});
td.verify(setValue('meow'), {times: 1});
td.verify(foundation.setValue('meow'), {times: 1});
});

test('#event.onFocus calls props.handleFocusChange(true)', () => {
Expand All @@ -215,7 +250,7 @@ test('#event.onFocus calls props.handleFocusChange(true)', () => {
});

test('#event.onFocus calls foundation.activateFocus()', () => {
const foundation = {activateFocus: td.func()};
const foundation = buildFoundation({activateFocus: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
wrapper.simulate('focus');
td.verify(foundation.activateFocus(), {times: 1});
Expand All @@ -237,7 +272,7 @@ test('#event.onBlur calls props.handleFocusChange(false)', () => {
});

test('#event.onBlur calls foundation.deactivateFocus()', () => {
const foundation = {deactivateFocus: td.func()};
const foundation = buildFoundation({deactivateFocus: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
wrapper.simulate('blur');
td.verify(foundation.deactivateFocus(), {times: 1});
Expand All @@ -252,7 +287,7 @@ test('#event.onBlur calls props.onBlur()', () => {
});

test('#event.onMouseDown calls foundation.setTransformOrigin()', () => {
const foundation = {setTransformOrigin: td.func()};
const foundation = buildFoundation({setTransformOrigin: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
const event = {preventDefault: () => {}};
wrapper.simulate('mouseDown', event);
Expand All @@ -268,7 +303,7 @@ test('#event.onMouseDown calls props.onMouseDown()', () => {
});

test('#event.onTouchStart calls foundation.setTransformOrigin()', () => {
const foundation = {setTransformOrigin: td.func()};
const foundation = buildFoundation({setTransformOrigin: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
const event = {preventDefault: () => {}};
wrapper.simulate('touchStart', event);
Expand All @@ -284,7 +319,7 @@ test('#event.onTouchStart calls props.onTouchStart()', () => {
});

test('#event.onChange calls foundation.autoCompleteFocus()', () => {
const foundation = {autoCompleteFocus: td.func()};
const foundation = buildFoundation({autoCompleteFocus: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
const event = {target: {value: 'apple'}};
wrapper.simulate('change', event);
Expand Down

0 comments on commit f5c9c35

Please sign in to comment.