diff --git a/README.md b/README.md index 6070a2b..929f3a7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ Components for providing validation via React context. + +# Motivation + +There are several scenarios where a parent component's validation depends on whether or not its descendant elements validate. By +using the [React `context` api][react-docs-context], manual crawling of the React render tree can be avoided. Instead, a handler +function in the context can be called to update the parent's state whenever the descendant updates. This simplifies the +implementation of validation in the parent component. + + # Install This package is available on `npm`. Install it using: @@ -29,6 +38,10 @@ change handlers. In general, a validity change handler has the following API: - `@param {Validity} isValid` - The current validity of the component. - `@param {Validity} wasValid` - The previous validity of the component. +The `name` identifier is also key to this library. It allows a collection of descendants to be validated by a parent component. +Essentially, this library provides a way of tracking the validities of these various names. Note that this is **not** the same as +tracking the validities of the components themselves, since a component's name may change as well! + ## `Validates` The `Validates` component is used to wrap a component that can be validated, providing the logic for validation change handlers. @@ -40,62 +53,92 @@ The `Validates` component is used to wrap a component that can be validated, pro - `{Function} onValidChange` - Validity change handler. - `{ReactElement} children` - Children. The component only accepts a single child, and will simply render as that child. +#### When is the `onValidChange` handler called? + +The function passed as the `onValidChange` prop will be called when: +- The component mounts and the `validates` prop is not `undefined` +- The component unmounts and the `validates` prop is not `undefined` +- The component's `validates` prop changes + +During these cases, the `onValidChange` handler is called with: +- The component's `name` prop +- The component's validity +- The component's previous validity + +However, if the component's `name` changes and the `validates` prop is not `undefined`, then the `onValidChange` handler will +first be called with: +- The previous `name` prop +- `undefined`, to indicate that the previous `name` no longer has validation defined +- The component's previous validity, if applicable + +Then, if the `validates` prop has changed, the `onValidChange` handler is called **a second time**. + ### Context -If `onValidChange` is present in `Validate`'s context, it will call that context handler appropriately. +If `onValidChange` is present in `Validate`'s context, it will call that context handler whenever the `onValidChange` handler in +the props would be called, as described above. ### Example usage ```jsx import React from 'react'; +import { string, func } from 'prop-types'; import { Validates } from 'react-validation-context'; +/** + * An input that only validates when its value is non-empty and non-whitespace. + */ export default class RequiredInput extends React.Component { - constructor (props) { - super(...arguments); + constructor(props) { + super(); // Set up the initial state based on whether the initial value validates const { value, defaultValue } = props; - this.state = { validates: this.validate(value || defaultValue) }; + const validates = this.validate(value || defaultValue); + this.state = { validates }; + + this.onChange = this.onChange.bind(this); } - validate (val) { - // Check that the value exists and has non-whitespace content - return val && val.trim().length > 0; + // Check that the value exists and is non-whitespace + validate(value) { + return value && value.trim().length > 0; } - render () { - const { onChange: origOnChange, onValidChange, name, children, ...rest } = this.props; - const { validates } = this.state; + // Wrap the onChange handler to update `this.state.validates` + onChange(e) { + const { onChange } = this.props; + if (onChange) { + onChange(e); + } + + const validates = this.validate(e.target.value); + this.setState({ validates }); + } - // Wrap the onChange handler to update `this.state.validates` - const onChange = (e) => { - if (origOnChange) { - origOnChange(e); - } + render() { + const { onChange } = this; + const { onValidChange, name, ...inputProps } = this.props; + const { validates } = this.state; - this.setState({ validates: this.validate(e.target.value) }); - }; + // Set up `input` props + Object.assign(inputProps, { onChange, name }); // Render `input` and validation context-aware `Validates` - return - + return + ; } } RequiredInput.propTypes = { - name: React.PropTypes.string.isRequired, // Input identifier name - value: React.PropTypes.string, // Input value - defaultValue: React.PropTypes.string, // Default input value - onChange: React.PropTypes.func, // onChange handler for input - onValidChange: React.PropTypes.func, // validity change handler - children: React.PropTypes.node // React children + name: string.isRequired, // Input identifier name + value: string, // Input value + defaultValue: string, // Default input value + onChange: func, // onChange handler for input + onValidChange: func // validity change handler }; + ``` @@ -116,6 +159,15 @@ the component itself. This component also inherits all the props of `Validate`. +#### When is the `validate` function called? + +The function passed as the `validate` prop will be called whenever the component must validate its descendants. This can occur +when the component first mounts, or when at least one of its descendants has changed its name **or** validity. + +Whenever the `validate` function is called, it is given a single argument: an `Object` whose keys are the `name`s of the +descendant components, and whose values are their validities. The `validate` function should then return the validity of the +component. + ### Context If `onValidChange` is present in `Validate`'s context, it will call that context handler appropriately. @@ -124,46 +176,60 @@ If `onValidChange` is present in `Validate`'s context, it will call that context ```jsx import React from 'react'; +import { string, func, node } from 'prop-types'; import { Validate } from 'react-validation-context'; +import styles from './form.less'; + export default class Form extends React.Component { - constructor () { - super(...arguments); + constructor() { + super(); // The form is initially valid this.state = { validates: true }; + + this.onValidChange = this.onValidChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); } - render () { - const { onValidChange: origOnValidChange, onSubmit: origOnSubmit, children, name, className, ...rest } = this.props; - const { validates } = this.state; + // Wrap the onValidChange handler to set this.state.validates + onValidChange(name, isValid, wasValid) { + const { onValidChange } = this.props; + if (onValidChange) { + onValidChange(name, isValid, wasValid); + } - // The form is invalid if there are any invalid items in its validation context - const validate = valids => Object.keys(valids).every(k => valids[k] !== false); + this.setState({ validates: isValid }); + } - // Wrap the onValidChange handler to set this.state.validates - const onValidChange = (name, isValid, wasValid) => { - if (origOnValidChange) { - origOnValidChange(name, isValid, wasValid); - } + // Wrap the onSubmit handler to prevent submission if the form is invalid + onSubmit(e) { + const { onSubmit } = this.props; + if (onSubmit) { + onSubmit(e); + } - this.setState({ validates: isValid }); + if (!this.state.validates) { + e.preventDefault(); } + } - // Wrap the onSubmit handler to prevent submission if the form is invalid - const onSubmit = e => { - if (origOnSubmit) { - origOnSubmit(e); - } + // The form is invalid if there are any invalid items in its validation context + validate(valids) { + return Object.keys(valids).every(k => valids[k] !== false); + } - if (!this.state.validates) { - e.preventDefault(); - } - } + render() { + const { onSubmit, onValidChange, validate } = this; + const { children, name, ...formProps } = this.props; + delete formProps.onValidChange; // avoid passing down `onValidChange` from props + + // Set up `form` props + Object.assign(formProps, { onSubmit, name }); // Render `form` and create validation context `Validate` (which is also validation context-aware) - return -
+ return + {children}
; @@ -171,11 +237,12 @@ export default class Form extends React.Component { } Form.propTypes = { - name: React.PropTypes.string.isRequired, // Form identifier name - onSubmit: React.PropTypes.func, // onSubmit handler for form - onValidChange: React.PropTypes.func, // validity change handler - children: React.PropTypes.node // React children + name: string.isRequired, // Form identifier name + onSubmit: func, // onSubmit handler for form + onValidChange: func, // validity change handler + children: node // React children }; + ``` diff --git a/demo/.babelrc b/demo/.babelrc index 0939263..686781c 100644 --- a/demo/.babelrc +++ b/demo/.babelrc @@ -1,5 +1,16 @@ { - "presets": ["react", "es2015"], - "plugins": ["transform-object-rest-spread"] + "presets": [ + ["env", { + "targets": { + "browsers": ["defaults"] + }, + "modules": false + }], + "react" + ], + "plugins": [ + "react-hot-loader/babel", + "transform-object-rest-spread" + ] } diff --git a/demo/.eslintrc.json b/demo/.eslintrc.json index dd690ce..25d2a60 100644 --- a/demo/.eslintrc.json +++ b/demo/.eslintrc.json @@ -1,24 +1,4 @@ { - "extends": ["eslint:recommended", "plugin:react/recommended"], - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true, - "jsx": true - } - }, - "env": { - "browser": true, - "es6": true - }, - "globals": { - "require": true, - "module": true - }, - "plugins": [ - "react" - ] + "extends": ["godaddy"] } diff --git a/demo/README.md b/demo/README.md index db766e6..0769b6c 100644 --- a/demo/README.md +++ b/demo/README.md @@ -8,15 +8,10 @@ Demo for react-validation-context. Install the dependencies: `npm install` -If you want to simultaneously work on `react-validation-context`, [you can use `npm link`][npm-docs-link]. - The following build scripts are available. You can run them using `npm run