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 |
|
10 | Simple form validation for React
|
11 |
|
12 | - Installation: `npm install react-form-with-constraints`
|
13 | - CDN: https://unpkg.com/react-form-with-constraints/dist/
|
14 |
|
15 | Check 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 |
|
32 | The `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 |
|
34 | Resources:
|
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 |
|
105 | The 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 |
|
114 | It is also inspired by [AngularJS ngMessages](https://docs.angularjs.org/api/ngMessages#usage).
|
115 |
|
116 | If you had to implement validation yourself, you would end up with [a global object that tracks errors for each field](examples/PlainOldReact/App.tsx).
|
117 | react-form-with-constraints [works similarly](packages/react-form-with-constraints/src/FieldsStore.ts).
|
118 | It 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 |
|
122 | The 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 | ```
|
132 | for 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 |
|
139 | Async 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 |
|
154 | Trigger validation:
|
155 |
|
156 | ```JSX
|
157 | function 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 |
|
202 | If 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).
|
203 | The 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 |
|
309 | react-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.
|
310 | It 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 |
|
312 | You 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 |
|
336 | In 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
|