@schematic-forms/react
v0.9.6
Published
react adapter for schematic forms
Downloads
79
Maintainers
Readme
Description
Schematic forms is schema based form processing library for react writen with typescript.
Instalation
npm i --save @schematic-forms/core @schematic-forms/react
Usage
Library uses context and hooks(or render props) as a main concept of usage.
import React, { FC } from 'react'
import { useController, FormProvider, FieldConsumer } from '@schematic-forms/react'
import { Str } from '@schematic-forms/core'
export const Form: FC<{}> = () => {
const { controller, submit, clear } = useController({
fields: {
email: Str(true),
password: Str(true)
},
validators: {
email: EmailValidator("INCORRECT_EMAIL")
},
submit: (data) => {
console.log(data)
}
})
return (
<FormProvider controller={controller} >
<FieldConsumer field="email">
{({ value, setValue, error }) => (
<div>
{error}
<input value={value} onChange={e => setValue(e.target.value)} />
</div>
)}
</FieldConsumer>
<FieldConsumer field="password">
{({ value, setValue, error }) => (
<div>
{error}
<input value={value} onChange={e => setValue(e.target.value)} />
</div>
)}
</FieldConsumer>
<ErrorConsumer>
{({ hasError }) => (
<button disabled={hasError} onClick={submit} >Submit</button>
)}
</ErrorConsumer>
<button onClick={clear}>Clear</button>
</FormProvider>
)
}
What's going on here, yeah? Let me explain. Lets start with useController() hook. it takes config and returns object like { controller, submit, clear }
type config = {
fields: {
[key: string]: SchemaType
},
validators: {
[key: string]: (value: FieldValue) => void | Error
},
submit: (data: Data) => void | { [key: string]: Error } | Promise<void | { [key: string]: Error }>
}
From top into bottom:
- fields - form fields. keys - common string, values something like Str(), Bool(), Num(), that u can import from @schematic-forms/core
- validators - object with field validation functions.
- submit - function that will be executed when all form conditions are met. submit will get form data and should return void or error map (also it can returns a Promise).
Then we'll take FormProvider component. It's simple context provider requiring controller that useController() returns, no more.
FieldConsumer is dirty(but easy) way to change fields. takes only one prop: field - field name to control. As children takes function with following type
type RenderFunction = (
props: { value: any, setValue: (nextValue: any
) => void, error: null | string }) => ReactNode
value and setValue react's useState() like functions. error will provide errors on this field
ErrorConsumer represents exception handling.
type ErrorConsumerProps = {
field?: string
code?: string
}
If no props were provided ErrorConsumer will react on any exception in any fields. As children u can pass RenderFunction like in example above or just ReactNode.
Value types
Types you can pass into "fields" object.
const fields = {
str: Str(required: boolean, defaultValue: string),
num: Num(required: boolean, defaultValue: number),
object: Obj(
objectSchema: ObjectSchema,
required: boolean,
defaultValue: ObjectSchemaRealization
),
enum: Enum(variant: Variant[], required: true, defaultValue: Variant),
array: Arr(
arrType: SchemaFieldType,
require: boolean,
defaultValue: SchemaFieldType[]
),
mix: Mix(TypeToMix[], required: boolean, defaultValue: TypeToMix)
}
// Object type example
const book = Obj({
title: Str(true),
count: Num(true),
author: Obj({
name: Str(true),
email: Str(true)
})
}, true)
// Enum type example
const gender = Arr<["M", "F"]>(["M", "F"], true)
/*
You have to use generic argument because typescript identifies arrays like ["M", "F"] as a string array.
*/
// Array type example
const titles = Arr(Str(true), true)
// Mix type example
const mixedType = Mix([
Str(true),
Obj({ title: Str(true) }, true)
])
Hooks
useForm
useForm is a right way to change controller fields and create custom form fields.
function useForm<ValueType>(
name: string,
nullValue?: ValueType
): [
ValueType | undefined, // value
| (nextVal: ValueType // change function
| ((prevValue?: ValueType) => ValueType | undefined)) => void,
string | null // error
]
// Example
const FormInput: FC<{ name: string }> = ({ name }) => {
const [value, setValue, error] = useForm(name, "");
return (
<div>
{error}
<input
value={value}
onChenge={e => setValue(e.target.value)}
/>
</div>
)
}
useHasError
Returns true if controller has errors and false if not.
// ... react component with useController hook
const hasErrors = useHasError(controller)
return (
<div>
{hasErrors && "ERRORS!"}
{/* ... */}
</div>
)
usePending
If your submit function returns Promise you probably want to know it`s status. usePending returns { isPending: true } if promise is pending and { isPending: false } if not.
// ... react component with useController hook
const { isPending } = usePending(controller)
return (
<div>
{isPending && "LOADING..."}
{/* ... */}
</div>
)
useValue
Returns controller field value
// ... react component with useController hook
const { controller } = useController({
fields: {
email: Str(true)
}
})
const email = useValue(controller, "email")
return (
<div>
email: {email}
</div>
)
Components
ErrorConsumer
Consumes controller errors using context.
interface ErrorConsumerProps {
children:
| ReactNode
| ((
props: { hasError: true, errorCode: string } | { hasError: false, errorCode: null }
) => ReactNode)
/** @description observable controller field*/
field?: string
/** @description error type */
error?: string
}
- Without "field" prop component will trigger on any error in any field.
- "error" prop specify which error type in "field" should trigger re-render.
// Example
// ... react component with useController hook
const { controller } = useController({
fields: {
email: Str(true)
},
validators: EmailValidator("EMAIL_ERROR")
})
return (
<FormProvider controller={controller}>
<ErrorConsumer field={email}>
Error in E-mail field
</ErrorConsumer>
<ErrorConsumer field={email} error="REQUIRED">
E-mail required
</ErrorConsumer>
<ErrorConsumer field={email} error="EMAIL_ERROR">
E-mail validation failed
</ErrorConsumer>
<ErrorConsumer field={email}>
{({ hasError, errorCode }) => !hasError ? null : (
<div>
{errorCode}
</div>
)}
</ErrorConsumer>
</FormProvider>
)
PendingConsumer
Keeping the controllers status in check.
// Example
// ... react component with useController hook
const { controller } = useController({
fields: {
email: Str(true)
},
validators: EmailValidator("EMAIL_ERROR"),
submit: async () => {
await delay(5000)
}
})
return (
<FormProvider controller={controller}>
<PendingConsumer>
Loading...
</PendingConsumer>
<PendingConsumer>
{({ isPending }) => (
<button disabled={isPending}>
submit
</button>
)}
</PendingConsumer>
</FormProvider>
)