Skip to content

Commit eccb13f

Browse files
committed
adds Control HOC and shouldValidate prop
1 parent cf6c129 commit eccb13f

File tree

9 files changed

+127
-42
lines changed

9 files changed

+127
-42
lines changed

examples/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"form-container": "^0.2.4-rc11",
6+
"form-container": "^0.2.5-rc1",
77
"material-ui-next": "^1.0.0-beta.38",
88
"react": "^16.2.0",
99
"react-dom": "^16.2.0",

examples/src/Form.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { connectForm, IFormProps } from 'form-container';
2+
import { connectForm, IFormProps, Control } from 'form-container';
33
import { email, required, alphaNumeric, strongPassword } from './validators';
44
import { TextField, Button, CardActions, CardHeader, CardContent } from 'material-ui-next';
55

@@ -28,14 +28,15 @@ class Form extends React.Component<IProps, {}> {
2828
<form name="login" onSubmit={this.handleSubmit}>
2929
<CardHeader title="Sign in" subheader="form-container example" />
3030
<CardContent>
31-
<TextField
32-
style={{ marginBottom: '20px' }}
33-
label="Enter your email"
34-
fullWidth={true}
35-
error={!!this.dirtyInputError('email')}
36-
helperText={this.dirtyInputError('email')}
37-
{...bindInput('email')}
38-
/>
31+
<Control name="email" shouldValidate={false} {...this.props}>
32+
<TextField
33+
style={{ marginBottom: '20px' }}
34+
label="Enter your email"
35+
fullWidth={true}
36+
error={!!this.dirtyInputError('email')}
37+
helperText={this.dirtyInputError('email')}
38+
/>
39+
</Control>
3940
<TextField
4041
type="password"
4142
style={{ marginBottom: '20px' }}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "form-container",
3-
"version": "0.2.4",
3+
"version": "0.2.5-rc1",
44
"engines": {
55
"node": ">=6.0.0"
66
},

src/FormContainer.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import * as React from 'react';
22
import { pipe, isNil } from './utils';
33

44
import * as validation from './validate';
5-
import { IFormConfig, IBoundInput } from './interfaces';
5+
import { IFormConfig, IBoundInput, IFormProps } from './interfaces';
6+
import { ReactElement } from 'react';
67

78
const hoistNonReactStatics = require('hoist-non-react-statics');
89

@@ -13,7 +14,8 @@ const makeWrapper = <T extends {}>(config: IFormConfig<T>) => (WrappedComponent:
1314
this.state = {
1415
model: config.initialModel || {},
1516
touched: {},
16-
inputs: {}
17+
inputs: {},
18+
shouldValidate: {}
1719
};
1820
}
1921

@@ -33,6 +35,12 @@ const makeWrapper = <T extends {}>(config: IFormConfig<T>) => (WrappedComponent:
3335
setFieldToTouched = (prop: keyof T) =>
3436
this.setTouched(Object.assign({}, this.state.touched, { [prop]: true }));
3537

38+
setShouldValidate = (prop: keyof T, isSet: boolean = true) => {
39+
const shouldValidate = Object.assign({}, this.state.shouldValidate, { [prop]: isSet });
40+
this.setState({ shouldValidate });
41+
return shouldValidate;
42+
};
43+
3644
getValue = (name: keyof T) => {
3745
const { state: { model: { [name]: modelValue } } } = this;
3846

@@ -97,15 +105,17 @@ const makeWrapper = <T extends {}>(config: IFormConfig<T>) => (WrappedComponent:
97105
form: {
98106
model: this.state.model,
99107
inputs: this.state.inputs,
100-
touched: this.state.touched
108+
touched: this.state.touched,
109+
shouldValidate: this.state.shouldValidate
101110
},
102111
formMethods: {
103112
bindInput: this.bindInput,
104113
bindNativeInput: this.bindNativeInput,
105114
bindToChangeEvent: this.bindToChangeEvent,
106115
setProperty: this.setProperty,
107116
setModel: this.setModel,
108-
setFieldToTouched: this.setFieldToTouched
117+
setFieldToTouched: this.setFieldToTouched,
118+
setShouldValidate: this.setShouldValidate
109119
}
110120
});
111121

@@ -122,3 +132,35 @@ export const connectForm = <T extends {} = any>(
122132
validators: any[] = [],
123133
config: IFormConfig<T> = {}
124134
) => (Component: any) => pipe(validation.validate(validators), makeWrapper<T>(config))(Component);
135+
136+
export interface IControlProps extends IFormProps {
137+
name: string;
138+
render?: (...args: any[]) => any;
139+
shouldValidate?: boolean;
140+
}
141+
142+
export class Control extends React.Component<IControlProps, {}> {
143+
componentDidMount() {
144+
this.setShouldValidate();
145+
}
146+
147+
componentWillReceiveProps(nextProps: IControlProps) {
148+
if (nextProps.shouldValidate !== this.props.shouldValidate) {
149+
this.setShouldValidate();
150+
}
151+
}
152+
153+
private setShouldValidate() {
154+
const { shouldValidate, formMethods } = this.props;
155+
if (shouldValidate !== undefined) {
156+
formMethods.setShouldValidate(this.props.name, shouldValidate);
157+
}
158+
}
159+
160+
render() {
161+
const { name, formMethods: { bindInput } } = this.props;
162+
return React.Children.map(this.props.children, child =>
163+
React.cloneElement(child as ReactElement<any>, { ...bindInput(name) })
164+
);
165+
}
166+
}

src/__tests__/validate.test.tsx

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import { mount } from 'enzyme';
44
import * as validation from '../validate';
55
import { ValidationType, Condition } from '../interfaces';
66
import { ValidationRuleFactory } from '../validators';
7+
import { Control } from '../FormContainer';
78
const hoistNonReactStatics = require('hoist-non-react-statics');
89

910
const isRequired: Condition = value => !!value;
1011
const required = ValidationRuleFactory(isRequired, 'This field is required');
1112

13+
const initialFormProps = {
14+
model: {},
15+
shouldValidate: {},
16+
validationErrors: {},
17+
validationWarnings: {}
18+
};
19+
1220
describe('Validation', () => {
1321
describe('validate error validator', () => {
1422
it('should return a valid result of validationFn execution', () => {
@@ -17,24 +25,17 @@ describe('Validation', () => {
1725
<input {...bindInput('foo')} />
1826
</form>
1927
);
20-
const props = {
21-
form: {
22-
model: {
23-
foo: 'test'
24-
}
25-
}
26-
};
28+
const props = { form: { ...initialFormProps, model: { foo: 'test' } } };
2729
const result = validation.validate([required('foo', 'Required field')])(
2830
MockComponent as any
2931
)(props);
3032
expect(result.props).toEqual({
3133
form: {
34+
...initialFormProps,
3235
isValid: true,
3336
model: {
3437
foo: 'test'
35-
},
36-
validationErrors: {},
37-
validationWarnings: {}
38+
}
3839
}
3940
});
4041
});
@@ -47,6 +48,7 @@ describe('Validation', () => {
4748
);
4849
const props = {
4950
form: {
51+
...initialFormProps,
5052
model: {
5153
foo: ''
5254
}
@@ -57,14 +59,14 @@ describe('Validation', () => {
5759
)(props);
5860
expect(result.props).toEqual({
5961
form: {
62+
...initialFormProps,
6063
isValid: false,
6164
model: {
6265
foo: ''
6366
},
6467
validationErrors: {
6568
foo: 'Required field'
66-
},
67-
validationWarnings: {}
69+
}
6870
}
6971
});
7072
});
@@ -77,24 +79,17 @@ describe('Validation', () => {
7779
<input {...bindInput('foo')} />
7880
</form>
7981
);
80-
const props = {
81-
form: {
82-
model: {
83-
foo: 'test'
84-
}
85-
}
86-
};
82+
const props = { form: { ...initialFormProps, model: { foo: 'test' } } };
8783
const result = validation.validate([
8884
required('foo', 'Required field', ValidationType.Warning)
8985
])(MockComponent as any)(props);
9086
expect(result.props).toEqual({
9187
form: {
88+
...initialFormProps,
9289
isValid: true,
9390
model: {
9491
foo: 'test'
95-
},
96-
validationErrors: {},
97-
validationWarnings: {}
92+
}
9893
}
9994
});
10095
});
@@ -107,6 +102,7 @@ describe('Validation', () => {
107102
);
108103
const props = {
109104
form: {
105+
...initialFormProps,
110106
model: {
111107
foo: ''
112108
}
@@ -117,16 +113,54 @@ describe('Validation', () => {
117113
])(MockComponent as any)(props);
118114
expect(result.props).toEqual({
119115
form: {
116+
...initialFormProps,
120117
isValid: true,
121118
model: {
122119
foo: ''
123120
},
124-
validationErrors: {},
125121
validationWarnings: {
126122
foo: 'Required field'
127123
}
128124
}
129125
});
130126
});
127+
128+
it('should conditionally validate', () => {
129+
const MockComponent = (props: any) => {
130+
const { formMethods: { bindInput }, form } = props;
131+
return (
132+
<form>
133+
<Control name="foo" shouldValidate={false} {...props}>
134+
<input />
135+
</Control>
136+
</form>
137+
);
138+
};
139+
const props = {
140+
form: {
141+
...initialFormProps,
142+
model: {
143+
foo: ''
144+
},
145+
shouldValidate: {
146+
foo: false
147+
}
148+
}
149+
};
150+
151+
const result = validation.validate([required('foo')])(MockComponent as any)(props);
152+
expect(result.props).toEqual({
153+
form: {
154+
...initialFormProps,
155+
isValid: true,
156+
model: {
157+
foo: ''
158+
},
159+
shouldValidate: {
160+
foo: false
161+
}
162+
}
163+
});
164+
});
131165
});
132166
});

src/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface IFormMethods<T = any> {
3838
setProperty: (prop: keyof T, value: T[keyof T]) => any;
3939
setModel: (model: { [name in keyof T]?: any }) => any;
4040
setFieldToTouched: (prop: keyof T) => any;
41+
setShouldValidate: (prop: keyof T, isSet: boolean) => any;
4142
}
4243

4344
export interface IFormProps<T = any> {

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { connectForm } from './FormContainer';
1+
export { connectForm, Control } from './FormContainer';
22
export { IFormProps, IFormConfig, ValidationRule, ValidationType } from './interfaces';
33
export { ValidationRuleFactory } from './validators';

src/validate.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ const getValidationResult = ({
2020
rules
2121
.filter(([rule, field, type = ValidationType.Error]) => type === validationType)
2222
.reduce((errors, [rule, field, type]) => {
23+
const fieldKey = Object.keys(field)[0];
24+
const shouldValidate = allProps.form.shouldValidate[fieldKey];
25+
26+
if (shouldValidate === false) {
27+
return errors; // skip further validation
28+
}
29+
2330
const isValid = rule(model, allProps);
2431

2532
if (isValid) {

0 commit comments

Comments
 (0)