npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nerdgeschoss/react-use-form-library

v0.0.70

Published

A simple form library using react hooks

Downloads

867

Readme

React Use Form Library

Tests

A simple form libray for React using hooks.

Motivation

The motivation for working on this solution for form handling comes from the fact that we wanted something very simple and straighforward to use, while also being as agnostic to the implementation as possible, to make it easy to integrate in every kind of project.

Contrary to other solutions that provide you with react components and/or wrappers, we went for a simple react hook that gives you all the tools needed to implement your forms as you see fit.

  • Easy to use: you supply a model and a submit function to the hook and you get back helpers to use in your inputs.
  • Small: the codebase is quite simple and small. Currently only under 7kb gzipped.
  • Declarative: we use the same handlers as the native inputs { value, onChange, onBlur }.

Table of Contents

  1. Installation
  2. Basic Example
  3. Optional fields
  4. Validation
  5. Error Handling
  6. Advanced Example
  7. Nested Objects
  8. Array values (FieldSet)
  9. API
  10. Development

Installation

Add the library to your project:

yarn add @nerdgeschoss/react-use-form-library

or

npm install @nerdgeschoss/react-use-form-library

Basic example

Codepen

To initialize the hook you will need to supply a valid model object and a custom onSubmit function.

The three main fields you get from the hook are model, fields and onSubmit.

  • model is the updated object which will contain the modified fields.
  • fields is an object in which each key will be generated from properties of the original model you supplied to the hook.
  • onSubmit is the handler you can provide to the <form> element. It will call preventDefault internally and execute the function provided to the hook as onSubmit
import { useForm } from '@nerdgeschoss/react-use-form-library';

function App(): JSX.Element {
  const { fields, onSubmit } = useForm({
    model: {
      name: '',
    },
    onSubmit: async ({ model }) => {
      console.log(model);
    },
  });

  return (
    <form onSubmit={onSubmit}>
      <input
        value={fields.name.value}
        onChange={(v) => fields.name.onChange(v.target.value)}
      />
      <button>Submit</button>
    </form>
  );
}

Optional Fields

You don't need to explicitly enumerate all properties in your model, the library will generate all necesary fields on demand by using Proxy.

import { useForm } from '@nerdgeschoss/react-use-form-library';

function App(): JSX.Element {
  const { fields, onSubmit } = useForm({
    // You can supply an empty object
    model: {},
    onSubmit: async ({ model }) => {
      console.log(model);
    },
  });

  return (
    <form onSubmit={onSubmit}>
      <input
        value={fields.name.value}
        onChange={(v) => fields.name.onChange(v.target.value)}
      />
      <input
        value={fields.phone.value}
        onChange={(v) => fields.phone.onChange(v.target.value)}
      />
      <button>Submit</button>
    </form>
  );
}

If you use Typescript you will have to supply the interface for your model

// ...
const { model, fields, onSubmit } = useForm<{ name?: string; phone?: number }>({
  model: {},
  onSubmit: async () => {
    console.log(model);
  },
});
// ...

Validation

Codepen

The validations object needs to correspond your model object. Each key can take one of the following values:

  • A string that contains prebuild common validations. Currently supported are: required, email, number and more.
  • A custom validation function with the signature (value: model): string[] => {}. The value parameter is useful to supply the updated model and make comparisons. For this validation to report an error you have to return an array of strings. Each string is supposed to be an error that you can later display in the UI.
  • An array which can contain the previous both.
const { model, fields, onSubmit, valid } = useForm({
  model: {
    name: '',
    phone: '',
    customRegex: '',
    email: '',
    emailConfirmation: '' ,
  },
  validations: {
    // A single string
    name: 'required',
    // A function
    phone: (model) => {
      if (isNaN(model.phone)) {
        return ['not a number'];
      }
    },
    // A Regex
    customRegex: /[a-g]/,
    // A combination
    email: ['required', 'email']
    emailComfirmation: [
      'required',
      'email',
      (model) => {
        if (model.email !== model.emailConfirmation) {
          return ['Email must be the same']
        }
      }
    ]
  },
});

Predefined validation strings

| Property | Details | | -------- | ------------------------------------------------------ | | required | checks if the field is not empty. | | email | checks if the value is a valid email address. | | json | checks if the value is a valid json object. | | website | checks if the value is a valid website (http / https). | | number | checks if the value is a number. |


Submit Error Handling

Codepen

The hook also exposes another method: onSubmitError, which is handy if you don't want to use a try/catch in your onSubmit function.

const { model, fields, onSubmit, valid } = useForm({
  model: {
    name: '',
    phone: '',
  },
  onSubmit: async () => {
    throw new Error('submit error');
  },
  onSubmitError: (error) => {
    alert('error');
  },
});

Form state

Codepen

There are several properties exposed from the hook to deal with the state throughout the form lifetime

| Property | Details | | ---------------- | ------------------------------------------------------------------------------------------------------ | | dirty | A boolean value indicating that one or more fields have been changed | | valid | A boolean indicating that the form is valid or not, this one depends on the validation rules provided. | | error | any error thrown within the onSubmit function is stored here as an error object | | submissionStatus | Displays the current status of the submission process |

Submission Status

This variable contains the current state of the form submission process. At any time the form will be in one of the following states:

| Status | Details | | ---------- | --------------------------------------------------------------------------- | | idle | The form has not yet been submitted. It also applied when the form is reset | | submitting | The form is being submitted. This works like a loading state. | | submitted | The form has been successfully submitted. | | error | There is an error while submitting. |

Reset

Sometimes it is useful to reset the form programatically. For this there are two helpful methods:

| Property | Details | | ---------- | -------------------------- | | reset | Clears all fields. | | resetError | Removes the current error. |


Fields

Codepen

Every key supplied to the model property of the hook will be parsed into a field

// ...
const { fields } = useForm({
  model: {
    name: '',
  },
});
// ...

A field will be an object that you can use in a simple setup like:

// ...
<input
  // The updated value
  value={fields.phone.value}
  // A function to modify the value
  onChange={(v) => fields.phone.onChange(v.target.value)}
/>
// ...

Or a more complex solution:

// ...
<div>
  // The required property is derived from the validation of the field
  <label>Name {fields.name.required ? '(required)' : ''}: </label>
  <input
    value={fields.name.value}
    onChange={(v) => fields.name.onChange(v.target.value)}
  />
  // The errors property is an array of strings with all errors for the field
  {fields.name.errors.length && (
    <div>
      {fields.errors.forEach((error) => {
        return <div>{error}</div>;
      })}
    </div>
  )}
</div>
// ...

Helpful properties

touched

If the field is required, the required-field error will always be present if the value is empty. It is good UX to only display the required error if the field has been touched, for this you can use the touched property which will be true once the field has lost focus for the first time (onBlur event).

Important !!

If you want to immediately display errors based on the touched property, you need to add the onBlur event to your input. Otherwise it will only be displayed after an update event triggered by onChange.

Submitting the form will also "touch" all fields, to make sure errors are displayed if the form is invalid.

// ...
const displayErrors = fields.name.touched && fields.name.errors.length;

<div>
  <input
    value={fields.name.value}
    onChange={(v) => fields.name.onChange(v.target.value)}
    onBlur={fields.name.onBlur}
  />
  {
    displayErrors && (
      <div>
        {fields.errors.forEach((error) => {
          return <div>{error}</div>;
        })}
      </div>
    );
  }
</div>
// ...

valid

A simple getter that returns true if there aren't any errors. This would simplify the above conditional to:

const displayErrors = fields.name.touched && !fields.name.valid;

It is also useful if you want to give your input a conditional class to show the user if the field is valid or not

<div className={fields.phone.valid ? 'input--valid' : ''}>
  <input
    value={fields.phone.value}
    onChange={(v) => fields.phone.onChange(v.target.value)}
  />
</div>

dirty

When instantiated, a field will store it's original value in a variable. This getter will then compare the original value to the current value and return true if they are different.


Advanced example

import { useForm } from '@nerdgeschoss/react-use-form-library';

export function MyForm({ isNewItem, addItem, updateItem }: Props): JSX.Element => {
  const { fields, onSubmit, submissionStatus } = useForm({
    model: {
      name: '',
      age: 25,
    },
    onSubmit: async ({model, changes, reset}) => {
      if (isNewItem) {
        // You can use here the updated model, which includes the original model and any changes made
        await addItem(model);
      } else {
        // For updating, you can use only the changes that were made
        await updateItem(changes);
      }
      // If you need to clear the fields, you can call on reset form
      reset();
    },
    // Handle any errors
    onSubmitError: (error) => {
      alert('error');
    },
  });

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          value={fields.name.value}
          onChange={(v) => fields.name.onChange(v.target.value)}
        />
        <input
          value={fields.age.value}
          // Make sure you parse the value to be consistent with the model
          onChange={(v) => fields.age.onChange(Number(v.target.value))}
        />
        // You can use submitting to display loading state
        {submissionStatus === 'submitting' ? (
          <div>Loading...</div>
        ) : (
          // Valid state can also be used to disable submit
          <button disabled={!valid || !dirty}>Submit</button>
        )}
      </form>
    </div>
  );
}

Nested Objects

Each FormField object is also able to contain fields. It behaves similarly to the Form object but with slight differences in its methods (See API).

import { useForm } from '@nerdgeschoss/react-use-form-library';

function App(): JSX.Element {
  const { fields, onSubmit } = useForm({
    model: {
      address: {
        streetName: '',
      },
    },
    onSubmit: async ({ model }) => {
      console.log(model);
    },
  });

  return (
    <form onSubmit={onSubmit}>
      <input
        value={fields.address.fields.streetName.value}
        onChange={(v) =>
          fields.address.fields.streetName.onChange(v.target.value)
        }
      />
      <button>Submit</button>
    </form>
  );
}

Validations also work with nested objects

import { useForm } from '@nerdgeschoss/react-use-form-library';

function App(): JSX.Element {
  const { fields, onSubmit } = useForm({
    model: {
      address: {
        streetName: '',
      },
    },
    onSubmit: async ({ model }) => {
      console.log(model);
    },
    validations: {
      address: {
        streetName: 'required',
      },
    }
  });

  // ...

Since the fields property within FormField is also based on a Proxy object, you can access nested properties without explicitely declaring them

function App(): JSX.Element {
  /* If you use Typescript you have to define the model interface, otherwise you'll get an error
  while trying to access the nested properties */
  const { fields, onSubmit } = useForm<{
    bankDetails?: {
      address?: {
        screetName: string;
      };
    };
  }>({
    model: {},
    onSubmit: async ({ model }) => {
      console.log(model);
    },
  });

  return (
    <form onSubmit={onSubmit}>
      <input
        value={fields.bankDetails.fields.address.fields.streetName.value}
        onChange={(v) =>
          fields.bankDetails.fields.address.fields.streetName.onChange(
            v.target.value
          )
        }
      />
      <button>Submit</button>
    </form>
  );
}

FieldSet

A FieldSet is basically an array of FormFields. Contrary to nested objects, a FieldSet will only be instantiated if it is explicitly declared in the model.

Important !!

If you don't initialize the property in the model and try to access it in your code you will get an error.

This is because at the moment of field instantiation, the addField method within a Form (or a FormField if it is a nested object) will ask if the model value is actually an Array. It is not possible to implicitly determine if the field is array-based only from the type definition.

const { fields } = useForm<{ emails: string[]; images: string[] }>({
  model: {
    images: [],
  },
});

return (
  <form onSubmit={onSubmit}>
    /* This will result in an error because emails has been instantiated as a
    FormField not as a FieldSet */
    {fields.emails.elements.map((field) => {
      // ...
    })}
    /* This will work as intented */
    {fields.images.elements.map((field) => {
      // ...
    })}
  </form>
);

The FieldSet object has a elements property, which is an array of FormField (more in the API)

function App(): JSX.Element {
  const { fields, onSubmit } = useForm({
    model: {
      emails: ['[email protected]'],
    },
    onSubmit: async ({ model }) => {
      console.log(model);
    },
  });

  return (
    <form onSubmit={onSubmit}>
      // a FieldSet fields property is iterable
      {fields.emails.fields.map((field) => {
        // Every FieldSet field is a FormField
        return (
          <div className="input">
            <input
              value={field.value}
              onChange={(v) => field.onChange(v.target.value)}
            />
            // FormFields regenerated from a FieldSet also have a remove method
            <button onClick={() => field.remove()}>remove</button>
          </div>
        );
      })}
      // FieldSet also has an insert method to create new items
      <button onClick={() => fields.emails.add('')}>Add Field</button>
      <button>Submit</button>
    </form>
  );
}

Removing Items

FormField objects inside a FieldSet include a remove method to remove the corresponding element..

<button onClick={() => fields.emails.fields[0].remove()}>remove</button>

Validating a FieldSet

A FieldSet takes the same kind of validation as any other field. On instantiation it will be saved in memory and it will be further applied to any new field created.

useForm({
  model: {
    // Initialize the field
    emails: [],
  },
  validations: {
    // Will be applied to any item added to emails
    emails: ['required', 'email'],
  },
});

If the validation is of the type required, it will also make the field invalid unless it has at least one element.

useForm({
  model: {
    // emails.valid will be false because we're initializing it with no elements.
    emails: [],
  },
  validations: {
    emails: 'required',
  },
});

Advanced FieldSet

A common escenario would be to have an array of objects in your model.

const { fields, onSubmit } = useForm<{
  images: Array<{
    id: string;
    url?: string;
  }>;
}>({
  model: {
    // Initialize the field
    images: [],
  },
});

return (
  <form onSubmit={onSubmit}>
    {fields.images.fields.map((field) => {
      return (
        <div className="input">
          /* You can access a property whithin the field, as you would with a
          nested object */
          <input
            value={field.fields.url.value}
            onChange={(v) => field.fields.url.onChange(v.target.value)}
          />
          <button onClick={() => field.remove()}>remove</button>
        </div>
      );
    })}
    <button
      onClick={() =>
        fields.images.insert({
          id: generateId(),
        })
      }
    >
      Add Field
    </button>
    <button>Submit</button>
  </form>
);

API

useForm

Props

| Property | Details | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | model | Your form model, it should be an object (can be empty). Every property will be mapped into a field. | | validations | A validations object. | | onSubmit | Your custom submit function. It will be parsed internally and provide a onSubmit handler to call programaticaly. | | onSubmitError | A useful handler to deal with errors. | | onChange | A handler for changes in all form fields. |

Form

| Property | Details | | ---------- | --------------------------------------------------------------------- | | changes | A plain object that contains only the modified fields | | model | An updated object that contains the updated model | | fields | A mapped collection, which has a FormField for every key in the model | | dirty | a getter checking if there are any changes | | valid | a getter checking if all required fields are valid | | onSubmit | a method that triggers the function passed as onSubmit param. | | submitting | a loading state for the onSubmit method | | reset | this helper method will reset every field to it's original value |

Form Field

| Property | Details | | ---------- | ------------------------------------------------------------------------------------------------------------ | | value | the updated value of the current field | | required | a required field useful to determine a valid state for the form | | errors | a string array containing any errors | | validation | a validation object that is triggered on every update | | focused | a boolean stating if the field is currently in focus, you'll need to use onFocus/onBlur to change this state | | onChange | a value change method, sets touched to true and triggers an update | | onBlur | this method sets the touched state to true and focused to false | | onFocus | sets focused to true | | hasValue | a helper method to determine empty state. Checks for empty strings | | validate | a validation function triggered on every update | | setTouched | takes a boolean and will set the touched state to this value | | reset | a helper method that resets the field to it's original value | | valid | a getter that checks if the field has any errors | | dirty | a getter that compares the current value to the original value passed on instantiation | | touched | a touched state, initially false and changed to true when calling onChange/onBlur | | remove | Only within a FieldSet, it removes the object from the collection |

Nested Fields

These following methods will behave differently if the FormField has nested fields

| Property | Details | | ---------- | ---------------------------------------------------------------------------------- | | onChange | It takes an object and will create/update every field from the keys of this object | | hasValue | It is true when all fields have value | | validate | It will run validations for all nested fields | | setTouched | Takes a booleand and will set every nested field touched property to this value | | reset | It resets the value of every nested field | | touched | Will be touched when every fields is touched | | valid | Will be valid when every fields is touched | | dirty | Will be valid when at least one field is dirty | | fields | A mapped collection, which has a FormField for every key in the value |

FieldSet

| Property | Details | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | fields | An array of FormField objects | | onChange | Useful to update many fields at the same time. It takes an array value and will update every field based on the index. If the passed array is longer than the generated items, it will create more | | setTouched | It takes a boolean parameter and will set every item touched property to this value | | reset | It resets every field | | validate | It validates every field | | insert | It takes a comma separated array of arguments and adds a new FormField for each value | | removeField | It removes a field given an object reference value | | value | It returns an array with the value of every FormField item | | dirty | Will be true if any item is dirty | | touched | Will be true if every item is touched |


Development

Install dependencies with yarn install. You can run interactive tests with yarn test and lint your project with yarn lint.

If you work with VSCode, it automatically switches to the correct TS version and formats on save via prettier. There is also an included launch config, so you can run and debug tests.

The VSCode Jest Extension is highly recommended as it gives you inline test results, code coverage and debugging right within VSCode (VSCode will automatically prompt you to install this extension).

Releases are done via GitHub Releases. Create a new release there, this will automatically trigger a workflow to publish the package.

Sandbox App

There is a also a sandbox basic application to play around with the library. Use yarn start to start up the parcel server, and you can find the files inside the /sandbox-app folder.

Troubleshoot

Sometimes you may get the following bug/error from parcel:

Conflicting babel versions found in .babelrc.

To solve this, after you started the dev server, go to the .babelrc file and comment these two lines:

{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"],
  "plugins": [
    // "@babel/plugin-transform-runtime",
    // "babel-plugin-transform-class-properties"
  ]
}

After the server starts correctly you should uncomment those lines. If you still have some issues, try restarting the dev server.