React form helper
Install the package.
$ npm install komo -s
OR
$ yarn add komo
I would encourage you to look at the examples in src/example
. You can also clone the repository source then run these examples. Form validation no matter your effort can get complicated. Komo works hard to simplify that but in the end some things just require more control. The examples will help you work through those use cases!
$ git clone https://github.com/blujedis/komo.git
$ yarn install
$ yarn start
Below we import react and komo then initialize the useForm hook.
We create our form
element and the elements we need for our form inputs. We'll do a more complete example below but let's take it in steps so it's easier to make sense of.
import React, { FC } from 'react';
import useForm from 'komo';
const MyForm: FC = () => {
const { register } = useForm();
return (
<form noValidate>
<label htmlFor="firstName">First Name: </label>
<input name="firstName" type="text" ref={register} />
</form>
);
};
export default MyForm;
This is just a simple example of what you can do watching our error state to display feedback to the user. The sky is really the limit but this will give you the idea.
const Error = ({ name }) => {
const prop = state.errors && state.errors[name];
// No error at this key just return null.
if (!prop || !prop.length)
return null;
const err = prop[0];
// Simply return a div colored red with the error message.
// See below for error object properties.
return (<div style={{ color: 'red' }}>{err.message}</div>);
};
We'll create a simple handler, it won't do much, that's up to you but let's how to wire up the handle submit function so that you can submit your form.
const onSubmit = (model) => {
if (state.isSubmitted) // no need to submit again.
return;
console.log(model); // display our model data.
};
Not let's put it all together so you can see it in one place.
import React, { FC } from 'react';
import useForm from 'komo';
const MyForm: FC = () => {
// Import the Use Form Hook //
const { register, handleSubmit, handleReset, state } = useForm({
// Simple function to validate our schema.
// If our function returns null/undefined or
// an empty object there is no error.
// if an object with keys and error messages is
// returned your form state will be updated and
// you can use those errors to display messages etc.
// NOTE you can also return a promise. In order for errors
// to be triggered the must be returned as a rejection.
// Could be as simple as Promise.reject(errors);
validationSchema: (model) => {
const errors = {};
if (message.length < 10)
errors.message = ['Message must be at least 10 characters'];
return errors;
},
// Enable native validation like required, min, max etc...
// NOTE: Not all native validation types are supported
// but the majority of what you need is there!
enableNativeValidation: true
});
// This will handle when a user submits the form.
const onSubmit = (model) => {
if (state.isSubmitted) // no need to submit again.
return;
console.log(model); // display our model data.
};
// This will dispaly an error if one is present for the provided key/name.
const Error = ({ name }) => {
const prop = state.errors && state.errors[name];
if (!prop || !prop.length)// No error at this key just return null.
return null;
const err = prop[0];
return (<div style={{ color: 'red' }}>{err.message}</div>); // return our err message.
};
return (
<div>
<h2>My Form</h2>
<hr /><br />
<form noValidate onSubmit={handleSubmit(onSubmit)} onReset={handleReset}>
<label htmlFor="firstName">First Name: </label>
<input name="firstName" type="text" ref={register} defaultValue="Peter" /><br /><br />
<label htmlFor="lastName">Last Name: </label>
<input type="text" name="lastName" ref={register} defaultValue="Gibbons" /><br /><br />
<label htmlFor="email">Email: </label>
<input name="email" type="email" ref={register} required /><br /><br />
<label htmlFor="message">Message: </label>
<textarea name="message" ref={register} defaultValue="too short" >
</textarea><Error name="message" /><br /><br />
<hr />
<br />
<input type="reset" value="Reset" />
<input type="submit" value="Submit" />
</form>
</div>
);
};
export default MyForm;
There are three ways to set defaults for an element. You can usse the traditional defaultValue
prop or defaultChecked
in the case of a checkbox. Additionally you can set defaults in your defaults object, lastly you can set defaults in your yup schema
.
<input name="firstName" type="text" ref={register} defaultValue="Your_Name_Here" />
const { register } = useForm({
defaults: {
firstName: 'Your_Name_Here'
}
});
Komo also supports promises for your defaults. Below we're just using Promise.resolve
but you could use fetch or axios etc to get your data. Note errors are logged to the console but does not stop Komo's initialization. If an error or no data returned Komo will just initialize with an empty defaults object.
const { register } = useForm({
defaults: Promise.resolve({
firstName: 'Your_Name_Here'
})
});
When using a yup schema for your validationSchema
, Komo will grab the defaults and use them for your form model.
import * as yup from 'yup';
const schema = yup.object({
firstName: string().default('Your_Name_Here')
});
const { register } = useForm({
validationSchema: schema
});
Komo allows you to register with options as well. For example perhaps you wish to disable ALL validation triggers on a given element.
<input name="firstName" type="text" ref={register({ validateChange: false, validateBlur: false})} />
Komo allows models with nested data. Any form field can have a mapped path enabling getting and setting from a nested prop.
The below will map errors in our error model { firstName: [ error objects here ] }
but it's model data will get/set from the nested path of user.name.first
.
<input name="firstName" type="text" ref={register({ path: 'user.name.first' })} />
There are cases where you may need to initialize your data after you mount using useEffect. Komo exposes a method for this called reinit
. This method is used by Komo internally itself.
const { reinit } = useForm({
// options here.
});
useEffect(() => {
// some promise or state change data is now available.
reinit(defaults_from_state);
}, [defaults_from_state]);
Komo has some built in hooks that make it trivial to wire up to fields or expose your own hooks using the built in withKomo
helper.
Your best bet is to clone the repo then run yarn start
. Then navigate to the /advanced page. This will give you an idea of some of the cool stuff Komo can do.
Expose the hook from Komo's main useForm hook, then call the hook and pass in the form element name you wish to use. Simple as that. Just remember your hook will not have access to the element until Komo has mounted.
const { useField } = useForm({
// options here excluded for brevity.
});
const firstName = useField('firstName');
Below we have an Error Component and a TextInput Component that use the errors exposed from our hook.
/**
* Simple component to display our errors.
* @param props our error component's props.
*/
const ErrorMessage = ({ errors }) => {
if (!errors || !errors.length)
return null;
const err = errors[0];
return (<div style={{ color: 'red', margin: '6px 0 10px' }}>{err.message}</div>);
};
/**
* Custom input message text field.
*/
const TextInput = (props: InputProps) => {
const { hook, ...clone } = props;
const { name } = clone;
const field = props.hook(name);
const capitalize = v => v.charAt(0).toUpperCase() + v.slice(1);
return (
<div>
<label htmlFor={name}>{capitalize(name)}: </label>
<input type="text" ref={field.register} {...clone} />
<ErrorMessage errors={field.errors} />
</div>
);
};
const App = () => {
// Create some hooks to our fields
const firstName = useField('firstName');
const lastName = useField('lastName');
// When the first name field blurs set the
// focus to the last name field.
const onFirstBlur = (e) => lastName.focus();
// When last name changes manually validate
// and display response or error.
const onLastChange = (e) => {
lastName.validate()
.then(res => console.log.bind(console))
.catch(err => console.log.bind(console));
};
return (
<form noValidate onSubmit={handleSubmit(onSubmit)} onReset={handleReset}>
<label htmlFor="firstName">First Name: </label>
<input name="firstName" type="text" ref={register} onChange={onFirstBlur} /><br /><br />
<TextInput name="lastName" hook={useField} onChange={onLastChange} /><br />
<button type="button" onClick={lastName.focus}>Set LastName Focus</button><br /><br />
<button type="button" onClick={updateLast('Waddams')}>Set LastName to Waddams</button><br /><br />
<button type="button" onClick={updateLast('')}>Set LastName to Undefined</button><br /><br />
<JsonErrors errors={state.errors} />
<br />
<hr />
<br />
<input type="reset" value="Reset" />
<input type="submit" value="Submit" />
</form>
);
}
Some times you have a complex component that requires you to create a virtual registration where you might have other bound elements you wish to get values from and/or validate. That's where virtual elements come in.
Komo's hooks make this a snap! Let's walk through it.
Let's say you want to create a virtual element called fullName which is simply a concatenation
of both name.first
and name.last
. We only want to trigger validation for fullName.
const model = {
name: {
first: 'Milton',
last: 'Waddams'
}
}
We create a virtual element below by passing in the second arg in our useField hook
const fullName = hook(name, true);
. This tells Komo not wire up data binding
events as we typically would for an element.
const VirtualField: FC<Props> = ({ name, hook }) => {
// Below we create hooks for our Virtual (fullName)
const fullName = hook(name, true);
// The below to element hooks are created dynamically.
// meaning we didn't pass them into our model.
const first = hook('first');
const last = hook('last');
// We register our virtual element.
// Note we derive our default value from
// the vaules of the two bound elements.
fullName.register({
defaultValue: (model) => {
if (model.name && model.name.first && model.name.last)
return model.name.first + ' ' + model.name.last;
return '';
},
required: true
});
// On blur listener to update our virtual element.
const onBlur = (e) => {
// We trim here so we don't end up with ' ' as space.
fullName.update((first.value + ' ' + last.value).trim());
};
// Finally we return our simple component
// registering our bound elements as usual.
return (
<>
<p>
<span>Virtual Value: </span><span style={{ fontWeight: 'bolder' }}>{fullName.value}</span>
</p>
<label htmlFor="first">First Name: </label>
<input
name="first"
type="text"
onBlur={onBlur}
ref={first.register({ path: 'name.first', validateBlur: false })} />
<br /><br />
<label htmlFor="last">Last Name: </label>
<input
name="last"
type="text"
onBlur={onBlur}
ref={last.register({ path: 'name.last', validateBlur: false })} />
<br /><br />
</>
);
};
By default Komo will attemp to cast your data before persisting to model. For example a string of "true" will become an actual boolean true
.
This feature can be disabled by setting options.castHandler
to false or null.
You can also pass your own function that casts the value and simply returns it.
Although your model will likely be converted to a string using JSON.stringify
before posting
to your server, casting the model value only allows you to do checks against the model state as you'd expect.
Again if this is not what you want simply disable it.
const { register } = useForm({
castHandler: (value, path, name) => {
// do something with value and return.
return value;
}
})
You can of course use Komo with most UI libraries although they may differ in configuration. Komo aims to give you granular control. It's up to use to create your own Component wrappers and things to make life easier. This is by design. Otherwise too many opinions get in the way.
We'll show just the form and one input for brevity/clarity but using with Matieral is as simple as passing our register function to Material's inputRef
prop.
See More From Material-UI Docs
import Input from '@material-ui/core/Input';
<form>
<Input name="phone" inputRef={register({ path: 'numbers.home' })} placeholder="Phone" />
</form>
See https://blujedis.github.io/komo/
See CHANGE.md
See LICENSE.md