create-form is a declarative, typescript-driven form control library. create-form allows you to quickly apply setters/getters/validators within atomic, props-less components by following React's Context API.
This README was generated by anansi.
yarn add @react-noui/create-form
This library employs the factory pattern when creating the data-layer for your form controls.
If you are familiar with React.createContext
then you will be familiar with how to use this library.
createForm
creates the state management for Controlled inputs. This example uses
type Todo = {
userId: number;
title: string;
completed: boolean;
}
const TodoForm = createForm<Todo>({
props: {
completed: {
type: 'checkbox',
},
title: {
placeholder: 'What needs to be done?',
},
userId: {
type: 'hidden', // required, but only for fetch
},
},
});
function CreateTodo({ user }: { user: User }) {
return (
<TodoForm.Provider defaultValues={{ userId: user.id, title: '', completed: false }}>
<TodoCompleted />
<TodoTitle />
<TodoUserId />
<CreateTodoSave />
</TodoForm.Provider>
);
}
function EditTodo({ user, todo }: { user: User, todo: Todo }) {
return (
<TodoForm.Provider defaultValues={todo}>
<TodoCompleted />
<TodoTitle />
<TodoUserId />
<EditTodoSave todoId={todo.id} />
</TodoForm.Provider>
);
}
function TodoCompleted() {
const { completed } = useForm(TodoForm);
return <input {...completed} />
}
function TodoTitle() {
const { title } = useForm(TodoForm);
return <input {...title} />
}
function TodoUserId() {
const { userId } = useForm(TodoForm);
return <input {...userId} />
}
function EditTodoSave({ todoId }: { todoId: number }) {
const form = useForm(TodoForm);
const handleClick = useCallback(() => {
fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
method: 'PUT',
body: JSON.stringify(form.toJSON()),
});
}, [form, todoId]);
return <button onClick={handleClick}>Update</button>
}
function CreateTodoSave() {
const form = useForm(TodoForm);
const handleClick = useCallback(() => {
fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify(form.toJSON()),
});
}, [form]);
return <button onClick={handleClick}>Create</button>
}
import { createForm } from '@react-noui/create-form';
type Login = {
email: string;
password: string;
session: boolean;
}
const LoginForm = createForm<Login>()
// -> { Context, Provider }
These props map directly to the desired field. Props
can be any semantic HTMLInputElement
propery.
const LoginForm = createForm<Login>({
props: {
email: {
type: 'email',
placeholder: 'Account email',
},
password: {
type: 'password',
placeholder: 'Account password',
},
},
});
Provide custom validation methods upon receiving changes to key K
of provided type T
.
The keys allowed in your validate
option must match the keys you provided from type T
,
but are not required.
For the LoginForm
example, we can provide options to validate email | password
:
const LoginForm = createForm<Login>({
validate: {
email: (value) => !value.length ? 'Email cannot be empty' : undefined,
password: (value) => !value.length ? 'Password cannot be empty' : undefined,
},
});
If you don't want to use controlled inputs, you can create an uncontrolled form. Most of the controlled APIs do not exist for uncontrolled forms due to the nature of uncontrolled inputs.
Also, options.validate
is not possible because onChange
events are no longer run against the individual inputs.
import { createFormUncontrolled } from '@react-noui/create-form';
const LoginForm = createForm<Login>({
props: {...}
})
// -> { Context, Provider }
Short-cut access to the LoginForm
APIs. This should be composed within LoginForm.Provider
.
function LoginFormConsumer() {
const form = useForm(LoginForm)
// ...
}
attribute | type | effect |
---|---|---|
form.reset(field) |
(form[FIELD]) => void |
Resets form[FIELD] values, errors, files, etc. Resets form[FIELD].current to the defaultValues[field.name] for field . |
form.resetAll() |
() => void |
Resets form[FIELD] values, errors, files, etc. Resets form[FIELD].current to the defaultValues for all fields. |
form.toJSON() |
() => void |
Returns JSON format matching shape T |
form.toFormData() |
() => FormData |
Obtain FormData for use with http request/fetch Content-Type: application/x-www-form-urlencoded . |
form.toURLSearchParams() |
() => string |
Returns query string with url-encoded form fields |
form.options |
{props: {...}, validate: {...}} |
Returns the options used in createForm(options) |
Short-cut access to the Uncontrolled LoginForm
APIs. This should be composed within LoginForm.Provider
.
function LoginFormConsumer() {
const form = useFormUncontrolled(LoginForm)
// ...
}
attribute | type | effect |
---|---|---|
form.options |
{props: {...}} |
Returns the options used in createFormUncontrolled(options) |
This behaves like React.createContext(...).Provider
such that access to LoginForm
values can only be made from components within this provider.
const LOGIN_DEFAULTS: Login = {
email: '',
password: '',
session: false,
}
function LoginFormPage() {
return (
<LoginForm.Provider defaultValues={LOGIN_DEFAULTS}>
{...components}
</LoginForm.Provider>
)
}
Where T extends Record<string, string | number | boolean>
. This library currently supports primitive values. HTML
change events typically involve primitive values.
This will be the default values we use in our form as type T
. In the example above, T
is of type Login
.
This behaves like React.createContext(...)
such that access to the LoginForm
APIs can be made.
// Controlled inputs
function LoginFormEmailComponent() {
const form = React.useContext(LoginForm.Context);
return (
<>
<label htmlFor={form.email.name}>Email</label>
<input
id={form.email.name}
name={form.email.name}
value={form.email.value}
onChange={(event) => form.set.email(event.target.value)}
/>
{form.email.error && <div>{form.email.error}</div>}
</>
)
};
Each field defined in type T
is accessible via form[FIELD]
attribute | type | effect |
---|---|---|
[FIELD].default |
T[keyof T] |
Default value provided by you within <MyForm.Provider defaultValues={value} /> . |
[FIELD].current |
T |
Current state of the form value. |
[FIELD].set(value: T[keyof T]) |
(value: T[keyof T]) => void |
Allows setting of [FIELD].current values for matching [FIELD] . |
[FIELD].error |
string | undefined |
Custom string will exist if you have the matching options.validate[FIELD](value: T[keyof T]) defined. |
[FIELD].name |
string |
Provides a random, unique value for use as a primary key for any given field. |
[FIELD].reset() |
() => void |
Reset [FIELD].current state to Provider.props.defaultValues[FIELD] . Clears any files, reader data, etc. |
[FIELD].handleFileEvent(event: React.ChangeEvent<HTMLInputElement>) |
() => void |
Allow files to be referenced for a file change event. onChange={handleFileEvent} . *(see side effects) |
This particular function will capture the event.target.files
for use outside of the component in question.
Basic login form with POST fetch
using JSON payload.
const LoginForm = createForm<Login>({
validate: {
email: (value) => value.length === 0 ? 'Cannot be empty' : undefined,
password: (value) => value.length === 0 ? 'Cannot be empty' : undefined,
},
props: {
email: {
type: 'email',
placeholder:
}
},
})
function Login() {
return (
<LoginForm.Provider defaultValues={{ email: '', password: '', session: false }}>
<LoginEmail />
<LoginEmailError />
<LoginPassword />
<LoginPasswordError />
<SubmitButton />
</LoginForm.Provider>
)
}
function LoginEmail() {
const { email } = useForm(LoginForm);
return <input {...email} />
}
function LoginEmailError() {
const { errors } = useForm(LoginForm);
return (errors.email ? <span>{errors.email}</span> : null)
}
function LoginPassword() {
const { password } = useForm(LoginForm);
return <input {...password} />
}
function LoginPasswordError() {
const { errors } = useForm(LoginForm);
return (errors.password ? <span>{errors.password}</span> : null)
}
function SubmitButton() {
const form = useForm(LoginForm);
const handleClick = useCallback(() => {
fetch('/login', { method: 'post', body: JSON.stringify(form.toJSON()) })
}, [form])
return <button onClick={handleClick}>Login</button>
}
With Visual Studio Code, simply press F5
to start the development server and browser.
yarn start
Ctrl+shift+B
in Visual Studio Code
yarn build
yarn start:server
yarn build:analyze
Run with React Profiler:
yarn build:profile
yarn pkgcheck
Run with Storybook:
yarn storybook