65 | );
66 | }
67 | }
68 |
69 | PropUpdater.propTypes = {
70 | children: PropTypes.element.isRequired,
71 | propsUpdate: PropTypes.object.isRequired
72 | };
73 |
74 | export { simulate } from "react-dom-testing";
75 |
76 | export default expect;
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Enform
2 |
3 | ### 👏 Thank you for contributing
4 |
5 | We are all busy with jobs, family or hobbies, but also want to spend some time on open source. Time is limited resource, though, so this short guide is here to make contribution productive:
6 |
7 | ### ⌨︎ Working with the codebase
8 | Before opening a PR:
9 | - check if what you are planning to work on is in [open issues](https://github.com/moubi/enform/issues)
10 | - if not, open a new one and attach some labels *(check below)*
11 | - if it's there - join the discussion
12 | - go ahead with your PR
13 |
14 | #### 🐞 Bug report
15 | If you are about to open new bug report issue try to be more specific about things like **version, browser, OS, steps**. Don't forget to attach the bug label.
16 |
17 | Ideally, provide codesandbox example 🙏. [Fork the basic demo](https://codesandbox.io/s/basic-form-with-enform-dv69b) if that will make it easy for you. More examples to fork from are available on [docs page](docs/index.md#documentation).
18 |
19 | #### 💡 Feature request
20 | It is good to start working on new features based on issues/discussion and not opened PR in order to avoid closing irrelevant ones. Creating an issue with feature request label is a good starting point.
21 |
22 | ### ❓ If you have a question
23 | Check the [docs](docs/index.md#documentation) first. May be you will find the answer there. How about the [open issues](https://github.com/moubi/enform/issues) - did someone already ask the same?
24 | **If not feel free to create new issue and attach the question label.**
25 |
26 | ### 🏷 Issue labels/tags
27 | Mark your issues with the following lables where it make sense:
28 | - bug - for bug reports
29 | - feature request - if you want that functionality
30 | - question - want to ask about something
31 | - help wanted - if you need help with the code
32 | - discussion - anything that you would like to bring in
33 |
34 | All supported labels are [listed here](https://github.com/moubi/enform/labels).
35 | ___
36 |
37 | ### ☕️ Thank you again for finding time for [Enform](https://github.com/moubi/enform)!
38 |
--------------------------------------------------------------------------------
/src/Enform.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | // Object props does have an order
5 | const sortObj = obj =>
6 | Object.keys(obj)
7 | .sort()
8 | .reduce(function(result, key) {
9 | result[key] = obj[key];
10 | return result;
11 | }, {});
12 |
13 | const errorsFromInitialValues = initial =>
14 | Object.keys(initial).reduce(
15 | (errors, field) => ({ ...errors, [field]: false }),
16 | {}
17 | );
18 |
19 | export const useEnform = (initial, validation) => {
20 | const [values, setValues] = useState({ ...initial });
21 | const [errors, setErrors] = useState(() => errorsFromInitialValues(initial));
22 | const ref = useRef(sortObj(initial));
23 |
24 | const reset = useCallback(() => {
25 | setValues({ ...initial });
26 | setErrors(errorsFromInitialValues(initial));
27 | }, [initial]);
28 |
29 | useEffect(() => {
30 | // That should cover most of the use cases.
31 | // JSON.stringify is reliable here.
32 | // Enform will sort before compare stringified versions of the two object
33 | // That gives a higher chance for success without the need of deep equal.
34 | // Note: JSON.stringify doesn't handle javascript Sets and Maps.
35 | // Using such in forms is considered more of an edge case and should be
36 | // handled by consumer components by turning these into an object or array
37 | const sortedInitial = sortObj(initial);
38 | if (JSON.stringify(sortedInitial) !== JSON.stringify(ref.current)) {
39 | reset();
40 | ref.current = sortedInitial;
41 | }
42 | }, [initial, reset]);
43 |
44 | const isDirty = useCallback(() => {
45 | const fields = Object.keys(initial);
46 | return fields.some(field => initial[field] !== values[field]);
47 | }, [initial, values]);
48 |
49 | const validate = useCallback((onValid = () => {}) => {
50 | if (!validation) return true;
51 | const newErrors = {};
52 |
53 | for (let i in validation) {
54 | newErrors[i] = validation[i](values) || false;
55 | }
56 |
57 | const isValid = !Object.values(newErrors).some(err => err);
58 |
59 | setErrors({
60 | ...errors,
61 | ...newErrors
62 | });
63 |
64 | if (isValid) {
65 | onValid(values);
66 | }
67 | }, [validation, errors, values]);
68 |
69 | const validateField = useCallback(name => {
70 | if (typeof values[name] !== "undefined") {
71 | if (typeof validation[name] === "function") {
72 | const isInvalid = validation[name](values) || false;
73 | setErrors({
74 | ...errors,
75 | [name]: isInvalid
76 | });
77 | // Returning the oposite: is the field valid
78 | return !isInvalid;
79 | }
80 | return true;
81 | }
82 | }, [values, validation, errors]);
83 |
84 | const clearErrors = useCallback(() => {
85 | setErrors(errorsFromInitialValues(initial));
86 | }, [initial]);
87 |
88 | const clearError = useCallback(name => {
89 | // Use an updater function here since this method is often used in
90 | // combination with onChange(). Both are setting state, so we don't
91 | // want to lose changes.
92 | setErrors(prevErrors => ({
93 | ...prevErrors,
94 | [name]: false
95 | }));
96 | }, []);
97 |
98 | const onSubmit = useCallback(submitCallback => {
99 | validate(values => {
100 | if (typeof submitCallback === "function") {
101 | submitCallback(values);
102 | }
103 | });
104 | }, [validate]);
105 |
106 | const onChange = useCallback((name, value) => {
107 | setValues(prevValues => ({
108 | ...prevValues,
109 | [name]: value
110 | }));
111 | errors[name] && clearError(name);
112 | }, [errors, clearError]);
113 |
114 | // This method is usually used with API calls to programmatically set
115 | // field errors coming as a payload and not as a result of direct user input
116 | const setErrorsIfFieldsExist = useCallback(newErrors => {
117 | if (typeof newErrors !== "object") return false;
118 | const errorsCopy = { ...errors };
119 |
120 | Object.keys(newErrors).forEach(fieldName => {
121 | if (fieldName in errorsCopy) {
122 | errorsCopy[fieldName] = newErrors[fieldName];
123 | }
124 | });
125 |
126 | setErrors({ ...errorsCopy });
127 | }, [errors]);
128 |
129 | return {
130 | values,
131 | errors,
132 | onChange,
133 | onSubmit,
134 | isDirty,
135 | validateField,
136 | clearError,
137 | clearErrors,
138 | setErrors: setErrorsIfFieldsExist,
139 | reset
140 | };
141 | };
142 |
143 | export default function Enform({ initial, validation, children }) {
144 | return children(useEnform(initial, validation));
145 | }
146 |
147 | Enform.propTypes = {
148 | children: PropTypes.func.isRequired,
149 | initial: PropTypes.object.isRequired,
150 | validation: PropTypes.object
151 | };
152 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
18 |
19 | #### `` helps you manage:
20 | - form validation
21 | - form dirty state
22 | - form submission and reset
23 | - field values and changes
24 | - error messages
25 |
26 | Forms in React are common source of frustration and code repetition. Enform moves that hassle out of the way.
27 |
28 | **✔️ Check the [docs with live demos](docs/index.md#documentation) or jump to the [basic usage](#basic-usage).**
29 |
30 | ## So, another form component?
31 | Many form libraries are after wide range of use cases. As a result they grow in size bigger than the few form components one may ever need to handle. **Enform is built for most common use cases without going to the edge. Thanks to that it remains small, but very powerful.**
32 |
33 | ## Basic usage
34 |
35 |
36 | ```jsx
37 | import React from "react";
38 | import Enform from "enform";
39 |
40 | const App = () => (
41 |
62 | );
63 | ```
64 | [](https://codesandbox.io/s/newsletter-form-with-enform-dv69b?fontsize=14&hidenavigation=1&theme=dark)
65 |
66 | **View more intereactive [examples here](docs/index.md#documentation)**.
67 |
68 | This [⚠️ note on re-rendering](docs/index.md#%EF%B8%8F-note-on-re-rendering) gives answers to some common questions about auto updating field values based on changes in the `initial` prop.
69 |
70 | ## Install
71 | ```
72 | yarn add enform
73 | ```
74 |
75 | ### Requirements ✅
76 | Enform is using React hooks ↩ as per `v2.0.0`.
77 |
78 | Consumer projects should have react >= 16.8.0 (the one with hooks) in order to use it.
79 |
80 | ## API
81 | ### Component props
82 | | Prop | Type | Required | Description |
83 | | ------------- | ------------- | -------- | ----------- |
84 | | children | function | yes | Function that your need to wrap your DOM with. It accepts the `props` object to help with form state manipulation. |
85 | | [initial](docs/index.md#initial--fieldname-value----required) | object | yes | Initial form field values in a form of `{ fieldName: value, ... }`. |
86 | | [validation](docs/index.md#validation--fieldname-functionvalues--boolstring-) | object | no | Validation for the fields. It takes the form of `{ fieldName: function(values), ... }` where `function(values)` accepts all form field values and should return an error message or truthy value. Example: `{ username: values => values.username === "" ? "This field is required" : false }`. |
87 |
88 | ✔️ Read more about these props [here](docs/index.md#enform-component-props).
89 |
90 | ### State Api
91 | Enform exposes its handy Api by passing an `object` down to the function wrapper.
92 | ```jsx
93 |
94 | {props => (
95 |
96 | ...
97 |
98 | )}
99 |
100 | ```
101 | **The `props` object contains 2 data items:**
102 | |prop|Description|
103 | |-|-|
104 | | [values](docs/index.md#propsvalues--fieldname-value-) | Current field values - `{ fieldName: value, ... }`. |
105 | | [errors](docs/index.md#propserrors--fieldname-value-) | Current field errors - `{ fieldName: errorMessage, ... }`. |
106 |
107 | **and these 8 methods:**
108 |
109 | |method|Description|
110 | |-|-|
111 | | [onChange](docs/index.md#propsonchange-fieldname-value--void) | Updates single field's value - `onChange(fieldName, value)`. The `value` is usually what what comes from `e.target.value`. **Side effects:** clears previously set field error. |
112 | | [onSubmit](docs/index.md#propsonsubmit-functionvalues--void--void) | `onSubmit(successCallback)`. Usually attached to a button click or directly to `` onSubmit. `successCallback(values)` will only be executed if all validations pass. **Side effects:** triggers validation or calls successCallback. |
113 | | [reset](docs/index.md#propsreset---void) | Empties form elements. |
114 | | [isDirty](docs/index.md#propsisdirty---bool) | Reports if the form is dirty. It takes into account the `initial` field values passed to ``. |
115 | | [validateField](docs/index.md#propsvalidatefield-fieldname--bool) | Triggers single form field validation - `validateField(fieldName)`. |
116 | | [clearError](docs/index.md#propsclearerror-fieldname--void) | Clears single form field's error - `clearError(fieldName)`. |
117 | | [clearErrors](docs/index.md#propsclearerrors---void) | Clears all errors in the form. |
118 | | [setErrors](docs/index.md#propssetErrors--fieldName-errorMessagebool---void) | Sets Enform's internal error state directly. This may be handy when `props.errors` needs to be updated based on an API call (async) and not on user input. Example: `setErrors({ email: "Example error message" }, ...)` |
119 |
120 | `props.values` get updated with `onChange` and `reset` calls.
121 |
122 | `props.errors` get updated with `onChange`, `onSubmit`, `reset`, `validateField`, `clearError`, `clearErrors` and `setErrors` calls.
123 |
124 | ✔️ See more details about [Enform's state API](docs/index.md#enform-state-api).
125 |
126 | ## [Documentation](docs/index.md)
127 | Docs has its own home [here](docs/index.md#documentation). It further expands on the topics covered previously. Many [examples](docs/index.md#examples) and [how to guides](docs/index.md#how-to) for variety of use cases take place on its pages too. Ref to this [⚠️ note on re-rendering](docs/index.md#%EF%B8%8F-note-on-re-rendering) for a common pitfall case.
128 |
129 | ## Development
130 | Run tests with `jest` in watch mode
131 | ```
132 | yarn test
133 | ```
134 | or no watch
135 |
136 | ```
137 | yarn test:nowatch
138 | ```
139 | Get gzip size by
140 | ```
141 | yarn size
142 | ```
143 |
144 | Build with
145 | ```
146 | yarn build
147 | ```
148 | That will pipe `src/Enform.js` through babel and put it as `index.js` under `lib/` folder.
149 |
150 | ## Contributing
151 | You are welcome to open pull requests, issues with bug reports (use [codesandbox](https://codesandbox.io/)) and suggestions or simply **tweet about Enform**. Check the relavant [guides here](CONTRIBUTING.md).
152 |
153 | **Immediate and fun contrubution:** help create more usable examples. Is it a full-fetured form, third party integration or a filter form with bunch of options - feel free fork the [basic form in codesandbox](https://codesandbox.io/s/basic-form-with-enform-dv69b).
154 |
155 | ## Inspiration
156 | Enform is inspired by my experience with form refactoring, [@jaredpalmer](https://jaredpalmer.com/)'s great work on [Formik](https://github.com/jaredpalmer/formik) and the way [@kamranahmedse](https://github.com/kamranahmedse)'s presented [driver.js](https://github.com/kamranahmedse/driver.js).
157 |
158 | ## Authors
159 | Miroslav Nikolov ([@moubi](https://github.com/moubi))
160 |
161 | ## License
162 | [MIT](LICENSE)
163 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | - [Overview](#overview)
4 | - [Examples](#examples)
5 | - [Basic form (field and a button)](#basic-form-field-and-a-button)
6 | - [Newsletter form](#newsletter-form)
7 | - [Registration form](#registration-form)
8 | - [Form with dynamic elements](#form-with-dynamic-elements)
9 | - [Full-featured form](#full-featured-form)
10 | - [API](#api)
11 | - [Enform component props](#enform-component-props)
12 | - [initial](#initial--fieldname-value----required)
13 | - [validation](#validation--fieldname-functionvalues--boolstring-)
14 | - [Enform state API](#enform-state-api)
15 | - [props.values](#propsvalues--fieldname-value-)
16 | - [props.errors](#propserrors--fieldname-value-)
17 | - [props.onChange](#propsonchange-fieldname-value--void)
18 | - [props.onSubmit](#propsonsubmit-functionvalues--void--void)
19 | - [props.reset](#propsreset---void)
20 | - [props.isDirty](#propsisdirty---bool)
21 | - [props.validateField](#propsvalidatefield-fieldname--bool)
22 | - [props.clearError](#propsclearerror-fieldname--void)
23 | - [props.clearErrors](#propsclearerrors---void)
24 | - [props.setErrors](#propssetErrors--fieldName-errorMessagebool---void)
25 | - [How to](#how-to)
26 | - [handle validation](#handle-validation)
27 | - [reset a form](#reset-a-form)
28 | - [submit a form](#submit-a-form)
29 | - [disable button based on dirty state](#disable-button-based-on-dirty-state)
30 | - [handle contentEditable elements](#handle-contenteditable-elements)
31 | - [handle form-like DOM](#handle-form-like-dom)
32 | - [⚠️ Note on re-rendering](#%EF%B8%8F-note-on-re-rendering)
33 |
34 | ## Overview
35 | Enform was born while trying to deal with forms in React repetitive times with state involved in the picture as usual. Let's face it, things always end up the same. The result is a big state object to manage and a bunch of component methods to handle changes, submission and validation.
36 |
37 | It feels like these should be somehow hidden or extracted away in another component. `` is such a component that uses the **"render props" pattern. It nicely moves that state management away by taking advantage of React's superpower**.
38 |
39 | Ok, enough theory, let's see some real use cases.
40 |
41 | ## Examples
42 | All examples in this section are available in [Codesandbox](https://codesandbox.io/s/basic-form-with-enform-dv69b) with the latest version of Enform. Feel free to experiment, fork or share. Ping me if you think I have messed something up 🤭.
43 |
44 | ### Basic form (field and a button)
45 |
46 |
47 | ```jsx
48 | import React from "react";
49 | import Enform from "enform";
50 |
51 | const App = () => (
52 |
73 | );
74 | ```
75 | [](https://codesandbox.io/s/newsletter-form-with-enform-dv69b?fontsize=14&hidenavigation=1&theme=dark)
76 |
77 | **Few things to note here:**
78 | - `initial` (required) prop is set with the field's default value
79 | - `validation` object sets the field should not be empty
80 | - `props.onSubmit` is bound to the button click. It will submit whenever validation is passed
81 | - the input field is fully controlled with the help of `props.values` and `props.onChange`.
82 |
83 | This [⚠️ note on re-rendering](#%EF%B8%8F-note-on-re-rendering) may save few hours of headache too.
84 | ___
85 |
86 | ### Newsletter form
87 |
88 |
89 | ```jsx
90 |
94 | !/^[A-Za-z0-9._%+-]{1,64}@(?:[A-Za-z0-9-]{1,63}\.){1,125}[a-z]{2,63}$/.test(
95 | values.email
96 | )
97 | }}
98 | >
99 | {props => (
100 |
112 | )}
113 |
114 | ```
115 | [](https://codesandbox.io/s/newsletter-form-with-enform-t1zyk?fontsize=14&hidenavigation=1&theme=dark)
116 |
117 | In this example `validation` is set for the email field using RegEx. It will return `true` if email is invalid or `false` otherwise. All [validator functions](#validation--fieldname-functionvalues--boolstring-) must return truthy value in case of error.
118 | ___
119 |
120 | ### Registration form
121 |
122 |
123 |
124 | Expand code snippet
125 |
126 | ```jsx
127 | {
138 | if (values.password.length < 6) {
139 | return "Password must be at least 6 chars in length!";
140 | } else if (values.password !== values.repeatPassword) {
141 | return "Password doesn't match!";
142 | }
143 | return false;
144 | },
145 | repeatPassword: values =>
146 | values.repeatPassword.length < 6
147 | ? "Password must be at least 6 chars in length!"
148 | : false
149 | }}
150 | >
151 | {props => (
152 |
170 | )}
171 |
172 | ```
173 |
174 |
175 | [](https://codesandbox.io/s/registration-form-with-enform-u6up9?fontsize=14&hidenavigation=1&theme=dark)
176 |
177 | This example is shortened, so that it's easy to focus on two interesting parts - **password validation** and **clearing errors**. Take a look at the [full demo in the codesandbox](https://codesandbox.io/s/registration-form-with-enform-u6up9?fontsize=14&hidenavigation=1&theme=dark).
178 |
179 | This registration form displays error messages as well. In order that to work each validator function must return the error string in case of error. The `password` validation depends on both password and repeatPassword values, so it will display two different error messages.
180 |
181 | **Secondly**, password's `onChange` should also clear the error for `repeatPassword`. `props.onChange("password", e.target.value)` does so for the password field, but it needs to be done for repeatPassword as well. That can be achieved by calling `props.clearError("repeatPassword")`.
182 | ___
183 |
184 | ### Form with dynamic elements
185 |
186 |
187 |
188 | Expand code snippet
189 |
190 | ```jsx
191 |
202 | !/^[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,125}[a-zA-Z]{2,63}$/.test(
203 | values.email
204 | ),
205 | // Spread the validation object for the rest of the fields -
206 | // stored in the component's state.
207 | ...this.state.fieldsValidation
208 | }}
209 | >
210 | {props => (
211 |
212 | {/* Map your newly added fields to render in the DOM */}
213 | {Object.keys(this.state.fields).map(field => (
214 |
239 | )}
240 |
241 | ```
242 |
243 |
244 | [](https://codesandbox.io/s/dynamic-form-fields-with-enform-bnho9?fontsize=14&hidenavigation=1&theme=dark)
245 |
246 | Enfrom does not automatically handle dynamic form elements (adding or removing felds), but it is possible to make it aware of such changes with few adjustments. The example above is a short version of the [codesandbox demo](https://codesandbox.io/s/dynamic-form-fields-with-enform-bnho9?fontsize=14&hidenavigation=1&theme=dark).
247 |
248 | **Let's start with the basics:** Enform wraps the form DOM and helps with handling its state. But it's required to make Enform aware when new DOM is added, just because it needs to be controlled being a form element. In this example **Enform is forced to reinitialize** whenever more fields are added or removed. It is done by setting the `key={fieldNames.length}` prop.
249 |
250 | Next logical step would be to **update** the `initial` and `validation` props with up to date fields data. *The data could be stored in the consumer's component state fx*. Last thing to do - render all these newly added fields. Enform will do the rest as usual.
251 | ___
252 |
253 | ### Full-featured form
254 |
255 |
256 |
257 | Expand code snippet
258 |
259 | ```jsx
260 |
275 | !/^[A-Za-z0-9._%+-]{1,64}@(?:[A-Za-z0-9-]{1,63}\.){1,125}[A-Za-z]{2,63}$/.test(
276 | email
277 | )
278 | ? "Enter valid email address!"
279 | : false,
280 | password: ({ password }) =>
281 | password.length < 6
282 | ? "Password must be at least 6 chars in length!"
283 | : false,
284 | age: ({ age }) => (age === "" ? "Select age range" : false),
285 | bio: ({ bio }) => (bio.length > 140 ? "Try to be shorter!" : false)
286 | }}
287 | >
288 | {props => (
289 |
290 |
291 | {
296 | props.onChange("email", e.target.value);
297 | // This will validate on every change.
298 | // The error will disappear once email is valid.
299 | if (props.errors.email) {
300 | props.validateField("email");
301 | }
302 | }}
303 | />
304 |
446 | )}
447 |
448 | ```
449 |
450 |
451 | [](https://codesandbox.io/s/full-featured-form-with-enform-qw3tu?fontsize=14&hidenavigation=1&theme=dark)
452 |
453 | Demonstration of Enform handling full-featured form, using all API props and methods.
454 |
455 | **Few interesting points:**
456 | - **Passing custom callback to `onSubmit`.** The handler is attached to the submit button and simply alerts all field values in pretty format.
457 | - **Resetting the form.** With the `props.reset()` call fields are reverted back to `initial` values and all errors are cleared.
458 | - **Clear error on focus.** This is done by calling `props.clearError("bio")` when focusing the bio field.
459 | - **Validate while typing.** Calling `props.validateField("email")` as part of the `onChange` handler will trigger validation for each change.
460 | ___
461 |
462 | ## API
463 | `` component wraps the form DOM (or custom component) and enables state control via `props`. They are split in two groups - props set to `` directly and props that it passes down to the consumer component (DOM).
464 |
465 | ### Enform component props
466 | Two props can be set to the component itself - `initial` and `validation`.
467 | ```jsx
468 | values.username.length === 0 }}
471 | >
472 | ...
473 |
474 | ```
475 |
476 | #### `initial: { fieldName: value }` - required
477 | The prop is the only **required one**. It is so, because it needs to tell Enform what is the current field structure. The initial value of each field should be a valid React element's value. That means if there is a `checkbox` fx. it make sense for its initial value to be `boolean`. The structure could be something like `{ email: "", password: "", newsletter: false }`.
478 |
479 | #### `validation: { fieldName: function(values) => bool|string) }`
480 | It is used for specifying validation conditions and error messages. Don't set it if no validation is needed. The `key` (field name) should be the same as in the `initial`. The `value` is a validator function which accepts all field values. Validators should return an **error message** when field is invalid or just `true` if no messages are needed. The following: `{ username: values => values.username.length === 0 }` returns a boolean simply telling if the field is empty. Such a condition may be useful when setting an error class to a field is enough. Setting up error messages is achieved with something like `{ username: values => values.username.length === 0 ? "This field is required" : "" }`. **If validation is passing it will always default to `false` value for that field in the errors object.**
481 | ___
482 |
483 | ### Enform state API
484 | Enform manages the form's state and provides access to it by exposing several props and methods. These are passed down to the component (DOM) via the `props` object.
485 |
486 | ```jsx
487 |
488 | {props => (
489 |
490 | ...
491 |
492 | )}
493 |
494 | ```
495 |
496 | #### `props.values: { fieldName: value }`
497 | Object containing all field values. The signature is `{ fieldName: value }` where `fieldName` is the field name as defined in the [initial](#initial--fieldname-value----required) and `value` is the current value of the element.
498 |
499 | `props.values` get updated when calling:
500 | - props.onChange
501 | - props.reset
502 |
503 | #### `props.errors: { fieldName: value }`
504 | Object containing errors for all fields. These are either the error messages or simply the boolean `true`. `fieldName` is the same field name defined in the [initial](#initial--fieldname-value----required) while `value` is returned from the validator function (error message or boolean) defined in [validation](#validation--fieldname-functionvalues--boolstring-). In case of no error `props.errors.` will be `false`.
505 |
506 | `props.errors` get updated when calling:
507 | - props.onChange
508 | - props.onSubmit
509 | - props.validateField
510 | - props.clearError
511 | - props.clearErrors
512 | - props.setErrors
513 | - props.reset
514 |
515 | #### `props.onChange: (fieldName, value) => void`
516 | Handler method used for setting the value of a field.
517 |
518 | ```jsx
519 |
520 | {props => (
521 | ...
522 | {
523 | props.onChange("email", e.target.value);
524 | }}>
525 | )}
526 |
527 | ```
528 | As a **side effect** calling this method will also **clear** previously set error for that field.
529 |
530 | #### `props.onSubmit: (function(values) => void) => void`
531 | By calling `props.onSubmit()` Enform will do the following: **trigger validation** on all fields and either set the corresponding **errors** or **call** the `successCallback` if validation is passed. `successCallback` accepts the `values` object as an argument.
532 |
533 | ```jsx
534 |
535 | {props => (
536 |
542 | )}
543 |
544 | ```
545 |
546 | #### `props.reset: () => void`
547 | Clears all fields and errors. Calling `props.reset()` will set the fields back to their `initial` values. As a side effect it will also clear the errors as if `props.clearErrors()` was called. The following [full-featured form](#full-featured-form) uses `props.reset()` on button click.
548 |
549 | #### `props.isDirty: () => bool`
550 | Calling `props.isDirty()` reports if form state has changed. It does so by performing comparison between fields current and `initial` values. Since it is an expensive operation Enform does't keep track of dirty state internally. That's why isDirty is method instead.
551 |
552 | #### `props.validateField: (fieldName) => bool`
553 | It triggers validation for a single field (ex. `props.validateField("email")`). As a result the validator function (if any) for that field will be executed and the corresponding value in `props.errors` set. Common use case would be if a field needs to be validated every time while user is typing.
554 |
555 | ```jsx
556 |
557 | {props => (
558 | {
560 | props.onChange("email", e.target.value);
561 | props.validateField("email");
562 | }}
563 | />
564 | )}
565 |
566 | ```
567 |
568 | #### `props.clearError: (fieldName) => void`
569 | Clears the error for a single field. (ex. `props.clearError("email")`). Calling `props.onChange()` will do that by default, but `props.clearError` is built for other cases. An example is clearing an error as part of `onFocus`.
570 |
571 | ```jsx
572 |
573 | {props => (
574 | {
576 | props.clearError("email");
577 | }}
578 | onChange={e => {
579 | props.onChange("email", e.target.value);
580 | }}
581 | />
582 | )}
583 |
584 | ```
585 |
586 | #### `props.clearErrors: () => void`
587 | Calling this method will clear all errors for all fields.
588 |
589 | #### `props.setErrors: ({ fieldName: errorMessage|bool }) => void`
590 | This method can be used to directly set field errors.
591 |
592 | **Consider this use case:** a valid form has been submitted, but based on some server side validtion one of the field values appeared to be invalid. The server returns back this field's specific error or error message. Then in the form it is also necessary to display the error message next to the corresponding field. For such scenarios `props.setErrors()` comes to the rescue.
593 |
594 | ```javascript
595 | setErrors({
596 | username: "Already exists",
597 | password: "Password too long",
598 | ...
599 | })
600 | ```
601 | If a key part of the error object passed to `setErrors` does't match any field name (defined in the `initial` prop) it will simply be ignored.
602 | ___
603 |
604 | ## How to
605 | The idea of these short guides is to elaborate a little bit more on specific areas. Something that is done often when handling forms - validation, resetting/submitting, button states and so on. It will also touch few non trivial uses cases like handling `contentEditables`, **third party** integrations and form with `` elements.
606 |
607 | ### Handle validation
608 | Quick validator function examples.
609 |
610 | #### Simple error indication
611 | ```jsx
612 | values.name.length === "" }}
615 | >
616 | ```
617 | If name field is empty `props.errors.name` will be set to `true`. Otherwise it will go `false`.
618 |
619 | #### With error message
620 | ```jsx
621 | (
624 | values.name.length === "" ? "This field can not be empty" : ""
625 | )}}
626 | >
627 | ```
628 | If name field is empty the `"This field can not be empty"` message will be stored in `props.errors.name`. Otherwise it will be `false`.
629 |
630 | #### Validation while typing
631 | ```jsx
632 | values.name.length < 3 }}
635 | >
636 | {props =>
637 | {
641 | props.onChange("name", e.target.value);
642 | props.validateField("name");
643 | }}
644 | />
645 | }
646 |
647 | ```
648 | The name field validator will be called every time user is typing in the field. This will cause `props.errors.name` to be updated constantly and cleared (with `props.onChange`) once the value reaches at least `3` chars.
649 |
650 | #### Password validation
651 | Typical example is a signup form with `password` and `repeatPassword` fields. Find more details in this [full codesandbox demo](https://codesandbox.io/s/registration-form-with-enform-u6up9?fontsize=14&hidenavigation=1&theme=dark).
652 |
653 | ```jsx
654 | {
658 | if (values.password.length < 6) {
659 | return "Password must be at least 6 chars in length!";
660 | } else if (values.password !== values.repeatPassword) {
661 | return "Password doesn't match!";
662 | }
663 | return false;
664 | },
665 | repeatPassword: values =>
666 | values.repeatPassword.length < 6
667 | ? "Password must be at least 6 chars in length!"
668 | : false
669 | }}
670 | >
671 | ```
672 | Current validation sets error messages for both password and repeatPassword if their values are less than `6` chars. `props.errors.password` will also store error message for **values missmatch**. This validator is an example on how several field values could be combined.
673 | ___
674 |
675 | ### Reset a form
676 | Let's see how to reset a form on button click:
677 |
678 | ```jsx
679 |
680 | {props =>
681 |
684 | }
685 |
686 | ```
687 | Resetting a form with Enform will **reset all fields** and **clear all errors**. The action is similar to re-initializing. See this [full form demo](https://codesandbox.io/s/full-featured-form-with-enform-qw3tu?fontsize=14&hidenavigation=1&theme=dark) for usage example.
688 | ___
689 |
690 | ### Submit a form
691 | There are few ways to handle form submission with Enform.
692 |
693 | ```jsx
694 |
695 | {props =>
696 |
699 | }
700 |
701 | ```
702 | Calling `props.onSubmit()` as part of button `onClick` handler.
703 |
704 | or
705 |
706 | ```jsx
707 |
708 | {props =>
709 |
715 | }
716 |
717 | ```
718 | Calling `props.onSubmit()` as part of ``'s `onSubmit` handler. Note that it is often reasonable to also prevent form default behavior when submitting. This is because Enform works with controlled form elements.
719 |
720 | What if calling an **Api endpoint** or some other action to deal with the **form values** is required on submission? Passing a custom `successCallback` will help in that case:
721 |
722 | ```jsx
723 |
731 | ```
732 | If success callback function is provided to `props.onSubmit()` it will be called only if all fields are **passing their validation**. Check [the demo here](https://codesandbox.io/s/full-featured-form-with-enform-qw3tu?fontsize=14&hidenavigation=1&theme=dark) with full code example.
733 | ___
734 |
735 | ### Disable button based on dirty state
736 | It is a common use case to enable/disable form buttons when dirty state changes.
737 |
738 | ```jsx
739 |
740 | {props =>
741 |
742 | }
743 |
744 | ```
745 | The submit button will render as `disabled` if form is not dirty - has no changes. Or in other words - all fields are displaying their `initial` values.
746 | ___
747 |
748 | ### Handle contentEditable elements
749 | ContentEditable elements are sometimes very quirky. With React it is often required to additionally manage cursor position and console warnings. Below is a basic example of `contentEditable` div:
750 |
751 | ```jsx
752 |
753 | {props => (
754 |
762 | )}
763 |
764 | ```
765 | There are few differences with the standard ``. Instead of `onChange` changes are registered via `onInput`, the text resides in `e.target.innerText` and the value is placed as a `child` of the div.
766 | ___
767 |
768 | ### Handle form-like DOM
769 | All examples so far show how Enform works with controlled elements and components. It doesn't need to know about DOM structure at all. **An interesting idea emerges - is it possible to manage the state of something that is not a form? `That is possible`.** Let's see the following example:
770 |
771 | ```jsx
772 |
773 | {props => (
774 |
781 | )}
782 |
783 | ```
784 | The `` element takes it's default text from the `initial` object and updates it on click with some data that comes from the consumer component's state. Such cases could be expanded further more, but the idea is `it should be possible to use Enform for state management of anything that deals with values and validation`.
785 | ___
786 |
787 | ## ⚠️ Note on re-rendering
788 | It is important to note that `` **will do its best to re-render when `initial` values change**. That is done by comparing the current and previous `initial` values.
789 |
790 | Usage like this fx is **guaranteed** to work:
791 | ```jsx
792 | class ConsumerComponent extends Component {
793 | ...
794 |
795 | render() {
796 | {/* newly created initial object is passed on each render */}
797 |
798 | {props => (
799 | {/* input will render correct value whenever state changes */}
800 |
801 | )}
802 |
803 | }
804 | }
805 | ```
806 |
807 | Something like that should also work:
808 | ```jsx
809 | class ConsumerComponent extends Component {
810 | constructor(props) {
811 | super(props);
812 | // Defining it just for the sake of causing re-render
813 | this.state = { loading: false };
814 | // name with default value of ""
815 | this.initial = { name: "" };
816 | }
817 |
818 | componentDidMount() {
819 | this.setState({ loading: true });
820 |
821 | this.getName().then(name => {
822 | this.setState({ loading: false });
823 | // An API call that will set name to "Justin Case"
824 | this.initial.name = name;
825 | });
826 | }
827 |
828 | render() {
829 |
830 | {props => (
831 | {/* ⚠️ NOTE: the input will still render "" */}
832 |
833 | )}
834 |
835 | }
836 | }
837 | ```
838 |
839 | ### Using Javascript `Set` and `Map` as values
840 | This is considered more as an edge case and may cause issues in some cases. Fx. Enform uses sorting and `stringification` to compare `initial` values, but `JSON.stringify` doesn't transform `Set` and `Map` structures correctly. It may lead to the state not being updated correctly.
841 |
842 | Ensure these values are transformed to an `Object` or `Array` before passing down.
843 |
844 | ### What is the solution?
845 | [Recommended technique](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key) would be to use the special `key` prop forcing that way Enform to update. Let's see the changes in the `render()` method:
846 |
847 | ```jsx
848 | render() {
849 |
854 | {props => (
855 | {/* input will render "Justin Case" */}
856 |
857 | )}
858 |
859 | }
860 | ```
861 | The only difference is that now the `key` prop is set to `this.state.name` telling React to *create a new component instance rather than update the current one*.
862 | ___
863 |
864 |