{"id":19082703,"url":"https://github.com/alsiola/form-and-function","last_synced_at":"2025-04-30T08:24:19.554Z","repository":{"id":57240228,"uuid":"118293682","full_name":"alsiola/form-and-function","owner":"alsiola","description":"A functional-inspired Form management library for React","archived":false,"fork":false,"pushed_at":"2018-04-04T23:25:55.000Z","size":2217,"stargazers_count":66,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-12T09:17:39.966Z","etag":null,"topics":["form-validation","form-validation-react","forms","react"],"latest_commit_sha":null,"homepage":"https://alsiola.github.io/form-and-function/","language":"TypeScript","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/alsiola.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-01-21T00:59:20.000Z","updated_at":"2023-06-10T06:13:36.000Z","dependencies_parsed_at":"2022-08-29T22:21:31.915Z","dependency_job_id":null,"html_url":"https://github.com/alsiola/form-and-function","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alsiola%2Fform-and-function","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alsiola%2Fform-and-function/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alsiola%2Fform-and-function/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alsiola%2Fform-and-function/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alsiola","download_url":"https://codeload.github.com/alsiola/form-and-function/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251667364,"owners_count":21624479,"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-validation","form-validation-react","forms","react"],"created_at":"2024-11-09T02:44:08.267Z","updated_at":"2025-04-30T08:24:19.533Z","avatar_url":"https://github.com/alsiola.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# form-and-function\n\nform-and-function is a functional-inspired Form management library for React, written in TypeScript.\n\n## What problem is solved?\n\nManaging form state can be a pain, and the need for a form management abstraction is reasonably well\nestablished. redux-form is great at what it does, but depends upon you using a particular state-management\nsolution. I wanted to create a state-management agnostic library (using simple component state by default) that\nprovides similar convenience and ease of use.\n\nAnother issue is that of form validation. As a fan of functional programming, I wanted validators that were pure functions that could be composed at will. I think the approach in form-and-function provides a better developer experience than the use of, for example, JSON schema based validation.\n\nMany modern applications need to be internationalized, and this can be an issue in form libraries. I18n has been considered from the start in the design of the API, with the aim of seamless integration with existing solutions.\n\nIn modern JavaScript development bundle size is always a concern - currently form-and-function weights in at just 3.54KB gzipped (14.87KB uncompressed).\n\n## Contents\n\n*   [Examples](#examples)\n*   [Installation](#installation)\n*   [Usage](#usage)\n    *   [Form](#form)\n    *   [Field](#field)\n    *   [Validation](#validation)\n        *   [Built-In Validators](#built-in-validators)\n        *   [Custom Errors](#custom-validation-errors)\n        *   [Combining Validators](#combining-validators)\n        *   [Covalidating Fields](#covalidated-fields)\n        *   [Custom Validators](#writing-your-own-validators)\n    *   [Internationalization](#internationalization)\n*   [State Management](#state-management)\n\n## Examples\n\nThere are several examples in the `demo` folder of this repository. They can be run locally by cloning this repository, installing dependencies and running `yarn start`. If anything is unclear then raise an issue, or tweet me @bigalreturns\n\n## Installation\n\nAdd the package to your React app using yarn or npm:\n\n```\n    yarn add form-and-function\n```\n\nreact and react-dom are peer dependencies listed as version \u003e= 15.x.x as this is what I have tested. There are no direct dependencies. The current release version of form-and-function is 0.2.2\n\n## Usage\n\nform-and-function relies upon render props, if you are unfamiliar with this concept then there is a great overview [here](https://reactjs.org/docs/render-props.html)\n\nThere is no configuration, and no App level providers (no context was used in the making of this library!). Simply import the Form component and get to work.\n\nThe minimum usage of Form is as follows - notice that the Field component is injected as a Prop into the render method.\nThe `form` prop should be spread into the `form` element, and each fields `input` prop into their respective `input` elements.\n\n```js\nimport React from \"react\";\nimport { Form } from \"form-and-function\";\n\nexport const YourApp = () =\u003e (\n    \u003cForm\n        name=\"my-form\"\n        render={({ form, Field }) =\u003e (\n            \u003cform {...form}\u003e\n                \u003cField\n                    name=\"my-field\"\n                    render={({ input }) =\u003e \u003cinput type=\"text\" {...input} /\u003e}\n                /\u003e\n            \u003c/form\u003e\n        )}\n    /\u003e\n);\n```\n\nYou are probably going to want to factor out those render methods into functional components:\n\n```js\nimport React from \"react\";\nimport { Form } from \"form-and-function\";\n\nconst TextInput = ({ input }) =\u003e \u003cinput type=\"text\" {...input} /\u003e;\n\nconst RenderForm = ({ form, Field }) =\u003e (\n    \u003cform {...form}\u003e\n        \u003cField name=\"my-field\" render={TextInput} /\u003e\n    \u003c/form\u003e\n);\n\nexport const YourApp = () =\u003e \u003cForm name=\"my-form\" render={RenderForm} /\u003e;\n```\n\nThe chances are strong that your RenderForm component will also need some other props. We can provide those\nusing the `renderProps` prop on Form. These are then passed through to the render component within an `ownProps`\nobject. For example, we can give our form a title:\n\n```js\nimport React from \"react\";\nimport { Form } from \"form-and-function\";\n\nconst TextInput = ({ input }) =\u003e \u003cinput type=\"text\" {...input} /\u003e;\n\nconst RenderForm = ({ form, Field, ownProps: { title } }) =\u003e (\n    \u003cform {...form}\u003e\n        \u003ch2\u003e{title}\u003c/h2\u003e\n        \u003cField name=\"my-field\" render={TextInput} /\u003e\n    \u003c/form\u003e\n);\n\nexport const YourApp = () =\u003e (\n    \u003cForm\n        name=\"my-form\"\n        render={RenderForm}\n        renderProps={{\n            title: \"My smashing form!\"\n        }}\n    /\u003e\n);\n```\n\nThe same principle applies when passing additional props down to our field renderers - we can give our fields labels:\n\n```js\nimport React from \"react\";\nimport { Form } from \"form-and-function\";\n\nconst TextInput = ({ input, ownProps: { id, label } }) =\u003e (\n    \u003clabel htmlFor={id}\u003e\n        \u003cspan\u003e{label}\u003c/span\u003e\n        \u003cinput type=\"text\" id={id} {...input} /\u003e\n    \u003c/label\u003e\n);\n\nconst RenderForm = ({ form, Field, ownProps: { title } }) =\u003e (\n    \u003cform {...form}\u003e\n        \u003ch2\u003e{title}\u003c/h2\u003e\n        \u003cField\n            name=\"my-field\"\n            render={TextInput}\n            renderProps={{\n                label: \"Name\",\n                id: \"name\"\n            }}\n        /\u003e\n    \u003c/form\u003e\n);\n\nexport const YourApp = () =\u003e (\n    \u003cForm\n        name=\"my-form\"\n        render={RenderForm}\n        renderProps={{\n            title: \"My smashing form!\"\n        }}\n    /\u003e\n);\n```\n\n### Form\n\nThe `Form` component will accept the following props:\n\n*   name (string, required) - The name of the form.\n*   render (component/function, required) - Function/functional component that will render the form - passed InjectedFormProps as below\n*   renderProps (object, optional) - Custom props to pass to the render component\n*   validators (object, optional) - Form validation object - see validation\n*   initialValues (object, optional) - Initial form values in the form `{ [fieldName]: value }`\n*   onSubmit (function, optional) - Called on form submission with form values. Should return void or Promise\u003cvoid\u003e\n*   onSubmitFailed (function, optional) - Called when submission fails due to validation errors, with form values. Should return void or Promise\u003cvoid\u003e\n*   onChange (function, optional) - Called when any form value changes, with all form values\n\nThe render component you provide will receive the following props:\n\n*   Field (Component) - A component to create fields\n*   form (object) - Props that must be passed to a \u003cform\u003e element\n*   values (object) - Current form values\n*   meta (object)\n    *   valid (boolean) - Is validation currently passing\n    *   submitted (boolean) - Has the form been submitted at any time\n    *   errors: (object) - Current errors for the form, { [fieldName]: { error: string }}\n    *   isValidating (boolean) - Is validation currently ongoing\n    *   isSubmitting (boolean) - Is submission currently ongoing\n*   actions (object)\n    *   reset (function) - Call to reset the form to initial values and clear validation errors\n    *   submit: (function) - Call to submit the form\n*   ownProps (object) - Any additional props passed via `renderProps` above\n\n### Field\n\nThe `Field` component (as provided to the `Form` renderer), can be passed the following props:\n\n*   name (string, required) - The field name\n*   render (component/function, required) - Field renderer - passed InjectedFieldProps as below\n*   renderProps (object, optional) - Custom props to pass to the field renderer\n*   onChange (function, optional) - Called with the change event, and the field value, whenever the field value changes I.e. (e: SyntheticEvent, value: string | number | undefined) =\u003e void\n*   onFocus (function, optional) - Called with the focus event, and the field value, whenever the field value is focused\n*   onBlur (function, optional) - Called with the blur event, and the field value, whenever the field value is blurred\n\nThe render component passed to `Field` is provided with the following props. The input prop should generally be passed directly to the underlying \u003cinput\u003e element, i.e. \u003cinput {...input} /\u003e\n\n*   meta (object)\n    *   valid (boolean) - Does the field pass validation\n    *   error (string | undefined) - Current validation error\n    *   pristine (boolean) - True if the field has the same value as its initial value\n    *   touched (boolean) - Has the field has ever been focused\n    *   active (boolean) - Is the field currently focused\n    *   isValidating (boolean) - Is the field currently being validated\n*   input (object)\n    *   onChange (function) - Called with (event, value) when the field value changes\n    *   onFocus (function) - Called with (event, value) when the field is focused\n    *   onBlur (function) - Called with (event, value) when the field is blurred\n    *   value (string | number | undefined) - Current field value\n    *   name (string) - Name of the field\n*   ownProps - Any custom props passed to `Field`s `renderProps`\n\n### Validation\n\nValidation follows a single route - a validators function is passed to the `Form` component, which should return an object with keys for any field that requires validation. This function is called with two \"reporters\" - `valid` and `invalid` - which should be called by each individual field's validator depending on the validity of the field. `valid` takes\nno arguments, and `invalid` should be called with a string describing the validation error.\n\nA convenience function `validators.create` is provided, which will pass `valid` and `invalid` to each entry in\na provided object.\n\nIt's much easier to see it in some code, so if our form has fields named `firstName`, `telephone` and `password`,\nand we want to validate `firstName`, we would use:\n\n```js\nimport { Form, validation } from \"form-and-function\";\n\n\u003cForm\n    validators={validation.create({\n        firstName: ({ valid, invalid }) =\u003e value =\u003e\n            value.length \u003c 3 ? valid() : invalid(`Must be more than 3 chars.`)\n    })}\n/\u003e;\n```\n\n#### Built In Validators\n\nIf this looks long-winded then say hello to some built in validators! Currently we provide the following:\n\n*   `validation.required` - Is the value defined, with a length of \u003e 0 when converted to a string\n*   `validation.atLeast` - Is the value at least some length.\n*   `validation.atMost` - Is the value at most some length.\n*   `validation.numeric` - Is the value numeric only.\n*   `validation.equalTo` - Does the value match that of another field\n*   `validation.exactly` - Is the value exactly equal ( === ) to a specified value\n*   `validation.matches` - Does the value match a specified regular expression\n\nThey can be used as follows:\n\n#### required\n\n```js\n\u003cForm\n    validators={validation.create({\n        age: validation.required()\n    })}\n/\u003e\n```\n\n#### numeric\n\n```js\n\u003cForm\n    validators={validation.create({\n        age: validation.numeric()\n    })}\n/\u003e\n```\n\n#### atLeast/atMost\n\n```js\n\u003cForm\n    validators={validation.create({\n        firstName: validation.atLeast({ chars: 3 })\n    })}\n/\u003e\n```\n\n#### equalTo\n\nEnsure that `passwordConfirm` field is the same as `password` field.\n\n```js\n\u003cForm\n    validators={validation.create({\n        passwordConfirm: validation.equalTo({ field: \"password\" })\n    })}\n/\u003e\n```\n\n#### exactly\n\nIs the entered value exactly \"form-and-function\"\n\n```js\n\u003cForm\n    validators={validation.create({\n        repositoryName: validation.exactly({ value: \"form-and-function\" })\n    })}\n/\u003e\n```\n\n#### matches\n\nIs the entered value alphabetic only\n\n```js\n\u003cForm\n    validators={validation.create({\n        repositoryName: validation.matches({ regex: /^[a-zA-Z]+$/ })\n    })}\n/\u003e\n```\n\n#### Custom Validation Errors\n\nIf customised error messages are needed, then an object of error messages can be passed to the validator. If the validator takes parameters (e.g. atLeast ), then this is the second argument. If the validation takes no parameters (e.g. required ) then it is the only argument.\nA function can also be passed for each message, which will be called with the an object - the union of the inputted\nvalue and the validator params, e.g. for `validation.atLeast`:\n\n```js\n{\n    value: \"whatever was entered\",\n    chars: 3\n}\n```\n\n```js\n\u003cForm\n    validators={validation.create({\n        firstName: validation.atLeast(\n            { chars: 3 },\n            {\n                short: ({ chars, value }) =\u003e\n                    `Please enter ${chars} characters minimum, you entered ${\n                        value.length\n                    }`,\n                undef: \"You must enter a message\"\n            }\n        ),\n        age: validation.numeric({\n            nonNumeric: ({ value }) =\u003e\n                `Please enter a number - you entered ${value}`\n        })\n    })}\n/\u003e\n```\n\n#### Combining Validators\n\nThe inbuilt validators can be combined to validate on multiple conditions. `validation.all` ensures that all of an array of validators pass, `validation.any` ensures that at least one of a collection of validators pass.\n\nAs an example, we might want to check if a field is numeric AND more than 5 characters.\n\n```js\n\u003cForm\n    validators={validation.create({\n        longNumber: validation.all([\n            validation.atLeast(\n                { chars: 3 },\n                {\n                    short: ({ chars }) =\u003e\n                        `Please enter ${chars} characters minimum`,\n                    undef: \"You must enter a message\"\n                }\n            ),\n            validation.numeric({\n                nonNumeric: \"Please enter a number\"\n            })\n        ])\n    })}\n/\u003e\n```\n\nBy default, error messages produced in `validation.all` are joined with `\" and \"` - this would produce an error such as:\n\n`Please enter 3 characters minimum and Please enter a number`\n\nNot quite perfect. We can pass a second argument to `validation.all`, which is an error combining function. It is passed\nan array of strings, and should return a string.\n\n```js\n\u003cForm\n    validators={validation.create({\n        longNumber: validation.all([\n            validation.atLeast({ chars: 3 }, {\n                short: ({ chars }) =\u003e `${chars} characters minimum`,\n                undef: \"provided\"\n            }),\n            validation.numeric({\n                nonNumeric: \"numbers only\"\n            })\n        ], errors =\u003e \"Please enter \" + errors.join(\", and also \")\n    })}\n/\u003e\n```\n\nNow we will get a much nicer looking message:\n\n`Please enter 3 characters minimum, and also numbers only`\n\n#### Covalidated fields\n\nIt is not infrequent that the validation of a field depends upon the value of another field. For example, we might have a `password` field that requires its value be at least 8 characters long, and a `passwordConfirm` field that must match the `password` field. We could write these requirements like this:\n\n```js\n\u003cForm\n    validators={validation.create({\n        password: validation.atLeast({ chars: 8 }),\n        passwordConfirm: validation.equalTo({ field: \"password\" })\n    })}\n/\u003e\n```\n\nThis works, but if you try it out you will notice that the validation of `passwordConfirm` does not change when we are typing into the `password` field. Why is this? It's because by default, when a field changes, only the validator for that field's value is run. In our case when we change `password`, form-and-function will validate `password` only, not `passwordConfirm`.\n\nOf course, we want to give feedback to our users on both fields. We can do this by using a covalidator to specify additional validators that should be run when a field's value changes. We want to run the `passwordConfirm` validator whenever `password` changes, so we create a covalidator on `password` as follows:\n\n```js\n\u003cForm\n    validators={validation.create({\n        password: validation.covalidate(\n            { fields: [\"passwordConfirm\"] },\n            validation.atLeast({ chars: 8 })\n        ),\n        passwordConfirm: validation.matches({ field: \"password\" })\n    })}\n/\u003e\n```\n\n#### Writing Your Own Validators\n\nAlthough the provided validators cover a lot of situations, undoubtedly at some point you will need to create your own. This is relatively simple. Validators must match the signature:\n\n```js\ninterface ValidResult {\n    valid: true;\n}\n\ninterface InvalidResult {\n    valid: false;\n    error: string;\n}\n\n({ valid, invalid }) =\u003e value =\u003e ValidResult | InvalidResult | Promise\u003cValidResult | InvalidResult\u003e\n```\n\n`valid` and `invalid` are functions that will generate appropriate results - if the value is valid then return `valid()`, if the value is invalid return `invalid(\"reason for invalidity\")`. Asynchronous functions are fine, just return a Promise that will resolve to a validation result.\n\nIt's much easier to look at some example code, so let's make a validator that verifies that the provided value is an odd number of characters:\n\n```js\nconst isOddLength = ({ valid, invalid }) =\u003e value =\u003e {\n    // It's possible that value is undefined, so checking up front is a good idea\n    if (typeof value === \"undefined\") {\n        return invalid(\"Please enter a value\");\n    }\n\n    if (value.length % 2 === 1) {\n        return valid();\n    } else {\n        return invalid(\"Entered value must be an odd number of characters\");\n    }\n};\n```\n\nWe can now use the `isOddLength` validator like any other validator:\n\n```js\n\u003cForm\n    validators={validation.create({\n        oddField: isOddLength\n    })}\n/\u003e\n```\n\n### Internationalization\n\nIn the most part, `form-and-function` does not produce outputted strings but simply manages the form data, so i18n\nis handled by your own code. The exception to this is in the generation of validation errors, which may need to\nbe translated. The validators above are all usable with an i18n library, such as react-intl, with very little effort.\n\nThese examples assume that your application has been set up with react-intl, and you are somewhat familiar with its concepts.\n\nThe `validation.create` function above has an optional second argument - an `options` object, which includes a formatter. This formatter has the signature:\n\n`type Formatter\u003cT\u003e = (x: T, params?: Record\u003cstring, any\u003e) =\u003e string;`\n\nConveniently, this is the same signature as `react-intl`'s `formatMessage` function, which we can get in our component by using react-intl's `injectIntl` higher-order function. Using the custom messages option described previously, we can now return a react-intl `MessageDescriptor` instead of a string, and this will be passed to our `formatMessages` formatter, with\nthe arguments passed to the validator as the second argument, with the entered value under the `value` key, i.e.\n\n```js\n{\n    chars: 3,\n    value: \"whatever was entered\"\n}\n```\n\nAn internationalized form might use validators like this:\n\n```js\n// Wherever our messages are defined\nconst messages = {\n    short: {\n        id: \"form.validation.short\",\n        defaultMessage: \"At least {chars} characters\"\n    },\n    undef: {\n        id: \"form.validation.undef\",\n        defaultMessage: \"Must be provided\"\n    },\n    nonNumeric: {\n        id: \"form.validation.nonNumeric\",\n        defaultMessage: \"Numbers only, you entered {value}\"\n    }\n};\n\n// In our component\n\u003cForm\n    validators={validation.create(\n        {\n            firstName: validation.atLeast(\n                { chars: 3 },\n                {\n                    short: messages.short,\n                    undef: messages.undef\n                }\n            ),\n            age: validation.numeric({\n                nonNumeric: messages.nonNumeric\n            })\n        },\n        {\n            formatter: this.props.intl.formatMessage\n        }\n    )}\n/\u003e;\n```\n\nIf, as above, we name our messages using the same keys as the form validation messages, then we can just pass in the whole messages object and save some typing:\n\n```js\n\u003cForm\n    validators={validation.create(\n        {\n            firstName: validation.atLeast({ chars: 3 }, messages),\n            age: validation.numeric(messages)\n        },\n        {\n            formatter: this.props.intl.formatMessage\n        }\n    )}\n/\u003e\n```\n\n## State Management\n\nOne of the design goals was to create a library that was agnostic to the variety of state management used. I'm not certain that this is an easy target, or one that is achieved entirely, but the current attempt uses a prop on the `Form` component: `stateEngine`. This is an optional prop, and if not passed then `form-and-function` will create its own state engine that uses the internal state of the `Form` component. If supplied, this prop should be an object that fulfills the following interface:\n\n```js\nexport interface StateEngine\u003cT extends object\u003e {\n    select: \u003cU\u003e(selector: (state: T) =\u003e U) =\u003e U;\n    set: \u003cK extends keyof T\u003e(\n        update: Pick\u003cT, K\u003e | ((s: T) =\u003e Pick\u003cT, K\u003e)\n    ) =\u003e Promise\u003cvoid\u003e;\n    get: () =\u003e T;\n}\n```\n\nEssentially, there should be a `select` function that runs the provided `selector` against the whole state, a `set` function that updates the state (with the same signature as React's `setState`, but returning a promise instead of using a callback), and a `get` function that returns the entire state.\n\nThe internal component-state stateEngine that is used by default looks like this - this is a function gets passed the componentInstance and the initialState, and returns the stateEngine. Currently, if you pass a `stateEngine` prop then it doesn't get provided with these arguments - but this part of the API is not nailed down.\n\n```js\nexport const componentStateEngine = (\n    componentInstance: Component\u003cany, FormState\u003e,\n    initialState: FormState\n): FormStateEngine =\u003e {\n    componentInstance.state = initialState;\n    return {\n        select: selector =\u003e selector(componentInstance.state),\n        set: update =\u003e\n            new Promise(r =\u003e componentInstance.setState(update as any, r)),\n        get: () =\u003e componentInstance.state\n    };\n};\n```\n\nAll state updates within `form-and-function` pass through this stateEngine, so providing an alternate will allow some external code to take over managing the entirety of the form's state. In the future the library may provided alternative stateEngines for you to use, such as a redux-connected one (if you believe form state should live in redux), or one using Apollo Client's local cache. For now, you're on your own.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falsiola%2Fform-and-function","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falsiola%2Fform-and-function","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falsiola%2Fform-and-function/lists"}