UNPKG

15.9 kBMarkdownView Raw
1# react-form-with-constraints
2
3[![npm version](https://badge.fury.io/js/react-form-with-constraints.svg)](https://badge.fury.io/js/react-form-with-constraints)
4[![Node.js CI](https://github.com/tkrotoff/react-form-with-constraints/workflows/Node.js%20CI/badge.svg?branch=master)](https://github.com/tkrotoff/react-form-with-constraints/actions)
5[![codecov](https://codecov.io/gh/tkrotoff/react-form-with-constraints/branch/master/graph/badge.svg)](https://codecov.io/gh/tkrotoff/react-form-with-constraints)
6[![Bundle size](https://badgen.net/bundlephobia/minzip/react-form-with-constraints)](https://bundlephobia.com/result?p=react-form-with-constraints@latest)
7[![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8[![Airbnb Code Style](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript)
9
10Simple form validation for React
11
12- Installation: `npm install react-form-with-constraints`
13- CDN: https://unpkg.com/react-form-with-constraints/dist/
14
15Check the [changelog](CHANGELOG.md) for breaking changes and fixes between releases.
16
17## Introduction: what is HTML5 form validation?
18
19⚠️ [Client side validation is cosmetic, you should not rely on it to enforce security](https://stackoverflow.com/q/162159)
20
21```HTML
22<form>
23 <label for="email">Email:</label>
24 <input type="email" id="email" required>
25 <button type="submit">Submit</button>
26</form>
27```
28
29![input required](doc/input-required.png)
30![input type="email"](doc/input-type-email.png)
31
32The `required` HTML5 attribute specifies that the user must fill in a value, [`type="email"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email) checks that the entered text looks like an email address.
33
34Resources:
35
36- [Making Forms Fabulous with HTML5](https://www.html5rocks.com/en/tutorials/forms/html5forms/)
37- [Constraint Validation: Native Client Side Validation for Web Forms](https://www.html5rocks.com/en/tutorials/forms/constraintvalidation/)
38- [MDN - Form data validation](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation)
39- [MDN - Form input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_<input>_types)
40- [UX Research Articles - Usability Testing of Inline Form Validation](https://baymard.com/blog/inline-form-validation)
41
42## What react-form-with-constraints brings
43
44- Minimal API and footprint
45- Unobtrusive: easy to adapt regular [React code](https://reactjs.org/docs/forms.html)
46- HTML5 error messages personalization: `<FieldFeedback when="valueMissing">My custom error message</FieldFeedback>`
47- Custom constraints: `<FieldFeedback when={value => ...}>`
48- Warnings and infos: `<FieldFeedback ... warning>`, `<FieldFeedback ... info>`
49- Async validation
50- No dependency beside React (no Redux, MobX...)
51- Re-render only what's necessary
52- Easily extendable
53- [Bootstrap](examples/Bootstrap) styling with npm package `react-form-with-constraints-bootstrap`
54- [Material-UI](examples/MaterialUI) integration with npm package `react-form-with-constraints-material-ui`
55- Support for [React Native](examples/ReactNative) with npm package `react-form-with-constraints-native`
56- ...
57
58```JSX
59<input type="password" name="password"
60 value={this.state.password} onChange={this.handleChange}
61 required pattern=".{5,}" />
62<FieldFeedbacks for="password">
63 <FieldFeedback when="valueMissing" />
64 <FieldFeedback when="patternMismatch">
65 Should be at least 5 characters long
66 </FieldFeedback>
67 <FieldFeedback when={value => !/\d/.test(value)} warning>
68 Should contain numbers
69 </FieldFeedback>
70 <FieldFeedback when={value => !/[a-z]/.test(value)} warning>
71 Should contain small letters
72 </FieldFeedback>
73 <FieldFeedback when={value => !/[A-Z]/.test(value)} warning>
74 Should contain capital letters
75 </FieldFeedback>
76</FieldFeedbacks>
77```
78
79## Examples
80
81- CodePen basic Password example: https://codepen.io/tkrotoff/pen/BRGdqL ([StackBlitz version](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/Password))
82
83 ![example-password](doc/example-password.png)
84
85- [Bootstrap example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/Bootstrap)
86- [Material-UI example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/MaterialUI)
87- [WizardForm example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/WizardForm)
88- [SignUp example (React classes)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/SignUp)
89- [ClubMembers example (React classes + MobX)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/ClubMembers)
90- [Password without state example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/PasswordWithoutState)
91- [Server-side rendering example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/ServerSideRendering)
92
93- [React Native example (React classes)](examples/ReactNative):
94
95 | iOS | Android |
96 | ----------------------------------------------------- | ------------------------------------------------------------- |
97 | ![react-native-example-ios](doc/react-native-ios.png) | ![react-native-example-android](doc/react-native-android.png) |
98
99- Other examples from [the examples directory](examples):
100 - [Plain old React form validation example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/PlainOldReact)
101 - [React with HTML5 constraint validation API example (React hooks)](https://stackblitz.com/github/tkrotoff/react-form-with-constraints/tree/master/examples/HTML5ConstraintValidationAPI)
102
103## How it works
104
105The API works the same way as [React Router](https://reacttraining.com/react-router/web/example/basic):
106
107```JSX
108<Router>
109 <Route exact path="/" component={Home} />
110 <Route path="/news" component={NewsFeed} />
111</Router>
112```
113
114It is also inspired by [AngularJS ngMessages](https://docs.angularjs.org/api/ngMessages#usage).
115
116If you had to implement validation yourself, you would end up with [a global object that tracks errors for each field](examples/PlainOldReact/App.tsx).
117react-form-with-constraints [works similarly](packages/react-form-with-constraints/src/FieldsStore.ts).
118It uses [React context](https://reactjs.org/docs/legacy-context.html) 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).
119
120## API
121
122The API reads like this: "for field when constraint violation display feedback", example:
123
124```JSX
125<FieldFeedbacks for="password">
126 <FieldFeedback when="valueMissing" />
127 <FieldFeedback when="patternMismatch">Should be at least 5 characters long</FieldFeedback>
128</FieldFeedbacks>
129```
130
131```
132for field "password"
133 when constraint violation "valueMissing" display <the HTML5 error message (*)>
134 when constraint violation "patternMismatch" display "Should be at least 5 characters long"
135```
136
137(\*) [element.validationMessage](https://www.w3.org/TR/html51/sec-forms.html#the-constraint-validation-api)
138
139Async support works as follow:
140
141```JSX
142<FieldFeedbacks for="username">
143 <Async
144 promise={checkUsernameAvailability} /* Function that returns a promise */
145 then={available => available ?
146 <FieldFeedback key="1" info style={{color: 'green'}}>Username available</FieldFeedback> :
147 <FieldFeedback key="2">Username already taken, choose another</FieldFeedback>
148 // Why key=*? Needed otherwise React gets buggy when the user rapidly changes the field
149 }
150 />
151</FieldFeedbacks>
152```
153
154Trigger validation:
155
156```JSX
157function MyForm() {
158 const form = useRef(null);
159
160 async function handleChange({ target }) {
161 // Validates only the given fields and returns Promise<Field[]>
162 await form.current.validateFields(target);
163 }
164
165 async function handleSubmit(e) {
166 e.preventDefault();
167
168 // Validates the non-dirty fields and returns Promise<Field[]>
169 await form.current.validateForm();
170
171 if (form.current.isValid()) console.log('The form is valid');
172 else console.log('The form is invalid');
173 }
174
175 return (
176 <FormWithConstraints ref={form} onSubmit={handleSubmit} noValidate>
177 <input
178 name="username"
179 onChange={handleChange}
180 required minLength={3}
181 />
182 <FieldFeedbacks for="username">
183 <FieldFeedback when="tooShort">Too short</FieldFeedback>
184 <Async
185 promise={checkUsernameAvailability}
186 then={available => available ?
187 <FieldFeedback key="1" info style={{color: 'green'}}>Username available</FieldFeedback> :
188 <FieldFeedback key="2">Username already taken, choose another</FieldFeedback>
189 }
190 />
191 <FieldFeedback when="*" />
192 </FieldFeedbacks>
193 </FormWithConstraints>
194 );
195}
196```
197
198<br>
199
200**Important note:**
201
202If a field (i.e an `<input>`) does not have a matching `FieldFeedbacks`, the library won't known about this field (and thus won't perform validation).
203The field name should match `FieldFeedbacks.for`:
204
205```JSX
206<input name="MY_FIELD" ...>
207<FieldFeedbacks for="MY_FIELD">
208 ...
209</FieldFeedbacks>
210```
211
212<br>
213<br>
214
215- [`FieldFeedbacks`](packages/react-form-with-constraints/src/FieldFeedbacks.tsx)
216
217 - `for: string` => reference to a `name` attribute (e.g `<input name="username">`), should be unique to the current form
218 - `stop?: 'first' | 'first-error' | 'first-warning' | 'first-info' | 'no'` =>
219 when to stop rendering `FieldFeedback`s, by default stops at the first error encountered (`FieldFeedback`s order matters)
220
221 Note: you can place `FieldFeedbacks` anywhere, have as many as you want for the same `field`, nest them, mix them with `FieldFeedback`... Example:
222
223 ```JSX
224 <input name="username" ... />
225
226 <FieldFeedbacks for="username" stop="first-warning">
227 <FieldFeedbacks>
228 <FieldFeedback ... />
229 <Async ... />
230 <FieldFeedbacks stop="first-info">
231 ...
232 </FieldFeedbacks>
233 </FieldFeedbacks>
234
235 <FieldFeedback ... />
236 <Async ... />
237 </FieldFeedbacks>
238
239 <FieldFeedbacks for="username" stop="no">
240 ...
241 </FieldFeedbacks>
242 ```
243
244- [`FieldFeedback`](packages/react-form-with-constraints/src/FieldFeedback.tsx)
245
246 - `when?`:
247 - [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) as a string => HTML5 constraint violation name
248 - `'*'` => matches any HTML5 constraint violation
249 - `'valid'` => displays the feedback only if the field is valid
250 - `(value: string) => boolean` => custom constraint
251 - `error?: boolean` => treats the feedback as an error (default)
252 - `warning?: boolean` => treats the feedback as a warning
253 - `info?: boolean` => treats the feedback as an info
254 - `children` => what to display when the constraint matches; if missing, displays the [HTML5 error message](https://www.w3.org/TR/html51/sec-forms.html#the-constraint-validation-api) if any
255
256- [`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))
257
258 - `promise: (value: string) => Promise<T>` => a promise you want to wait for
259 - `pending?: React.ReactNode` => runs when promise is pending
260 - `then?: (value: T) => React.ReactNode` => runs when promise is resolved
261 - `catch?: (reason: any) => React.ReactNode` => runs when promise is rejected
262
263- [`FormWithConstraints`](packages/react-form-with-constraints/src/FormWithConstraints.tsx)
264
265 - `validateFields(...inputsOrNames: Array<Input | string>): Promise<Field[]>` =>
266 Should be called when a `field` changes, will re-render the proper `FieldFeedback`s (and update the internal `FieldsStore`).
267 Without arguments, all fields (`$('[name]')`) are validated.
268
269 - `validateFieldsWithoutFeedback(...inputsOrNames: Array<Input | string>): Promise<Field[]>` =>
270 Validates only all non-dirty fields (won't re-validate fields that have been already validated with `validateFields()`),
271 If you want to force re-validate all fields, use `validateFields()`.
272 Might be renamed to `validateNonDirtyFieldsOnly()` or `validateFieldsNotDirtyOnly()` in the future?
273
274 - `validateForm(): Promise<Field[]>` =>
275 Same as `validateFieldsWithoutFeedback()` without arguments, typically called before to submit the `form`.
276 Might be removed in the future?
277
278 - `isValid(): boolean` => should be called after `validateFields()`, `validateFieldsWithoutFeedback()` or `validateForm()`, indicates if the fields are valid
279
280 - `hasFeedbacks(): boolean` => indicates if any of the fields have any kind of feedback
281
282 - `resetFields(...inputsOrNames: Array<Input | string>): Field[]` =>
283 Resets the given fields and re-render the proper `FieldFeedback`s.
284 Without arguments, all fields (`$('[name]')`) are reset.
285
286 - [`Field`](packages/react-form-with-constraints/src/Field.ts) =>
287 ```TypeScript
288 {
289 name: string;
290 validations: { // FieldFeedbackValidation[]
291 key: number;
292 type: 'error' | 'warning' | 'info' | 'whenValid';
293 show: boolean | undefined;
294 }[];
295 isValid: () => boolean
296 }
297 ```
298
299- [`Input`](packages/react-form-with-constraints/src/Input.tsx)
300
301 If you want to style `<input>`, use `<Input>` instead: it will add classes `is-pending`, `has-errors`, `has-warnings`, `has-infos` and/or `is-valid` on `<input>` when the field is validated.
302
303 Example: `<Input name="username" />` can generate `<input name="username" class="has-errors has-warnings">`
304
305 FYI `react-form-with-constraints-bootstrap` and `react-form-with-constraints-material-ui` already style the fields to match their respective frameworks.
306
307## Browser support
308
309react-form-with-constraints needs [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) which is supported by all modern browsers and IE 11.
310It also needs a polyfill such as [core-js](https://github.com/zloirock/core-js) to support IE 11, see [React JavaScript Environment Requirements](https://reactjs.org/docs/javascript-environment-requirements.html).
311
312You can use HTML5 attributes like `type="email"`, `required`, [`minlength`](https://caniuse.com/#feat=input-minlength)...
313
314```JSX
315<label htmlFor="email">Email</label>
316<input type="email" name="email" id="email"
317 value={this.state.email} onChange={this.handleChange}
318 required />
319<FieldFeedbacks for="email">
320 <FieldFeedback when="*" />
321</FieldFeedbacks>
322```
323
324...and/or rely on `when` functions:
325
326```JSX
327<label htmlFor="email">Email</label>
328<input name="email" id="email"
329 value={this.state.email} onChange={this.handleChange} />
330<FieldFeedbacks for="email">
331 <FieldFeedback when={value => value.length === 0}>Please fill out this field.</FieldFeedback>
332 <FieldFeedback when={value => !/\S+@\S+/.test(value)}>Invalid email address.</FieldFeedback>
333</FieldFeedbacks>
334```
335
336In the last case you will have to manage translations yourself (see SignUp example).
337
338## Notes
339
340- A [`type="hidden"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden#Validation), [`readonly`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) or [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-disabled) input won't trigger any HTML5 form constraint validation like [`required`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-required), see https://codepen.io/tkrotoff/pen/gdjVNv