Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 124 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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 <Validates validates={validates} onValidChange={onValidChange} name={name}>
<label>
<input type="text" onChange={onChange} name={name} {...rest} />
{children}
{validates ? null : 'This input is required'}
</label>
return <Validates validates={ validates } onValidChange={ onValidChange } name={ name }>
<input type='text' { ...inputProps } />
</Validates>;
}
}

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
};

```


Expand All @@ -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.
Expand All @@ -124,58 +176,73 @@ 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 <Validate validate={validate} onValidChange={onValidChange} name={name}>
<form onSubmit={onSubmit} name={name} {...rest}>
return <Validate validate={ validate } onValidChange={ onValidChange } name={ name }>
<form { ...formProps }>
{children}
</form>
</Validate>;
}
}

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
};

```


Expand Down
15 changes: 13 additions & 2 deletions demo/.babelrc
Original file line number Diff line number Diff line change
@@ -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"
]
}

22 changes: 1 addition & 21 deletions demo/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"]
}

7 changes: 1 addition & 6 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <script>`.

- `live` - Starts a local development server on port `8080`, and enables hot-reloading.
- `build` - Builds for a development environment.
- `watch` - Like `build`, but also watches for changes.
- `dist` - Builds for a production environment.


[npm-docs-link]: https://docs.npmjs.com/cli/link (npm link documentation)
- `lint` - Runs the linter.

Loading