├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── example
├── Control.tsx
├── app.tsx
├── fields.ts
├── index.html
└── validations.ts
├── package-lock.json
├── package.json
├── src
└── react-jeff.ts
├── tsconfig.json
└── types
└── email-regex.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .rts2*
4 | dist
5 | example-dist
6 | .cache
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "useTabs": true,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Jeff
2 |
3 | > A Good Form Library
4 |
5 | - ~800 bytes minified+gzip
6 | - Easy to learn API
7 | - Write your form code in a way that is reusable and testable
8 | - Seamless sync and async form validations (including on form submit)
9 | - Tons of utilty features out of the box
10 | - Written with React Hooks
11 | - Typed with TypeScript
12 |
13 | ## Install
14 |
15 | ```
16 | npm install react-jeff
17 | ```
18 |
19 | ## Usage
20 |
21 |
22 | ```js
23 | import React from "react"
24 | import { useField, useForm } from "react-jeff"
25 |
26 | /**
27 | * 1. Write some validations that accept an input value and return an array of errors.
28 | * (If the array is empty, the value is considered valid)
29 | */
30 |
31 | function validateUsername(value) {
32 | let errs = []
33 | if (value.length < 3) errs.push("Must be at least 3 characters long")
34 | if (!/^[a-z0-9_-]*$/i.test(value)) errs.push("Must only contain alphanumeric characters or dashes/underscores")
35 | if (!/^[a-z0-9]/i.test(value)) errs.push("Must start with alphanumeric character")
36 | if (!/[a-z0-9]$/i.test(value)) errs.push("Must end with alphanumeric character")
37 | return errs
38 | }
39 |
40 | function validatePassword(value) {
41 | let errs = []
42 | if (value.length < 6) errs.push("Must be at least 6 characters long")
43 | if (!/[a-z]/.test(value)) errs.push("Must contain at least one lowercase letter")
44 | if (!/[A-Z]/.test(value)) errs.push("Must contain at least one uppercase letter")
45 | if (!/[0-9]/.test(value)) errs.push("Must contain at least one number")
46 | return errs
47 | }
48 |
49 | /**
50 | * 2. Write some custom components that you'll reuse for all of your forms.
51 | */
52 |
53 | function Form({ onSubmit, ...props }) {
54 | return (
55 |
106 | )
107 | }
108 | ```
109 |
110 | ## API
111 |
112 | ### `useField()`
113 |
114 | Call `useField()` to create a single field in a form.
115 |
116 | ```js
117 | let field = useField({
118 | defaultValue: (value), // ....... (Required) The default value of the field.
119 | validations: [(...errors)], // .. (Optional) Validations to run when the field is `validate()`'d.
120 | required: boolean, // ........... (Optional) Should the field be required?
121 | disabled: boolean, // ........... (Optional) Should the field be disabled?
122 | readOnly: boolean, // ........... (Optional) Should the field be readOnly?
123 | })
124 | ```
125 |
126 | ```js
127 | field == {
128 | value: (value), // ......... The current value of the field.
129 | defaultValue: (value), // .. The `defaultValue` passed into `useField({ defaultValue })`.
130 |
131 | dirty: boolean, // ......... Has the field been changed from its defaultValue?
132 | touched: boolean, // ....... Has the element this field is attached to been focused previously?
133 | focused: boolean, // ....... Is the element this field is attached to currently focused?
134 | blurred: boolean, // ....... Has the element this field is attached to been focused and then blurred?
135 |
136 | validating: boolean, // .... Is the field validating itself?
137 | valid: boolean, // ......... Is the field currently valid? (must have no errors, and if the field is required, must not be empty)
138 | errors: [(...errors)], // .. The collected errors returned by `opts.validations`
139 |
140 | required: boolean, // ...... Is the field required?
141 | disabled: boolean, // ...... Is the field disabled?
142 | readOnly: boolean, // ...... Is the field readOnly?
143 |
144 | setValue: Function, // ..... Call with a value to manually update the value of the field.
145 | setRequired: Function, // .. Call with true/false to manually set the `required` state of the field.
146 | setDisabled: Function, // .. Call with true/false to manually set the `disabled` state of the field.
147 | setReadOnly: Function, // .. Call with true/false to manually set the `readOnly` state of the field.
148 |
149 | reset: Function, // ........ Reset the field to its default state.
150 | validate: Function, // ..... Manually tell the field to validate itself (updating other fields).
151 |
152 | // Props to pass into an component to attach the `field` to it:
153 | props: {
154 | value: (value), // ....... The current value (matches `field.value`).
155 |
156 | onChange: Function, // ... An `onChange` handler to update the value of the field.
157 | onFocus: Function, // .... An `onFocus` handler to update the focused/touched/blurred states of the field.
158 | onBlur: Function, // ..... An `onFocus` handler to update the focused/blurred states of the field.
159 |
160 | required: boolean, // .... Should the element be `required`?
161 | disabled: boolean, // .... Should the element be `disabled`?
162 | readOnly: boolean, // .... Should the element be `readOnly`?
163 | },
164 | }
165 | ```
166 |
167 | ### `useForm()`
168 |
169 | Call `useForm()` with to create a single form.
170 |
171 | ```js
172 | let form = useForm({
173 | fields: [(...fields)], // .. All of the fields created via `useField()` that are part of the form.
174 | onSubmit: Function, // ..... A submit handler for the form that receives the form submit event.
175 | })
176 | ```
177 |
178 | ```js
179 | form == {
180 | fieldErrors: [(...errors)], // ... The collected errors from all of the fields in the form.
181 | submitErrors: [(...errors)], // .. The errors returned by `opts.onSubmit`.
182 |
183 | submitted: boolean, // ........... Has the form been submitted at any point?
184 | submitting: boolean, // .......... Is the form currently submitting?
185 |
186 | focused: boolean, // ............. Are *any* of the fields in the form currently `focused`?
187 | touched: boolean, // ............. Are *any* of the fields in the form currently `touched`?
188 | dirty: boolean, // ............... Are *any* of the fields in the form currently `dirty`?
189 | valid: boolean, // ............... Are *all* of the fields in the form currently `valid`?
190 | validating: boolean, // .......... Are *any* of the fields in the form currently `validating`?
191 |
192 | reset: Function, // .............. Reset all of the fields in the form.
193 | validate: Function, // ........... Validate all of the fields in the form.
194 | submit: Function, // ............. Submit the form manually.
195 |
196 | // Props to pass into a form component to attach the `form` to it:
197 | props: {
198 | onSubmit: Function, // ......... An onSubmit handler to pass to an element that submits the form.
199 | },
200 | }
201 | ```
202 |
203 |
228 |
--------------------------------------------------------------------------------
/example/Control.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Form, Field } from "../src/react-jeff"
3 | import format from "pretty-format"
4 |
5 | export interface ControlProps {
6 | children: React.ReactNode
7 | form: Form
8 | field: Field
9 | }
10 |
11 | export function Control(props: ControlProps) {
12 | return (
13 |
14 | {props.children}
15 | {(props.field.dirty || props.form.submitted) && (
16 | <>
17 | {props.field.validating && (
18 |
Validating...
19 | )}
20 | {props.field.errors && (
21 |
22 | {props.field.errors.map(err => {
23 | return - {err}
24 | })}
25 |
26 | )}
27 | >
28 | )}
29 |
30 | {"field = "}
31 | {format(props.field, {
32 | printFunctionName: false,
33 | })}
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/example/app.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { render } from "react-dom"
3 | import { useForm } from "../src/react-jeff"
4 | import * as fields from "./fields"
5 | import { Control } from "./Control"
6 | import format from "pretty-format"
7 |
8 | function Form({ onSubmit, ...props }) {
9 | return (
10 |
105 | )
106 | }
107 |
108 | render(, document.getElementById("root"))
109 |
--------------------------------------------------------------------------------
/example/fields.ts:
--------------------------------------------------------------------------------
1 | import { useField, useForm } from "../src/react-jeff"
2 | import * as validations from "./validations"
3 |
4 | export function useEmailField() {
5 | return useField({
6 | defaultValue: "",
7 | validations: [validations.validateEmail, validations.validateEmailUnused],
8 | })
9 | }
10 |
11 | export function useUsernameField() {
12 | return useField({
13 | defaultValue: "",
14 | validations: [
15 | validations.validateUsername,
16 | validations.validateUsernameUnused,
17 | ],
18 | })
19 | }
20 |
21 | export function usePasswordField() {
22 | return useField({
23 | defaultValue: "",
24 | validations: [validations.validatePassword],
25 | })
26 | }
27 |
28 | export function useConfirmPasswordField(password: string) {
29 | return useField({
30 | defaultValue: "",
31 | validations: [
32 | value => validations.validateConfirmPassword(value, password),
33 | ],
34 | })
35 | }
36 |
37 | export function useAcceptTermsField() {
38 | return useField({
39 | defaultValue: false,
40 | validations: [validations.validateAcceptTerms],
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Jeff Example
8 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/example/validations.ts:
--------------------------------------------------------------------------------
1 | import emailRegex from "email-regex"
2 |
3 | let EMAIL_REGEX = emailRegex({ exact: true })
4 |
5 | export function validateEmail(value: string) {
6 | let errors = []
7 |
8 | if (!EMAIL_REGEX.test(value)) {
9 | errors.push("Must be valid email")
10 | }
11 |
12 | return errors
13 | }
14 |
15 | export function validateEmailUnused(value: string) {
16 | return fakeAsyncApiRequest().then(() => {
17 | let errors = []
18 | if (value === "me@thejameskyle.com") {
19 | errors.push(`There's already an account signed up with that email`)
20 | }
21 | return errors
22 | })
23 | }
24 |
25 | export function validateUsername(value: string) {
26 | let errors = []
27 |
28 | if (value.length < 3) {
29 | errors.push("Must be at least 3 characters long")
30 | }
31 |
32 | if (!/^[a-z0-9_-]*$/i.test(value)) {
33 | errors.push(
34 | "Must only contain alphanumeric characters or dashes/underscores",
35 | )
36 | }
37 |
38 | if (!/^[a-z0-9]/i.test(value)) {
39 | errors.push("Must start with alphanumeric character")
40 | }
41 |
42 | if (!/[a-z0-9]$/i.test(value)) {
43 | errors.push("Must end with alphanumeric character")
44 | }
45 |
46 | return errors
47 | }
48 |
49 | export function validateUsernameUnused(value: string) {
50 | return fakeAsyncApiRequest().then(() => {
51 | let errors = []
52 | if (value === "jamiebuilds") {
53 | errors.push(`That username is already taken`)
54 | }
55 | return errors
56 | })
57 | }
58 |
59 | export function validatePassword(value: string) {
60 | let errors = []
61 |
62 | if (value.length < 6) {
63 | errors.push("Must be at least 6 characters long")
64 | }
65 |
66 | if (!/[a-z]/.test(value)) {
67 | errors.push("Must contain at least one lowercase letter")
68 | }
69 |
70 | if (!/[A-Z]/.test(value)) {
71 | errors.push("Must contain at least one uppercase letter")
72 | }
73 |
74 | if (!/[0-9]/.test(value)) {
75 | errors.push("Must contain at least one number")
76 | }
77 |
78 | return errors
79 | }
80 |
81 | export function validateConfirmPassword(value: string, password: string) {
82 | let errors = []
83 |
84 | if (value !== password) {
85 | errors.push("Must match password")
86 | }
87 |
88 | return errors
89 | }
90 |
91 | export function validateAcceptTerms(value: boolean) {
92 | let errors = []
93 |
94 | if (value !== true) {
95 | errors.push("Must agree to terms and conditions")
96 | }
97 |
98 | return errors
99 | }
100 |
101 | function fakeAsyncApiRequest() {
102 | return new Promise(res => setTimeout(res, 1000))
103 | }
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-jeff",
3 | "version": "1.2.1",
4 | "description": "A Good Form Library",
5 | "main": "dist/react-jeff.js",
6 | "types": "dist/react-jeff.d.ts",
7 | "source": "src/react-jeff.ts",
8 | "amdName": "ReactJeff",
9 | "files": [
10 | "dist"
11 | ],
12 | "scripts": {
13 | "format": "prettier --write '**/*.{ts,json,html,md}'",
14 | "build": "rm -rf dist && tsc --noEmit && microbundle --no-compress",
15 | "prepublish": "npm run build",
16 | "test": "ava",
17 | "example:build": "parcel build example/index.html --out-dir example-dist",
18 | "start": "parcel example/index.html --out-dir example-dist"
19 | },
20 | "keywords": [
21 | "react",
22 | "form",
23 | "forms",
24 | "hooks",
25 | "hook",
26 | "final-form",
27 | "final",
28 | "formik",
29 | "library",
30 | "simple",
31 | "easy",
32 | "plain",
33 | "async",
34 | "validation",
35 | "validate",
36 | "valid",
37 | "dirty",
38 | "focused",
39 | "touch",
40 | "touched"
41 | ],
42 | "author": "Jamie Kyle ",
43 | "license": "MIT",
44 | "peerDependencies": {
45 | "react": "^16.8.4"
46 | },
47 | "devDependencies": {
48 | "@types/pretty-format": "^20.0.1",
49 | "@types/react": "^16.8.8",
50 | "email-regex": "^3.0.0",
51 | "husky": "^1.3.1",
52 | "lint-staged": "^8.1.5",
53 | "microbundle": "^0.11.0",
54 | "parcel": "^1.12.1",
55 | "prettier": "^1.16.4",
56 | "pretty-format": "^24.7.0",
57 | "react": "^16.0.0",
58 | "react-dom": "^16.8.4",
59 | "typescript": "^3.3.3333"
60 | },
61 | "husky": {
62 | "hooks": {
63 | "pre-commit": "lint-staged"
64 | }
65 | },
66 | "lint-staged": {
67 | "*.{ts,json,md}": [
68 | "prettier --write",
69 | "git add"
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/react-jeff.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | /**
4 | * A close enough check to test if a value is a Synthetic Event or not.
5 | * See: https://github.com/reactjs/rfcs/pull/112
6 | * @private
7 | */
8 | function isReactSyntheticEvent(value: any) {
9 | if (typeof value !== "object" || value === null) return false
10 | if (typeof value.bubbles !== "boolean") return false
11 | if (typeof value.cancellable !== "boolean") return false
12 | if (typeof value.defaultPrevented !== "boolean") return false
13 | if (typeof value.eventPhase !== "number") return false
14 | if (typeof value.isTrusted !== "boolean") return false
15 | if (typeof value.timestamp !== "number") return false
16 | if (typeof value.type !== "string") return false
17 | return true
18 | }
19 |
20 | function isEmptyValue(value: any) {
21 | if (value === "") return true
22 | if (value === null) return true
23 | if (typeof value === "undefined") return true
24 | return false
25 | }
26 |
27 | /**
28 | * A function that accepts a value and returns an array of errors. If the array
29 | * is empty, the value is assumed to be valid. The validation can also return
30 | * asynchronously using a promise.
31 | *
32 | * Validation errors default to being strings, but can have any shape.
33 | */
34 | export interface Validation {
35 | (value: Val): Err[] | Promise
36 | }
37 |
38 | /**
39 | * Options to pass into `useField(opts)`
40 | */
41 | export interface FieldOptions {
42 | /**
43 | * The default value of the field.
44 | */
45 | defaultValue: Val
46 | /**
47 | * Validations to run when the field is `validate()`'d.
48 | */
49 | validations?: Array>
50 | /**
51 | * Should the field be required?
52 | */
53 | required?: boolean
54 | /**
55 | * Should the field be disabled?
56 | */
57 | disabled?: boolean
58 | /**
59 | * Should the field be readOnly?
60 | */
61 | readOnly?: boolean
62 | }
63 |
64 | /**
65 | * Options to pass into `useForm(opts)`
66 | */
67 | export interface FormOptions {
68 | /**
69 | * All of the fields created via `useField()` that are part of the form.
70 | */
71 | fields: Array>
72 | /**
73 | * A submit handler for the form that receives the form submit event and can
74 | * return additional validation errors. May also return asynchronously using
75 | * a promise.
76 | */
77 | onSubmit: () => void | Err[] | Promise | Promise
78 | }
79 |
80 | /**
81 | * The object returned by `useField()`
82 | */
83 | export interface Field {
84 | /**
85 | * The current value of the field.
86 | */
87 | value: Val
88 | /**
89 | * The `defaultValue` passed into `useField({ defaultValue })`.
90 | */
91 | defaultValue: Val
92 |
93 | /**
94 | * Has the field been changed from its defaultValue?
95 | */
96 | dirty: boolean
97 | /**
98 | * Has the element this field is attached to been focused previously?
99 | */
100 | touched: boolean
101 | /**
102 | * Is the element this field is attached to currently focused?
103 | */
104 | focused: boolean
105 | /**
106 | * Has the element this field is attached to been focused and then blurred?
107 | */
108 | blurred: boolean
109 |
110 | /**
111 | * Is the field validating itself?
112 | */
113 | validating: boolean
114 | /**
115 | * Is the field currently valid?
116 | *
117 | * Must have no errors, and if the field is required, must not be empty.
118 | */
119 | valid: boolean
120 | /**
121 | * The collected errors returned by `opts.validations`
122 | */
123 | errors: Err[]
124 |
125 | /**
126 | * Is the field required?
127 | */
128 | required: boolean
129 | /**
130 | * Is the field disabled?
131 | */
132 | disabled: boolean
133 | /**
134 | * Is the field readOnly?
135 | */
136 | readOnly: boolean
137 |
138 | /**
139 | * Call with a value to manually update the value of the field.
140 | */
141 | setValue: (value: Val) => any
142 | /**
143 | * Call with true/false to manually set the `required` state of the field.
144 | */
145 | setRequired: (required: boolean) => void
146 | /**
147 | * Call with true/false to manually set the `disabled` state of the field.
148 | */
149 | setDisabled: (disabled: boolean) => void
150 | /**
151 | * Call with true/false to manually set the `readOnly` state of the field.
152 | */
153 | setReadOnly: (readonly: boolean) => void
154 |
155 | /**
156 | * Reset the field to its default state.
157 | */
158 | reset: () => void
159 | /**
160 | * Manually tell the field to validate itself (updating other fields).
161 | */
162 | validate: () => Promise
163 |
164 | /**
165 | * Props to pass into an component to attach the `field` to it
166 | */
167 | props: {
168 | /**
169 | * The current value (matches `field.value`).
170 | */
171 | value: Val
172 |
173 | /**
174 | * An `onChange` handler to update the value of the field.
175 | */
176 | onChange: (value: Val) => void
177 | /**
178 | * An `onFocus` handler to update the focused/touched/blurred states of the field.
179 | */
180 | onFocus: () => void
181 | /**
182 | * An `onFocus` handler to update the focused/blurred states of the field.
183 | */
184 | onBlur: () => void
185 |
186 | /**
187 | * Should the element be `required`?
188 | */
189 | required: boolean
190 | /**
191 | * Should the element be `disabled`?
192 | */
193 | disabled: boolean
194 | /**
195 | * Should the element be `readOnly`?
196 | */
197 | readOnly: boolean
198 | }
199 | }
200 |
201 | /**
202 | * The object returned by `useForm()`
203 | */
204 | export interface Form {
205 | /**
206 | * The collected errors from all of the fields in the form.
207 | */
208 | fieldErrors: Array
209 | /**
210 | * The errors returned by `opts.onSubmit`.
211 | */
212 | submitErrors: Array
213 | /**
214 | * Has the form been submitted at any point?
215 | */
216 | submitted: boolean
217 | /**
218 | * Is the form currently submitting?
219 | */
220 | submitting: boolean
221 | /**
222 | * Are *any* of the fields in the form currently `focused`?
223 | */
224 | focused: boolean
225 | /**
226 | * Are *any* of the fields in the form currently `touched`?
227 | */
228 | touched: boolean
229 | /**
230 | * Are *any* of the fields in the form currently `dirty`?
231 | */
232 | dirty: boolean
233 | /**
234 | * Are *all* of the fields in the form currently `valid`?
235 | */
236 | valid: boolean
237 | /**
238 | * Are *any* of the fields in the form currently `validating`?
239 | */
240 | validating: boolean
241 | /**
242 | * Reset all of the fields in the form.
243 | */
244 | reset: () => void
245 | /**
246 | * Validate all of the fields in the form.
247 | */
248 | validate: () => Promise
249 | /**
250 | * Submit the form manually.
251 | */
252 | submit: () => Promise
253 |
254 | /**
255 | * Props to pass into a form component to attach the `form` to it:
256 | */
257 | props: {
258 | /**
259 | * An `onSubmit` handler to pass to an element that submits the form.
260 | */
261 | onSubmit: () => Promise
262 | }
263 | }
264 |
265 | /**
266 | * Call `useField()` to create a single field in a form.
267 | */
268 | export function useField(
269 | options: FieldOptions,
270 | ): Field {
271 | let defaultValue = options.defaultValue
272 | let validations = options.validations || []
273 |
274 | let [value, setValue] = React.useState(defaultValue)
275 | let [errors, setErrors] = React.useState([])
276 | let [validating, setValidating] = React.useState(false)
277 | let [focused, setFocused] = React.useState(false)
278 | let [blurred, setBlurred] = React.useState(false)
279 | let [touched, setTouched] = React.useState(false)
280 | let [dirty, setDirty] = React.useState(false)
281 | let [required, setRequired] = React.useState(options.required || false)
282 | let [disabled, setDisabled] = React.useState(options.disabled || false)
283 | let [readOnly, setReadOnly] = React.useState(options.readOnly || false)
284 |
285 | let onFocus = React.useCallback(() => {
286 | setFocused(true)
287 | setTouched(true)
288 | }, [])
289 |
290 | let onBlur = React.useCallback(() => {
291 | setFocused(false)
292 | setBlurred(true)
293 | }, [])
294 |
295 | let valueRef = React.useRef(value)
296 |
297 | function reset() {
298 | valueRef.current = defaultValue;
299 | setValue(defaultValue)
300 | setErrors([])
301 | setValidating(false)
302 | setBlurred(false)
303 | if (!focused) setTouched(false)
304 | setDirty(false)
305 | }
306 |
307 | function validate() {
308 | let initValue = valueRef.current
309 | let errorsMap: Err[][] = []
310 |
311 | setValidating(true)
312 |
313 | let promises = validations.map((validation, index) => {
314 | return Promise.resolve()
315 | .then(() => validation(initValue))
316 | .then(errors => {
317 | errorsMap[index] = errors
318 | if (Object.is(initValue, valueRef.current)) {
319 | setErrors(errorsMap.flat(1))
320 | }
321 | })
322 | })
323 |
324 | return Promise.all(promises)
325 | .then(() => {
326 | if (Object.is(initValue, valueRef.current)) {
327 | setValidating(false)
328 | }
329 | })
330 | .catch(err => {
331 | if (Object.is(initValue, valueRef.current)) {
332 | setValidating(false)
333 | }
334 | throw err
335 | })
336 | }
337 |
338 | let setValueHandler = (value: Val) => {
339 | valueRef.current = value
340 | setDirty(true)
341 | setValue(value)
342 | setErrors([])
343 | validate()
344 | }
345 |
346 | let onChange = (value: Val) => {
347 | if (isReactSyntheticEvent(value)) {
348 | throw new TypeError(
349 | "Expected `field.onChange` to be called with a value, not an event",
350 | )
351 | }
352 | setValueHandler(value)
353 | }
354 |
355 | let valid = !errors.length && (required ? !isEmptyValue(value) : true)
356 |
357 | return {
358 | value,
359 | errors,
360 | defaultValue,
361 | setValue: setValueHandler,
362 | focused,
363 | blurred,
364 | touched,
365 | dirty,
366 | valid,
367 | validating,
368 | required,
369 | disabled,
370 | readOnly,
371 | setRequired,
372 | setDisabled,
373 | setReadOnly,
374 | reset,
375 | validate,
376 | props: {
377 | value,
378 | required,
379 | disabled,
380 | readOnly,
381 | onFocus,
382 | onBlur,
383 | onChange,
384 | },
385 | }
386 | }
387 |
388 | /**
389 | * Call `useForm()` with to create a single form.
390 | */
391 | export function useForm(options: FormOptions): Form {
392 | let fields = options.fields
393 | let onSubmit = options.onSubmit
394 | let [submitted, setSubmitted] = React.useState(false)
395 | let [submitting, setSubmitting] = React.useState(false)
396 | let [submitErrors, setSubmitErrors] = React.useState([])
397 |
398 | let focused = fields.some(field => field.focused)
399 | let touched = fields.some(field => field.touched)
400 | let dirty = fields.some(field => field.dirty)
401 | let valid = fields.every(field => field.valid)
402 | let validating = fields.some(field => field.validating)
403 |
404 | let fieldErrors = fields.reduce(
405 | (errors, field) => errors.concat(field.errors),
406 | [] as Err[],
407 | )
408 |
409 | function reset() {
410 | setSubmitted(false)
411 | setSubmitting(false)
412 | setSubmitErrors([])
413 | fields.forEach(field => field.reset())
414 | }
415 |
416 | function validate() {
417 | return Promise.all(fields.map(field => field.validate())).then(() => {})
418 | }
419 |
420 | function onSubmitHandler() {
421 | setSubmitted(true)
422 | setSubmitting(true)
423 |
424 | return Promise.resolve()
425 | .then(() => onSubmit() as any) // Dunno why TS is mad
426 | .then((errs: void | Err[]) => {
427 | setSubmitting(false)
428 | setSubmitErrors(errs || [])
429 | })
430 | .catch((err: Error) => {
431 | setSubmitting(false)
432 | throw err
433 | })
434 | }
435 |
436 | return {
437 | fieldErrors,
438 | submitErrors,
439 | submitted,
440 | submitting,
441 | focused,
442 | touched,
443 | dirty,
444 | valid,
445 | validating,
446 | validate,
447 | reset,
448 | submit: onSubmitHandler,
449 | props: {
450 | onSubmit: onSubmitHandler,
451 | },
452 | }
453 | }
454 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | // prettier-ignore
2 | {
3 | "exclude": ["example", "dist"],
4 | "compilerOptions": {
5 | /* Basic Options */
6 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
7 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
8 | // "lib": [], /* Specify library files to be included in the compilation. */
9 | // "allowJs": true, /* Allow javascript files to be compiled. */
10 | // "checkJs": true, /* Report errors in .js files. */
11 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | "declaration": true, /* Generates corresponding '.d.ts' file. */
13 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
14 | "sourceMap": true, /* Generates corresponding '.map' file. */
15 | // "outFile": "./", /* Concatenate and emit output to single file. */
16 | "outDir": "./dist", /* Redirect output structure to the directory. */
17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
18 | // "composite": true, /* Enable project compilation */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | // "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 |
52 | /* Source Map Options */
53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
57 |
58 | /* Experimental Options */
59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/types/email-regex.d.ts:
--------------------------------------------------------------------------------
1 | declare module "email-regex" {
2 | export interface EmailRegexOpts {
3 | exact?: boolean
4 | }
5 |
6 | export default function emailRegex(opts?: EmailRegexOpts): RegExp
7 | }
8 |
--------------------------------------------------------------------------------