Skip to content

Commit

Permalink
adds Control HOC and shouldValidate prop
Browse files Browse the repository at this point in the history
  • Loading branch information
vitkon committed Mar 26, 2018
1 parent cf6c129 commit 436c9e4
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 42 deletions.
6 changes: 3 additions & 3 deletions examples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"form-container": "^0.2.4-rc11",
"form-container": "^0.2.5-rc1",
"material-ui-next": "^1.0.0-beta.38",
"react": "^16.2.0",
"react-dom": "^16.2.0",
Expand Down
19 changes: 10 additions & 9 deletions examples/src/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { connectForm, IFormProps } from 'form-container';
import { connectForm, IFormProps, Control } from 'form-container';
import { email, required, alphaNumeric, strongPassword } from './validators';
import { TextField, Button, CardActions, CardHeader, CardContent } from 'material-ui-next';

Expand Down Expand Up @@ -28,14 +28,15 @@ class Form extends React.Component<IProps, {}> {
<form name="login" onSubmit={this.handleSubmit}>
<CardHeader title="Sign in" subheader="form-container example" />
<CardContent>
<TextField
style={{ marginBottom: '20px' }}
label="Enter your email"
fullWidth={true}
error={!!this.dirtyInputError('email')}
helperText={this.dirtyInputError('email')}
{...bindInput('email')}
/>
<Control name="email" shouldValidate={false} {...this.props}>
<TextField
style={{ marginBottom: '20px' }}
label="Enter your email"
fullWidth={true}
error={!!this.dirtyInputError('email')}
helperText={this.dirtyInputError('email')}
/>
</Control>
<TextField
type="password"
style={{ marginBottom: '20px' }}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "form-container",
"version": "0.2.4",
"version": "0.2.5-rc1",
"engines": {
"node": ">=6.0.0"
},
Expand Down
49 changes: 45 additions & 4 deletions src/FormContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as React from 'react';
import { pipe, isNil } from './utils';

import * as validation from './validate';
import { IFormConfig, IBoundInput } from './interfaces';
import { IFormConfig, IBoundInput, IFormProps } from './interfaces';
import { ReactElement } from 'react';

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

Expand All @@ -13,7 +14,8 @@ const makeWrapper = <T extends {}>(config: IFormConfig<T>) => (WrappedComponent:
this.state = {
model: config.initialModel || {},
touched: {},
inputs: {}
inputs: {},
shouldValidate: {}
};
}

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

setShouldValidate = (prop: keyof T, isSet: boolean = true) => {
const shouldValidate = Object.assign({}, this.state.shouldValidate, { [prop]: isSet });
this.setState({ shouldValidate });
return shouldValidate;
};

getValue = (name: keyof T) => {
const { state: { model: { [name]: modelValue } } } = this;

Expand Down Expand Up @@ -97,15 +105,17 @@ const makeWrapper = <T extends {}>(config: IFormConfig<T>) => (WrappedComponent:
form: {
model: this.state.model,
inputs: this.state.inputs,
touched: this.state.touched
touched: this.state.touched,
shouldValidate: this.state.shouldValidate
},
formMethods: {
bindInput: this.bindInput,
bindNativeInput: this.bindNativeInput,
bindToChangeEvent: this.bindToChangeEvent,
setProperty: this.setProperty,
setModel: this.setModel,
setFieldToTouched: this.setFieldToTouched
setFieldToTouched: this.setFieldToTouched,
setShouldValidate: this.setShouldValidate
}
});

Expand All @@ -122,3 +132,34 @@ export const connectForm = <T extends {} = any>(
validators: any[] = [],
config: IFormConfig<T> = {}
) => (Component: any) => pipe(validation.validate(validators), makeWrapper<T>(config))(Component);

export interface IControlProps extends IFormProps {
name: string;
shouldValidate?: boolean;
}

export class Control extends React.Component<IControlProps, {}> {
componentDidMount() {
this.setShouldValidate();
}

componentWillReceiveProps(nextProps: IControlProps) {
if (nextProps.shouldValidate !== this.props.shouldValidate) {
this.setShouldValidate();
}
}

private setShouldValidate() {
const { shouldValidate, formMethods } = this.props;
if (shouldValidate !== undefined) {
formMethods.setShouldValidate(this.props.name, shouldValidate);
}
}

render() {
const { name, formMethods: { bindInput } } = this.props;
return React.Children.map(this.props.children, child =>
React.cloneElement(child as ReactElement<any>, { ...bindInput(name) })
);
}
}
80 changes: 57 additions & 23 deletions src/__tests__/validate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import { mount } from 'enzyme';
import * as validation from '../validate';
import { ValidationType, Condition } from '../interfaces';
import { ValidationRuleFactory } from '../validators';
import { Control } from '../FormContainer';
const hoistNonReactStatics = require('hoist-non-react-statics');

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

const initialFormProps = {
model: {},
shouldValidate: {},
validationErrors: {},
validationWarnings: {}
};

describe('Validation', () => {
describe('validate error validator', () => {
it('should return a valid result of validationFn execution', () => {
Expand All @@ -17,24 +25,17 @@ describe('Validation', () => {
<input {...bindInput('foo')} />
</form>
);
const props = {
form: {
model: {
foo: 'test'
}
}
};
const props = { form: { ...initialFormProps, model: { foo: 'test' } } };
const result = validation.validate([required('foo', 'Required field')])(
MockComponent as any
)(props);
expect(result.props).toEqual({
form: {
...initialFormProps,
isValid: true,
model: {
foo: 'test'
},
validationErrors: {},
validationWarnings: {}
}
}
});
});
Expand All @@ -47,6 +48,7 @@ describe('Validation', () => {
);
const props = {
form: {
...initialFormProps,
model: {
foo: ''
}
Expand All @@ -57,14 +59,14 @@ describe('Validation', () => {
)(props);
expect(result.props).toEqual({
form: {
...initialFormProps,
isValid: false,
model: {
foo: ''
},
validationErrors: {
foo: 'Required field'
},
validationWarnings: {}
}
}
});
});
Expand All @@ -77,24 +79,17 @@ describe('Validation', () => {
<input {...bindInput('foo')} />
</form>
);
const props = {
form: {
model: {
foo: 'test'
}
}
};
const props = { form: { ...initialFormProps, model: { foo: 'test' } } };
const result = validation.validate([
required('foo', 'Required field', ValidationType.Warning)
])(MockComponent as any)(props);
expect(result.props).toEqual({
form: {
...initialFormProps,
isValid: true,
model: {
foo: 'test'
},
validationErrors: {},
validationWarnings: {}
}
}
});
});
Expand All @@ -107,6 +102,7 @@ describe('Validation', () => {
);
const props = {
form: {
...initialFormProps,
model: {
foo: ''
}
Expand All @@ -117,16 +113,54 @@ describe('Validation', () => {
])(MockComponent as any)(props);
expect(result.props).toEqual({
form: {
...initialFormProps,
isValid: true,
model: {
foo: ''
},
validationErrors: {},
validationWarnings: {
foo: 'Required field'
}
}
});
});

it('should conditionally validate', () => {
const MockComponent = (props: any) => {
const { formMethods: { bindInput }, form } = props;
return (
<form>
<Control name="foo" shouldValidate={false} {...props}>
<input />
</Control>
</form>
);
};
const props = {
form: {
...initialFormProps,
model: {
foo: ''
},
shouldValidate: {
foo: false
}
}
};

const result = validation.validate([required('foo')])(MockComponent as any)(props);
expect(result.props).toEqual({
form: {
...initialFormProps,
isValid: true,
model: {
foo: ''
},
shouldValidate: {
foo: false
}
}
});
});
});
});
1 change: 1 addition & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface IFormMethods<T = any> {
setProperty: (prop: keyof T, value: T[keyof T]) => any;
setModel: (model: { [name in keyof T]?: any }) => any;
setFieldToTouched: (prop: keyof T) => any;
setShouldValidate: (prop: keyof T, isSet: boolean) => any;
}

export interface IFormProps<T = any> {
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { connectForm } from './FormContainer';
export { connectForm, Control } from './FormContainer';
export { IFormProps, IFormConfig, ValidationRule, ValidationType } from './interfaces';
export { ValidationRuleFactory } from './validators';
7 changes: 7 additions & 0 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ const getValidationResult = ({
rules
.filter(([rule, field, type = ValidationType.Error]) => type === validationType)
.reduce((errors, [rule, field, type]) => {
const fieldKey = Object.keys(field)[0];
const shouldValidate = allProps.form.shouldValidate[fieldKey];

if (shouldValidate === false) {
return errors; // skip further validation
}

const isValid = rule(model, allProps);

if (isValid) {
Expand Down

0 comments on commit 436c9e4

Please sign in to comment.