OGURA_Daiki
Posted on April 1, 2019
Managing forms in React is hard but boring task.
In addition, you are going to suffer from type unsafeness due to of native DOM input restrictions if you preferred TypeScript.
There are several libraries to solve the problem. react-form, redux-form, formik...
Now I added the new one which names Mayoiga.
https://github.com/hachibeeDI/mayoiga
The charasteristics of Mayoiga are below:
- React Hooks base (useForm to summon form component)
- Strict type check (I love TypeScript!)
- Simple API for less learning cost
How to use
This is minimum working example.
import * as React from 'react';
import {FC} from 'react';
import {useForm, createFormScope} from 'mayoiga';
import {Input, NumberInput} from 'mayoiga/lib/forms';
const required = (target: string) => (target.length === 0 ? 'required' : undefined);
const between = (min: number, max: number) => (target: number) => {
if (target < min) {
return `more than ${min}`;
}
if (target > max) {
return `less than ${max}`;
}
};
const choice = function<T>(...candidates: Array<T>) {
return (target: T) => (candidates.includes(target) ? undefined : 'You should choose from the candidates.');
};
const INITIAL_FORM_STATE = {
name: '',
age: 13,
race: 'fish',
};
const {context, scope} = createFormScope<typeof INITIAL_FORM_STATE>();
const DemoForm = scope(props => {
const {Form, Field} = useForm(context);
return (
<Form onSubmit={value => console.log(value)}>
<Field name="name" component={Input} validations={[required]} />
<Field name="age" component={NumberInput} validations={[between(5, 20)]} />
<Field name="race" component={Input} validations={[choice('fish', 'squid', 'octopus')]} />
<button disabled={!props.touched || Object.values(props.errors).some(e => !!e.length)}>submit</button>
</Form>
);
});
export default function DemoApp() {
return <DemoForm initialState={INITIAL_FORM_STATE} onSubmit={value => alert(`submit ${JSON.stringify(value)}`)} />;
}
Let me explain some lines that is essential. (These are still working in improve progress so may be changed)
Scope
const {context, scope} = createFormScope<typeof INITIAL_FORM_STATE>();
createFormScope
is to create a scope which allows share the form state between Form and Fields.
A component was included in scope
is going to have two additional props initialState
and onSubmit
. Those values are working you can see <DemoApp />
.
Form
In form layer component, you can also handling onSubmit
on <Form />
component in case you need.
<Field />
requires name
and component
, optionally validations
.
name
of the Field isn't just typed as string
but keyof State
. So that if you had typo like as <Field name="rage" ...
TypeScript compiler warn like "rage is not a type of 'keyof typeof INITIAL_FORM_STATE'".
component
is also strictly typed. A component should follow the signature named InputProtocol<State, Name>
which is defined as below:
export type InputProtocol<S, Name extends keyof S, DelegatedProps> = Omit<InputHTMLAttributes<any>, 'name' | 'onChange' | 'value'> & {
name: Name;
value: S[Name];
errors: ReadonlyArray<string>;
touched: boolean;
onChange(name: Name, value: S[Name]): void;
};
I have written some glue for native DOM component. You can import those from mayoiga/lib/forms
.
You can also write glue for any components even if the component is complex which returns not primitive value i.e. Array or Object.
Validation
validations
of <Field />
is a array of Validator
. Definition of Validator
is below.
type Validator<S, Name extends keyof S> = (target: S[Name], record: S) => undefined | string;
Validator can get the value of the target and also all other records. You can return string as error message.
Lastly
Mayoiga is under development and no documentation so far (because of my English skill XD) though any questions/issues/requests are welcome. :)
However, I think anyone who hate managing week typed input with boilerplate codes are interested in the concept.
Thanks!
Posted on April 1, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.