komo
TypeScript icon, indicating that this package has built-in type declarations

0.1.56 • Public • Published

React form helper

Installation

Install the package.

$ npm install komo -s

OR

$ yarn add komo

Running Examples

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

The Basics

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;

Error Component

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>);

  };

Form Submission

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.
};

Complete Example Form

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" />&nbsp;
        <input type="submit" value="Submit" />

      </form>

    </div>
  );

};
export default MyForm;

Defaults & Options

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.

Using a Prop

   <input name="firstName" type="text" ref={register} defaultValue="Your_Name_Here" />

Using Defaults Options

  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'
    })
  });

Using Yup Schema

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
});

Registering with Options

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})} />

Nested Data

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' })} />

Reinit After Effect

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]);

Hooks

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.

useField Hook

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');

Companion Hook Components

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>
  );
};

Form with Hooks

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" />&nbsp;
      <input type="submit" value="Submit" />

    </form>
  );

}

Virtual Elements

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.

Consider a Nested Model

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'
  }
}

Virtual Component

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 />

    </>

  );

};

Casting Model

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;
  }
})

UI Libraries

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.

Material UI

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>

Docs

See https://blujedis.github.io/komo/

Change

See CHANGE.md

License

See LICENSE.md

Package Sidebar

Install

npm i komo

Weekly Downloads

43

Version

0.1.56

License

ISC

Unpacked Size

1.58 MB

Total Files

115

Last publish

Collaborators

  • blujedis