Skip to content

Commit

Permalink
Full rewrite, allow for nested FieldFeedbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
tkrotoff committed Apr 23, 2018
1 parent 3eb69b2 commit fa0de7f
Show file tree
Hide file tree
Showing 103 changed files with 7,529 additions and 4,427 deletions.
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
## 0.8.0 (2018/01/10)

### Features

- Async support
- Rewrite to allow nested `FieldFeedbacks`

### Breaking Changes

- `show` attribute replaced by `stop`
- `FieldFeedbacks` `show` attribute replaced by `stop`
- `validateFields()` now returns a list of promises
- Add `validateForm()`: does not re-validate fields already validated contrary to `validateFields()`
- Improve typings, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16318#issuecomment-362060939

### Fixes

- Fix `computeFieldFeedbackKey()` implementation, see 2291b3b
- Fix `computeFieldFeedbackKey()` implementation
- Fix possible crash with React Native, see 03d72e1
- Fix form reset #22 by introducing `reset()`

### Features

- Async support

## 0.7.1 (2017/11/27)

### Fixes
Expand Down
115 changes: 87 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![codecov](https://codecov.io/gh/tkrotoff/react-form-with-constraints/branch/master/graph/badge.svg)](https://codecov.io/gh/tkrotoff/react-form-with-constraints)
[![gzip size](http://img.badgesize.io/https://unpkg.com/react-form-with-constraints@latest/dist/react-form-with-constraints.production.min.js.gz?compression=gzip)](https://unpkg.com/react-form-with-constraints/dist/react-form-with-constraints.production.min.js.gz)

Simple form validation for React in [~500 lines of code](packages/react-form-with-constraints/src)
Simple form validation for React

- Installation: `npm install react-form-with-constraints`
- CDN: https://unpkg.com/react-form-with-constraints/dist/
Expand All @@ -14,6 +14,8 @@ Check the [changelog](CHANGELOG.md) for breaking changes and fixes between relea

## Introduction: what is HTML5 form validation?

⚠️ [Client side validation is cosmetic, you should not rely on it to enforce security](https://stackoverflow.com/q/162159)

```HTML
<form>
<label for="email">Email:</label>
Expand Down Expand Up @@ -103,8 +105,8 @@ The API works the same way as [React Router v4](https://reacttraining.com/react-
It is also inspired by [AngularJS ngMessages](https://docs.angularjs.org/api/ngMessages#usage).

If you had to implement validation yourself, you would end up with [a global object that tracks errors for each field](examples/NoFramework/App.tsx).
react-form-with-constraints [works similarly](packages/react-form-with-constraints/src/Fields.ts).
It uses [React context](https://facebook.github.io/react/docs/context.html#parent-child-coupling) to share the [`FieldsStore`](packages/react-form-with-constraints/src/FieldsStore.ts) object across [`FieldFeedbacks`](packages/react-form-with-constraints/src/FieldFeedbacks.tsx) and [`FieldFeedback`](packages/react-form-with-constraints/src/FieldFeedback.tsx).
react-form-with-constraints [works similarly](packages/react-form-with-constraints/src/FieldsStore.ts).
It uses [React context](https://github.com/reactjs/reactjs.org/blob/d59c4f9116138e419812e44b0fdb56644c498d3e/content/docs/context.md) to share the [`FieldsStore`](packages/react-form-with-constraints/src/FieldsStore.ts) object across [`FieldFeedbacks`](packages/react-form-with-constraints/src/FieldFeedbacks.tsx) and [`FieldFeedback`](packages/react-form-with-constraints/src/FieldFeedback.tsx).

## API

Expand Down Expand Up @@ -142,10 +144,10 @@ class MyForm extends React.Component {
async handleChange(e) {
const target = e.currentTarget;

// Validates only the given field and returns the related FieldFeedbacksValidation structures
const fieldFeedbacksValidations = await this.form.validateFields(target);
// Validates only the given fields and returns Promise<Field[]>
const fields = await this.form.validateFields(target);

const fieldIsValid = fieldFeedbacksValidations.every(fieldFeedbacksValidation => fieldFeedbacksValidation.isValid());
const fieldIsValid = fields.every(field => field.isValid());
if (fieldIsValid) console.log(`Field '${target.name}' is valid`);
else console.log(`Field '${target.name}' is invalid`);

Expand All @@ -156,11 +158,11 @@ class MyForm extends React.Component {
async handleSubmit(e) {
e.preventDefault();

// Validates the non-dirty fields and returns the related FieldFeedbacksValidation structures
const fieldFeedbacksValidations = await this.form.validateForm();
// Validates the non-dirty fields and returns Promise<Field[]>
const fields = await this.form.validateForm();

// or simply this.form.isValid();
const formIsValid = fieldFeedbacksValidations.every(fieldFeedbacksValidation => fieldFeedbacksValidation.isValid());
const formIsValid = fields.every(field => field.isValid());

if (formIsValid) console.log('The form is valid');
else console.log('The form is invalid');
Expand All @@ -169,7 +171,7 @@ class MyForm extends React.Component {
render() {
return (
<FormWithConstraints
ref={form => this.form = form}
ref={formWithConstraints => this.form = formWithConstraints}
onSubmit={this.handleSubmit} noValidate
>
<input
Expand All @@ -194,47 +196,104 @@ class MyForm extends React.Component {
}
```

- `FieldFeedbacks`
- [`FieldFeedbacks`](packages/react-form-with-constraints/src/FieldFeedbacks.tsx)
- `for: string` => reference to a `name` attribute (e.g `<input name="username">`), should be unique to the current form
- `stop?: 'first-error' | 'no'` => when to stop rendering `FieldFeedback`s, by default stops at the first error encountered (`FieldFeedback`s order matters)

Note: you can place `FieldFeedbacks` anywhere and have as many as you want for the same `field`
- `stop?: 'first' | 'first-error' | 'first-warning' | 'first-info' | 'no'` =>
when to stop rendering `FieldFeedback`s, by default stops at the first error encountered (`FieldFeedback`s order matters)

Note: you can place `FieldFeedbacks` anywhere, have as many as you want for the same `field`, nest them, mix them with `FieldFeedback`... Dirty example:

```JSX
<div>
<input name="username" ... />
</div>

<FieldFeedbacks for="username" stop="first-warning">

<FieldFeedbacks stop="no">
<div>
<FieldFeedbacks stop="first-error">
<FieldFeedbacks>
<div><FieldFeedback ... /></div>
<div><Async ... /></div>
<div><FieldFeedback ... /></div>
</FieldFeedbacks>
</FieldFeedbacks>
</div>
<FieldFeedbacks>
<FieldFeedback ... />
<Async ... />
<FieldFeedback ... />

<FieldFeedbacks stop="first-info">
<FieldFeedback ... />
<Async ... />
<FieldFeedback ... />
</FieldFeedbacks>

- `FieldFeedback`
- `when?: `[`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState)` string | '*' | function` => HTML5 constraint violation name or a callback
<FieldFeedback ... />
<Async ... />
<FieldFeedback ... />
</FieldFeedbacks>
</FieldFeedbacks>

<FieldFeedbacks stop="first-info">
<FieldFeedbacks>
<FieldFeedback ... />
<Async ... />
<FieldFeedback ... />
</FieldFeedbacks>
</FieldFeedbacks>

</FieldFeedbacks>

<div>
<FieldFeedbacks for="username" stop="no">
...
</FieldFeedbacks>
</div>
```

- [`FieldFeedback`](packages/react-form-with-constraints/src/FieldFeedback.tsx)
- `when?`:
- [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) as a string => HTML5 constraint violation name
- `'*'` => matches any HTML5 constraint violation
- `'valid'` => displays the feedback only if the field is valid
- `(value: string) => boolean` => custom constraint
- `error?: boolean` => treats the feedback as an error (default)
- `warning?: boolean` => treats the feedback as a warning
- `info?: boolean` => treats the feedback as an info

- `Async<T>` => Async version of FieldFeedback, similar API as [react-promise](https://github.com/capaj/react-promise)
- [`Async<T>`](packages/react-form-with-constraints/src/Async.tsx) => Async version of `FieldFeedback`, similar API as [react-promise](https://github.com/capaj/react-promise)
- `promise: (value: string) => Promise<T>` => a promise you want to wait for
- `pending?: React.ReactNode` => runs when promise is pending
- `then?: (value: T) => React.ReactNode` => runs when promise is resolved
- `catch?: (reason: any) => React.ReactNode` => runs when promise is rejected

- `FormWithConstraints`
- [`FormWithConstraints`](packages/react-form-with-constraints/src/FormWithConstraints.tsx)

- `validateFields(...inputsOrNames: Array<Input | string>): Promise<FieldFeedbacksValidation[]>` =>
- `validateFields(...inputsOrNames: Array<Input | string>): Promise<Field[]>` =>
Should be called when a `field` changes, will re-render the proper `FieldFeedback`s (and update the internal `FieldsStore`).
Without arguments, all fields (`$('[name]')`) are validated.

- `validateForm(): Promise<FieldFeedbacksValidation[]>` =>
- `validateForm(): Promise<Field[]>` =>
Should be called before to submit the `form`. Validates only all non-dirty fields (won't re-validate fields that have been already validated with `validateFields()`),
If you want to force re-validate all fields, use `validateFields()` without arguments.
- `isValid(): boolean` => should be called after `validateForm()` or `validateFields()`, tells if the known fields are valid (thanks to internal `FieldsStore`)
- `reset(): void` => resets internal `FieldsStore` and re-render all `FieldFeedback`s
- `reset(): Promise` => resets internal `FieldsStore` and re-render all `FieldFeedback`s
- `FieldFeedbacksValidation` =>
- [`Field`](packages/react-form-with-constraints/src/Field.ts) =>
```TypeScript
{
fieldName: string;
isValid: () => boolean;
fieldFeedbackValidations: {
name: string;
validations: {
key: number;
isValid: boolean | undefined;
}[]; // FieldFeedbackValidation[]
type: 'error' | 'warning' | 'info' | 'whenValid';
show: boolean | undefined;
}[]; // FieldFeedbackValidation[],
isValid: () => boolean
}
```
Expand Down Expand Up @@ -267,7 +326,7 @@ You can use HTML5 attributes like `type="email"`, `required`, `pattern`..., in t
In the last case you will have to manage translations yourself (see SignUp example).
react-form-with-constraints, like React 16, depends on the collection types [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
If you support older browsers (<IE11) you will need a global polyfill such as [core-js](https://github.com/zloirock/core-js) or [babel-polyfill](https://babeljs.io/docs/usage/polyfill/).
If you support older browsers (<IE11) you will need a polyfill such as [core-js](https://github.com/zloirock/core-js) or [babel-polyfill](https://babeljs.io/docs/usage/polyfill/).
## Notes
Expand Down
70 changes: 52 additions & 18 deletions examples/Bootstrap4/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,41 @@
import React from 'react';
import ReactDOM from 'react-dom';

import { FormWithConstraints, FieldFeedback } from 'react-form-with-constraints';
import { FieldFeedbacks, FormGroup, FormControlLabel, FormControlInput } from 'react-form-with-constraints-bootstrap4';
import { FormWithConstraints, FormControlInput, FieldFeedbacks, Async, FieldFeedback } from 'react-form-with-constraints-bootstrap4';
import { DisplayFields } from 'react-form-with-constraints-tools';

import './index.html';
import './App.scss';

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function checkUsernameAvailability(value) {
console.log('checkUsernameAvailability');
await sleep(1000);
return !['john', 'paul', 'george', 'ringo'].includes(value.toLowerCase());
}

class Form extends React.Component {
constructor(props) {
super(props);

this.state = {
this.state = this.getInitialState();

this.handleChange = this.handleChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleReset = this.handleReset.bind(this);
}

getInitialState() {
return {
username: '',
password: '',
passwordConfirm: '',
submitButtonDisabled: false
};

this.handleChange = this.handleChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

async handleChange(e) {
Expand Down Expand Up @@ -57,23 +73,37 @@ class Form extends React.Component {
}
}

handleReset() {
this.setState(this.getInitialState());
this.form.reset();
}

render() {
return (
<FormWithConstraints ref={formWithConstraints => this.form = formWithConstraints}
onSubmit={this.handleSubmit} noValidate>
<FormGroup for="username">
<FormControlLabel htmlFor="username">Username</FormControlLabel>
<FormControlInput type="email" id="username" name="username"
<div className="form-group">
<label htmlFor="username">Username <small>(already taken: john, paul, george, ringo)</small></label>
<FormControlInput id="username" name="username"
value={this.state.username} onChange={this.handleChange}
required minLength={3} />
<FieldFeedbacks for="username">
<FieldFeedback when="tooShort">Too short</FieldFeedback>
<FieldFeedback when="*" />
<Async
promise={checkUsernameAvailability}
pending="..."
then={available => available ?
<FieldFeedback info style={{color: '#28a745'}}>Username available</FieldFeedback> :
<FieldFeedback>Username already taken, choose another</FieldFeedback>
}
/>
<FieldFeedback when="valid">Looks good!</FieldFeedback>
</FieldFeedbacks>
</FormGroup>
</div>

<FormGroup for="password">
<FormControlLabel htmlFor="password">Password</FormControlLabel>
<div className="form-group">
<label htmlFor="password">Password</label>
<FormControlInput type="password" id="password" name="password"
innerRef={password => this.password = password}
value={this.state.password} onChange={this.handlePasswordChange}
Expand All @@ -85,19 +115,23 @@ class Form extends React.Component {
<FieldFeedback when={value => !/[a-z]/.test(value)} warning>Should contain small letters</FieldFeedback>
<FieldFeedback when={value => !/[A-Z]/.test(value)} warning>Should contain capital letters</FieldFeedback>
<FieldFeedback when={value => !/\W/.test(value)} warning>Should contain special characters</FieldFeedback>
<FieldFeedback when="valid">Looks good!</FieldFeedback>
</FieldFeedbacks>
</FormGroup>
</div>

<FormGroup for="passwordConfirm">
<FormControlLabel htmlFor="password-confirm">Confirm Password</FormControlLabel>
<div className="form-group">
<label htmlFor="password-confirm">Confirm Password</label>
<FormControlInput type="password" id="password-confirm" name="passwordConfirm"
value={this.state.passwordConfirm} onChange={this.handleChange} />
<FieldFeedbacks for="passwordConfirm">
<FieldFeedback when={value => value !== this.password.value}>Not the same password</FieldFeedback>
</FieldFeedbacks>
</FormGroup>
</div>

<button disabled={this.state.submitButtonDisabled} className="btn btn-primary">Submit</button>{' '}
<button type="button" onClick={this.handleReset} className="btn btn-secondary">Reset</button>

<button disabled={this.state.submitButtonDisabled} className="btn btn-primary">Submit</button>
<DisplayFields />
</FormWithConstraints>
);
}
Expand Down
2 changes: 2 additions & 0 deletions examples/Bootstrap4/App.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import "~bootstrap/scss/bootstrap";
@import "~react-form-with-constraints-bootstrap4/scss/bootstrap";
2 changes: 1 addition & 1 deletion examples/Bootstrap4/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<title>Example Bootstrap4</title>

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css">
<link rel="stylesheet" href="App.css">
</head>

<body>
Expand Down
9 changes: 8 additions & 1 deletion examples/Bootstrap4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
"react-form-with-constraints": "^0.7.0",
"react-form-with-constraints-bootstrap4": "^0.7.0",

"webpack": "latest",
"bootstrap": "latest",
"node-sass": "latest",
"style-loader": "latest",
"css-loader": "latest",
"sass-loader": "latest",
"extract-text-webpack-plugin": "latest",

"webpack": "^3",
"babel-loader": "latest",
"babel-core": "latest",
"babel-preset-react": "latest",
Expand Down
Loading

0 comments on commit fa0de7f

Please sign in to comment.