{"id":15387158,"url":"https://github.com/wildhoney/formv","last_synced_at":"2025-04-15T18:30:53.177Z","repository":{"id":34924365,"uuid":"190776617","full_name":"Wildhoney/Formv","owner":"Wildhoney","description":"🗳React form validation using the validation native to all recent browsers. Also includes support for handling API validation messages, success messages, memoized and nested form state, and super easy styling.","archived":false,"fork":false,"pushed_at":"2023-01-06T01:53:30.000Z","size":5142,"stargazers_count":9,"open_issues_count":21,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-14T20:42:53.340Z","etag":null,"topics":["form","form-validation","react","react-form","react-form-component","react-form-helper","react-form-validation","react-form-validator","react-forms","react-formutil","validation","validation-rules","validation-util"],"latest_commit_sha":null,"homepage":"https://formv.herokuapp.com/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Wildhoney.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-07T16:34:04.000Z","updated_at":"2023-09-13T23:33:57.000Z","dependencies_parsed_at":"2023-01-15T10:33:08.925Z","dependency_job_id":null,"html_url":"https://github.com/Wildhoney/Formv","commit_stats":null,"previous_names":[],"tags_count":68,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wildhoney%2FFormv","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wildhoney%2FFormv/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wildhoney%2FFormv/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wildhoney%2FFormv/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Wildhoney","download_url":"https://codeload.github.com/Wildhoney/Formv/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249128926,"owners_count":21217244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["form","form-validation","react","react-form","react-form-component","react-form-helper","react-form-validation","react-form-validator","react-forms","react-formutil","validation","validation-rules","validation-util"],"created_at":"2024-10-01T14:52:17.941Z","updated_at":"2025-04-15T18:30:52.551Z","avatar_url":"https://github.com/Wildhoney.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"example/images/logo.png\" alt=\"Formv\" width=\"150px\" /\u003e\n\n\u003e React form validation using the validation native to all recent browsers. Also includes support for handling API validation messages, success messages, memoized and nested form state, and super easy styling.\n\n![Travis](http://img.shields.io/travis/Wildhoney/Formv.svg?style=for-the-badge)\n\u0026nbsp;\n![npm](http://img.shields.io/npm/v/formv.svg?style=for-the-badge)\n\u0026nbsp;\n![License MIT](http://img.shields.io/badge/license-mit-lightgrey.svg?style=for-the-badge)\n\u0026nbsp;\n![Coveralls](https://img.shields.io/coveralls/Wildhoney/Formv.svg?style=for-the-badge)\n\u0026nbsp;\n[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=for-the-badge)](https://github.com/prettier/prettier)\n\n---\n\n## Contents\n\n1. [Getting Started](#getting-started)\n2. [Customising Messages](#customising-messages)\n3. [Skipping Validation](#skipping-validation)\n4. [JS Validation](#js-validation)\n5. [API Validation](#api-validation)\n6. [Success Messages](#success-messages)\n7. [Managing State](#managing-state)\n8. [Form Architecture](#form-architecture)\n\n## Getting Started\n\nFormv utilises the native [form validation](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation) which is built-in to all recent browsers \u0026ndash; as such, all validation rules are set on the relevant form fields using `required`, `pattern`, `minLength`, etc...\n\nFormv has a philosophy that it should be easy to opt-out of form validation if and when you want to use another technique in the future. That means not coupling your validation to a particular method, which makes it easily reversible \u0026ndash; that is why Formv comes with only one fundamental React component \u0026ndash; `Form`.\n\nTo get started you need to append the form to the DOM. Formv's `Form` component is a plain `form` element that intercepts the `onSubmit` function. We then nest all of our input fields in the `Form` component as you would normally. `Form` takes an optional function child as a function to pass the form's state \u0026ndash; you can also use the context API for more complex forms.\n\nIn the examples below we'll take a simple form that requires a name, email and an age. We'll add the front-end validation, capture any back-end validation errors, and show a success message when everything has been submitted.\n\n```jsx\nimport { Form, Messages } from 'formv';\n\nexport default function MyForm() {\n    return (\n        \u003cForm onSubmitted={handleSubmitted}\u003e\n            {({ isDirty, isSubmitting, feedback }) =\u003e (\n                \u003c\u003e\n                    \u003cMessages value={feedback.success} /\u003e\n                    \u003cMessages value={feedback.error} /\u003e\n\n                    \u003cinput type=\"text\" name=\"name\" required /\u003e\n                    \u003cMessages values={feedback.field.name} /\u003e\n\n                    \u003cinput type=\"email\" name=\"email\" required /\u003e\n                    \u003cMessages values={feedback.field.email} /\u003e\n\n                    \u003cinput name=\"age\" required min={18} /\u003e\n                    \u003cMessages values={feedback.field.age} /\u003e\n\n                    \u003cbutton disabled={!isDirty} type=\"submit\"\u003e\n                        {isSubmitting ? 'Submitting...' : 'Submit'}\n                    \u003c/button\u003e\n                \u003c/\u003e\n            )}\n        \u003c/Form\u003e\n    );\n}\n```\n\n**Note:** `isDirty` is an opt-in with the `withDirtyCheck` prop as it causes form data comparisons upon form change.\n\nVoila! Using the above code you have everything you need to validate your form. By clicking the `button` all validation rules will be checked, and if you've not filled in the required fields then you'll see a message appear next to the relevant `input` fields. We are also using the `isSubmitting` to determine when the form is in the process of being submitted \u0026ndash; including any async API requests, and also a dirty check to determine if the form data has been modified from its original state.\n\n## Customising Messages\n\nIt's good and well relying on the native validation, but if you were to look in different browsers, each validation message would read slightly differently \u0026ndash; which is awful for applications that strive for consistency! In those cases the `Form` component accepts a `messages` which is a map of [the `ValidityState` object](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState). By using the `messages` prop we can provide consistent messages across all browsers.\n\n```jsx\nimport { Form, Messages } from 'formv';\n\nconst messages = {\n    name: { valueMissing: 'Please enter your first and last name.' },\n    email: {\n        valueMissing: 'Please enter your email address.',\n        typeMismatch: 'Please enter a valid email address.',\n    },\n    age: {\n        valueMissing: 'Please enter your age.',\n        rangeUnderflow: 'You must be 18 or over to use this form.',\n    },\n};\n\n\u003cForm messages={messages} onSubmitted={handleSubmitted} /\u003e;\n```\n\nNow when you submit the form in Chrome, Firefox, Edge, Safari, Opera, etc... you will note that all of the messages appear the same \u0026ndash; you're no longer allowing the browser to control the content of the validation messages.\n\n## Skipping Validation\n\nThere are _some_ instances where skipping validation might be beneficial. Although you can't skip the back-end validation from `Formv` directly \u0026mdash; you should configure your back-end to accept an optional param that skips validation \u0026mdash; it's quite easy to skip the front-end validation by introducing another `button` element with the native [`formnovalidate` attribute](https://www.w3schools.com/jsref/prop_form_novalidate.asp).\n\nIn the above form if you were to add another button alongside our existing `button`, you can have one `button` that runs the front-end validation in its entirety, and another `button` that skips it altogether.\n\n```jsx\n\u003cbutton type=\"submit\"\u003eSubmit\u003c/button\u003e\n\u003cbutton type=\"submit\" formNoValidate\u003eSubmit Without Validation\u003c/button\u003e\n```\n\nInterestingly when you tap `enter` in a form, the first `button` in the DOM hierarchy will be the button that's used to submit the form; in the above case `enter` would run the validation. However if you were to reverse the order of the buttons in the DOM, the `enter` key will submit the form **without** the front-end validation.\n\n## JS Validation\n\nFor the most part the native HTML validation is sufficient for our forms, especially when you consider the power that the [`pattern` attribute](https://www.w3schools.com/tags/att_input_pattern.asp) provides with regular expression based validation. Nevertheless there will **always** be edge-cases where HTML validation doesn't quite cut the mustard. In those cases `Formv` provides the `Error.Validation` and `Error.Generic` exceptions that you can raise during the `onSubmitting` phase.\n\n```jsx\nimport { Form, Error } from 'formv';\nimport * as utils from './utils';\n\nconst handleSubmitting = () =\u003e {\n\n    if (!utils.passesQuirkyValidation(state)) {\n        throw new Error.Validation({\n            name: 'Does not pass our quirky validation rules.'\n        });\n    }\n\n});\n\n\u003cForm onSubmitting={handleSubmitting} /\u003e\n```\n\nIt's worth noting that any errors that are thrown from the `onSubmitting` handler will be merged with the native HTML validation messages.\n\n## API Validation\n\nIt's all good and well having the front-end validation for your forms, however there are always cases where the front-end validation passes just fine, whereas the back-end throws a validation error \u0026ndash; maybe the username is already taken, for instance. In those cases we need to feed the API validation messages back into the `Form` component by using the `Error.Validation` exception that Formv exports.\n\nThe validation messages need to be flattened and should map to your field names \u0026ndash; for cases where you have an array of fields, we recommend you name these `names.0.firstName`, `names.1.firstName`, etc... Note that we have a flattening helper for Django Rest Framework (DRF) under `parse.django.flatten`.\n\nContinuing from the above example, we'll implement the `handleSubmitted` function which handles the submitting of the data to the API.\n\n```jsx\nimport { Form, Error } from 'formv';\n\nasync function handleSubmitted() {\n    try {\n        await api.post('/send', data);\n    } catch (error) {\n        const isBadRequest = error.response.status === 400;\n        if (isBadRequest) throw new Error.Validation(error.response.data);\n        throw error;\n    }\n}\n\n\u003cForm onSubmitted={handleSubmitted} /\u003e;\n```\n\nIn the example above we're capturing all API errors \u0026ndash; we then check if the status code is a `400` which indicates a validation error in our application, and then feeds the validation errors back into `Formv`. The param passed to the `Error.Validation` should be a map of errors that correspond to the `name` attributes in your fields \u0026ndash; we will then show the messages next to the relevant fields \u0026ndash; for instance the `error.response.data` may be the following from the back-end if we were to hard-code it on the front-end.\n\n```javascript\nthrow new Error.Validation({\n    name: 'Please enter your first and last name.',\n    age: 'You must be 18 or over to use this form.',\n});\n```\n\nHowever there may be another error code that indicates a more generic error, such as that we weren't able to validate the user at this present moment \u0026ndash; perhaps there's an error in our back-end code somewhere. In those cases you can instead raise a `Error.Generic` to provide helpful feedback to the user.\n\n```jsx\nimport { Form, Error } from 'formv';\n\nasync function handleSubmitted() {\n    try {\n        await api.post('/send', data);\n    } catch (error) {\n        const isBadRequest = error.response.status === 400;\n        const isAxiosError = error.isAxiosError;\n\n        if (isBadRequest) throw new Error.Validation(error.response.data);\n        if (isAxiosError) throw new Error.Generic(error.response.data);\n        throw error;\n    }\n}\n\n\u003cForm onSubmitted={handleSubmitted} /\u003e;\n```\n\nUsing the above example we throw `Error.Validation` errors when the request yields a `400` error message, we raise a `Error.Generic` error when the error is Axios specific. Any other errors are re-thrown for capturing elsewhere, as they're likely to indicate non-request specific errors such as syntax errors and non-defined variables.\n\n## Success Messages\n\nWith all the talk of validation errors and generic errors, it may have slipped your mind that sometimes forms submit successfully! In your `onSubmitted` callback all you need to do is instantiate `Success` with the content set to some kind of success message.\n\n```jsx\nimport { Form, Success, Error } from 'formv';\n\nasync function handleSubmitted() {\n    try {\n        await api.post('/send', data);\n        return new Success('Everything went swimmingly!');\n    } catch (error) {\n        const isBadRequest = error.response.status === 400;\n        const isAxiosError = error.isAxiosError;\n\n        if (isBadRequest) throw new Error.Validation(error.response.data);\n        if (isAxiosError) throw new Error.Generic(error.response.data);\n        throw error;\n    }\n}\n\n\u003cForm onSubmitted={handleSubmitted} /\u003e;\n```\n\n## Managing State\n\nManaging the state for your forms is not typically an arduous task, nevertheless there are techniques that can make everything just a little bit easier, which is why `Formv` exports a `useMap` hook that has the same interface as [`react-use`'s `useMap` hook](https://github.com/streamich/react-use/blob/master/docs/useMap.md) with a handful of differences \u0026ndash; currying, memoization and nested properties.\n\n```javascript\nimport { useMap } from 'formv';\n\nconst [state, { set }] = useMap({\n    username: null,\n    profile: {\n        age: null,\n        created: null,\n    },\n});\n```\n\nUsing the `set` function provided by `useMap` you can use a curried function to pass to your `Input` component. Interestingly if you use the approach below rather than creating a new function every time, the `set('username')` will never change, and as such makes everything a whole lot easier when it comes to wrapping your `Input` field in [`memo`](https://reactjs.org/docs/react-api.html#reactmemo).\n\n```jsx\n\u003cInput value={state.username} onChange={set('username')} /\u003e\n\u003cInput value={state.profile.age} onChange={set('profile.age')} /\u003e\n\u003cInput value={state.profile.created} onChange={set('profile.created')} /\u003e\n```\n\nIn contrast, if you were to use the non-curried version \u0026ndash; which works perfectly fine and is illustrated below \u0026ndash; each time the component is rendered you'd be creating a new function which would cause the component to re-render even when it didn't need to. In an ideal scenario the component would only re-render when its value was modified.\n\n```jsx\n\u003cInput value={state.username} onChange={({ target }) =\u003e set('username', target.value)} /\u003e\n```\n\nYou'll also notice that nested objects are handled with the dot notation thanks to [`Lodash`](https://lodash.com/).\n\n## Form Architecture\n\nWhen deciding on an architecture for your forms, it's recommended to think about them as three separate layers. The first and most simple layer is the field which handles logic pertaining to an individual input field; it can maintain its own state such as a country selector maintains its state for a list of countries, but it does **not** maintain state for its value. Secondly there is the fieldset layer which composes many field components and is again stateless. Last of all there is the parent form component which maintains state for the entire form.\n\nWith the above architecture it allows your field and fieldset components to be used freely in any form components without any quirky state management. The form field has the ultimate responsibility of maintaining and submitting the data.\n\nUsing `Formv` it's easy to have the aforementioned setup as illustrated below.\n\n```jsx\nimport * as fv from 'formv';\n\nfunction Form() {\n    const [state, { set }] = fv.useMap({\n        name: null,\n        age: null,\n    });\n\n    return (\n        \u003cfv.Form onSubmitted={handleSubmitted}\u003e\n            {({ feedback }) =\u003e (\n                \u003c\u003e\n                    \u003cfv.Messages value={feedback.success} /\u003e\n                    \u003cfv.Messages value={feedback.error} /\u003e\n\n                    \u003cFieldset onChange={set} /\u003e\n                \u003c/\u003e\n            )}\n        \u003c/fv.Form\u003e\n    );\n}\n```\n\n```jsx\nfunction Fieldset({ onChange }) {\n    return (\n        \u003c\u003e\n            \u003cFieldName onChange={onChange('name')} /\u003e\n            \u003cFieldAge onChange={onChange('age')} /\u003e\n        \u003c/\u003e\n    );\n}\n```\n\n```jsx\nimport * as fv from 'formv';\n\nfunction FieldName({ onChange }) {\n    const { feedback } = fv.useState();\n\n    return (\n        \u003c\u003e\n            \u003cinput name=\"name\" type=\"text\" onChange={({ target }) =\u003e onChange(target.value)} /\u003e\n            \u003cfv.Messages values={feedback.field.name} /\u003e\n        \u003c/\u003e\n    );\n}\n```\n\n```jsx\nimport * as fv from 'formv';\n\nfunction FieldAge({ onChange }) {\n    const { feedback } = fv.useState();\n\n    return (\n        \u003c\u003e\n            \u003cinput name=\"age\" type=\"number\" onChange={({ target }) =\u003e onChange(target.value)} /\u003e\n            \u003cfv.Messages values={feedback.field.age} /\u003e\n        \u003c/\u003e\n    );\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwildhoney%2Fformv","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwildhoney%2Fformv","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwildhoney%2Fformv/lists"}