formular
Version:
Build forms in React. Easy-Peasy!
326 lines (246 loc) • 6.69 kB
Markdown
# Formular
Easy way to work with forms in React. Using React Hooks 😏
<p>❤️ Just 3kb gzip</p>
<p>❤️ React hooks</p>
<p>❤️ TypeScript</p>
<p>❤️ Changes made within form rerender only changed fields</p>
#### Installation
```
npm install formular
```
## Philosophy
There are many form libraries that works out of the box - _"import Form, Field and that's it"_. But in most projects common fields are customized by design and usage of Form, Field components become impossible. **Formular** doesn't provide inboxing components for fast start, but it provides easy way to attach form functionality to custimized fields!
For example you have your own styled `Input` component with specific logic inside
```tsx
import { useField, useFieldState } from 'formular'
const FormularInput = () => {
const field = useField()
const state = useFieldState(field)
return <CustomInput value={state.value} onChange={field.set} />
}
```
`useField` is a wrapper for `new Field`:
```tsx
const useField = (opts, deps) => useMemo(() => new Field(opts), deps || [])
```
So when field's state updates FormularSelect doesn't render. Here `useFieldState` comes, it triggers react state update which call component's render.
#### This is Field, and whats about Form?
Lets update the code above to reuse Select component
```tsx
import { useField, useFieldState } from 'formular'
const FormularInput = ({ field }) => {
const state = useFieldState(field)
return <CustomInput value={state.value} onChange={field.set} />
}
const Auth = () => {
const emailField = useField()
const passwordField = useField()
return (
<>
<FormularInput field={emailField} />
<FormularInput field={passwordField} />
</>
)
}
```
Let's add validators and submit logic
```tsx
const required = (value) => {
if (!value) {
return 'Required'
}
}
const Auth = () => {
const emailField = useField({
validate: [ required ],
})
const passwordField = useField({
validate: [ required ],
})
const handleSubmit = useCallbac(async () => {
const isEmailValid = await emailField.validate()
const isPasswordValid = await passwordField.validate()
const emailValue = emailField.state.value
const passwordValue = passwordField.state.value
const emailError = emailField.state.error
const passwordError = passwordField.state.error
}, [])
return (
<>
<FormularInput field={emailField} />
<FormularInput field={passwordField} />
<button onClick={handleSubmit}>Login</button>
</>
)
}
```
Same could be written using `useForm`
```tsx
const Auth = () => {
const form = useForm({
email: [ required ],
password: [ required ],
})
const handleSubmit = useCallbac(async () => {
const isValid = await form.validate()
const values = form.getValues() // { email: '', password: '' }
const errors = form.getErrors() // { email: 'Required', password: 'Required' }
}, [])
return (
<>
<FormularInput field={emailField} />
<FormularInput field={passwordField} />
<button onClick={handleSubmit}>Login</button>
</>
)
}
```
in most cases you need submit
```tsx
const handleSubmit = useCallbac(async () => {
try {
const values = await form.submit() // { email: '', password: '' }
}
catch (errors) {} // { email: 'Required', password: 'Required' }
}, [])
```
### Validation
```
const required = (value) => {
if (!value) {
return 'Required'
}
}
const uniqueEmail = async (value) => {
const isExist = await fetch(`check-email-exist?email={value}`)
if (isExists) {
return 'Account with such email already exists'
}
}
const field = useField({
validate: [ required, uniqueEmail ]
})
const isValid = await field.validate()
```
☝️ `field.validate()` always async!<br />
☝️ a validator function should return `undefined` if value is valid
## Examples
- [Basics](https://codesandbox.io/s/formular-basics-cke7r)
- [Building own Input](https://codesandbox.io/s/formular-building-own-input-4qsxd)
- [Async validation](https://codesandbox.io/s/formular-async-validation-i6l4c)
## Options
#### Form
```ts
type FormOpts = {
name?: string
fields: {
[key: string]: FieldOpts
}
initialValues?: object
}
```
#### Field
```ts
type FieldOpts = {
name?: string
value?: string // initial value
validate?: Array<Function> // list of validators
readOnly?: boolean
validationDelay?: number // adds debounce to validation
}
```
## Interfaces
#### Form
```ts
type FormEntity = {
name?: string
opts: FormOpts
fields: {
[key: string]: Field
}
state: {
isValid: boolean
isChanged: boolean
isValidating: boolean
isSubmitting: boolean
isSubmitted: boolean
}
setState(values: Partial<State>): void
setValues(values: object): void
getValues(): object
unsetValues(): void
getErrors(): object
async validate(): Promise<boolean>
async submit(): Promise<object>
on(eventName: string, handler: Function): void
off(eventName: string, handler: Function): void
}
const form: FormEntity = useForm(opts)
```
#### Field
```ts
type FieldEntity = {
form?: Form
name?: string
opts: FieldOpts
node?: HTMLElement
validators: Array<Function>
readOnly: boolean
debounceValidate: Function // method to call validation with debounce
state: {
value: any
error: any
isChanged: boolean
isValidating: boolean
isValidated: boolean
isValid: boolean
}
setState(values: Partial<State>): void
setRef(node: HTMLElement): void
unsetRef(): void
set(value: any, opts?: { silent: boolean }): void
unset(): void
validate = (): CancelablePromise
on(eventName: string, handler: Function): void
off(eventName: string, handler: Function): void
}
const field: FieldEntity = useField(opts)
```
**Note:** `{ silent: true }` in field.set doesn't trigger events.
## Events
#### Form
```ts
form.on('state change', (state) => {
// triggers on a form's state change
})
form.on('change', (field) => {
// triggers on a field change
})
form.on('blur', (field) => {
// triggers on a field blurring
})
```
#### Field
```ts
form.on('state change', (state) => {
// triggers on a field's state change
})
field.on('set', (value) => {
// triggers on a value set
})
field.on('change', (value) => {
// simlink to "set"
})
field.on('unset', () => {
// triggers on a value unset
})
field.on('start validate', () => {
// triggers on a validation start
})
field.on('validate', (error) => {
// triggers on a validation finish
})
field.on('blur', () => {
// triggers on a field blurring
})
```