From 436c9e4500f5bd08fdad5db2f3a47940a2690d64 Mon Sep 17 00:00:00 2001 From: Vitaly Kondratiev Date: Mon, 26 Mar 2018 22:24:34 +0100 Subject: [PATCH] adds Control HOC and shouldValidate prop --- examples/package-lock.json | 6 +-- examples/package.json | 2 +- examples/src/Form.tsx | 19 ++++---- package.json | 2 +- src/FormContainer.tsx | 49 ++++++++++++++++++-- src/__tests__/validate.test.tsx | 80 +++++++++++++++++++++++---------- src/interfaces.ts | 1 + src/main.ts | 2 +- src/validate.ts | 7 +++ 9 files changed, 126 insertions(+), 42 deletions(-) diff --git a/examples/package-lock.json b/examples/package-lock.json index e48453d..2adfcc6 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -3675,9 +3675,9 @@ } }, "form-container": { - "version": "0.2.4-rc11", - "resolved": "https://registry.npmjs.org/form-container/-/form-container-0.2.4-rc11.tgz", - "integrity": "sha512-N+f57VAOxbU0BqzsT5d3I6nSOrv1Mm/Jc6Ze5DozJ5he1LgXpHM1rh+xjlVEevjdA/nLY5DOumWzKNqWAz0U/g==", + "version": "0.2.5-rc1", + "resolved": "https://registry.npmjs.org/form-container/-/form-container-0.2.5-rc1.tgz", + "integrity": "sha512-EUxtOCxv5+eqaG7L8OND7ka1D0rhufiL6QXagMAcQDLaT7ok6XK1RicwjEG8Gpm/Y+T3zroFLHvFrfHuQL47fA==", "requires": { "hoist-non-react-statics": "2.5.0", "react": "16.2.0" diff --git a/examples/package.json b/examples/package.json index 2904c12..7f914f4 100644 --- a/examples/package.json +++ b/examples/package.json @@ -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", diff --git a/examples/src/Form.tsx b/examples/src/Form.tsx index 2f38bb6..b6d314d 100644 --- a/examples/src/Form.tsx +++ b/examples/src/Form.tsx @@ -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'; @@ -28,14 +28,15 @@ class Form extends React.Component {
- + + + =6.0.0" }, diff --git a/src/FormContainer.tsx b/src/FormContainer.tsx index e373bb4..0ff9111 100644 --- a/src/FormContainer.tsx +++ b/src/FormContainer.tsx @@ -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'); @@ -13,7 +14,8 @@ const makeWrapper = (config: IFormConfig) => (WrappedComponent: this.state = { model: config.initialModel || {}, touched: {}, - inputs: {} + inputs: {}, + shouldValidate: {} }; } @@ -33,6 +35,12 @@ const makeWrapper = (config: IFormConfig) => (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; @@ -97,7 +105,8 @@ const makeWrapper = (config: IFormConfig) => (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, @@ -105,7 +114,8 @@ const makeWrapper = (config: IFormConfig) => (WrappedComponent: bindToChangeEvent: this.bindToChangeEvent, setProperty: this.setProperty, setModel: this.setModel, - setFieldToTouched: this.setFieldToTouched + setFieldToTouched: this.setFieldToTouched, + setShouldValidate: this.setShouldValidate } }); @@ -122,3 +132,34 @@ export const connectForm = ( validators: any[] = [], config: IFormConfig = {} ) => (Component: any) => pipe(validation.validate(validators), makeWrapper(config))(Component); + +export interface IControlProps extends IFormProps { + name: string; + shouldValidate?: boolean; +} + +export class Control extends React.Component { + 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, { ...bindInput(name) }) + ); + } +} diff --git a/src/__tests__/validate.test.tsx b/src/__tests__/validate.test.tsx index 35c2e48..31d99c7 100644 --- a/src/__tests__/validate.test.tsx +++ b/src/__tests__/validate.test.tsx @@ -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', () => { @@ -17,24 +25,17 @@ describe('Validation', () => { ); - 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: {} + } } }); }); @@ -47,6 +48,7 @@ describe('Validation', () => { ); const props = { form: { + ...initialFormProps, model: { foo: '' } @@ -57,14 +59,14 @@ describe('Validation', () => { )(props); expect(result.props).toEqual({ form: { + ...initialFormProps, isValid: false, model: { foo: '' }, validationErrors: { foo: 'Required field' - }, - validationWarnings: {} + } } }); }); @@ -77,24 +79,17 @@ describe('Validation', () => { ); - 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: {} + } } }); }); @@ -107,6 +102,7 @@ describe('Validation', () => { ); const props = { form: { + ...initialFormProps, model: { foo: '' } @@ -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 ( +
+ + + +
+ ); + }; + 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 + } + } + }); + }); }); }); diff --git a/src/interfaces.ts b/src/interfaces.ts index 3ff1bc5..af3ebd5 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -38,6 +38,7 @@ export interface IFormMethods { 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 { diff --git a/src/main.ts b/src/main.ts index 65cc15f..3543def 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,3 @@ -export { connectForm } from './FormContainer'; +export { connectForm, Control } from './FormContainer'; export { IFormProps, IFormConfig, ValidationRule, ValidationType } from './interfaces'; export { ValidationRuleFactory } from './validators'; diff --git a/src/validate.ts b/src/validate.ts index a9be430..fdbc2da 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -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) {