Giter Club home page Giter Club logo

use-form's Introduction

another-use-form-hook

A React hook ๐ŸŽฃ for easy form handling


Build Status Code Coverage version downloads Total downloads MIT License PRs Welcome Code of Conduct

Watch on GitHub Star on GitHub


Table of Contents

Installation

This should be installed as one of your project dependencies:

yarn add another-use-form-hook

or

npm install --save another-use-form-hook

NOTE: another-use-form-hook only works with react >=16.8, since it is a hook.

Usage

This hook is intended to give a full solution for handling forms. From interdependent field value validations (meaning if a field value is dependent on other field value), to submitting the form, and providing information about when the UI should be unresponsive (loading of some kind of async-like operation), in addition to notification "hooks" to be able to inform the users the most efficient way.

To retrieve props for an input field, you have the following options:

  1. Using the form.inputs.{inputType}('name') input prop generator function (inputTypes is one of these)
  2. Using form.fields.{name}.{value|error} and form.handleChange functions

NOTE: The example below is available live at CodeSandbox

Let's see a complex example to understand how it works:

import React from "react";
import ReactDOM from "react-dom";
import useForm from "another-use-form-hook";
import "./styles.css";
/**
 * NOTE: We are using date-fns for this example,
 * but it is absolutly not a requirement.
 */
import { addDays, isAfter, differenceInDays, parseISO } from "date-fns";
const isValidEmail = email => /\w*@\w*\.\w*/.test(email); // Do better ๐Ÿ’ฉ

const TODAY = new Date();

const App = () => {
  const form = useForm({
    initialState: {
      email: "",
      arrival: TODAY,
      departure: TODAY
    },
    validators: (fields, isSubmitting) => ({
      // if not submitting, don't throw errors for invalid e-mail, like an empty field
      email: isSubmitting
        ? isValidEmail(fields.email)
        : typeof fields.email === "string",
      // earliest arrival must be tomorrow
      arrival: isAfter(parseISO(fields.arrival), TODAY),
      // earliest departure must be after tomorrow
      departure: isAfter(addDays(parseISO(fields.departure), 1), TODAY),
      // departure must be at least a day after arrival
      minOneNight: isSubmitting
        ? differenceInDays(
            parseISO(fields.departure),
            parseISO(fields.arrival)
          ) >= 1
        : true
    }),
    onNotify: (type, reason) => {
      // you can use type and reason to send specific notifications to the user
      switch (type) {
        case "submitError":
          console.error("Form could not be submitted: ", reason);
          break;
        case "submitSuccess":
          console.info("Form has been submitted.", reason);
          break;
        case "validationErrors":
          console.warn(
            "The following problems occurred while validating: ",
            reason
          );
          break;
        default:
          break;
      }
    },
    onSubmit: async ({ fields, setLoading, notify }) => {
      try {
        setLoading(true);
        // submitting the form, eg.: fetch("path/to/my/submit")
        const response = await new Promise(resolve => {
          setTimeout(() => {
            console.log("Submitting: ", fields);
            resolve(fields);
          }, 1000);
        });
        notify("submitSuccess", response);
      } catch (error) {
        notify("submitError", error.message);
      } finally {
        setLoading(false);
      }
    }
  });

  return (
    <form onSubmit={form.handleSubmit}>
      {/* option 1 (control all the props with a one-liner)*/}
      <fieldset>
        <legend>Option 1</legend>

        <label htmlFor="email">
          {form.fields.email.error ? "Invalid" : ""} email
        </label>
        <input {...form.inputs.email("email")} />

        <label htmlFor="departure">
          {form.fields.arrival.error ? "Invalid" : ""} arrival
        </label>
        <input
          {...form.inputs.date("arrival")}
          // You can override props by simply defining them last
          onChange={e => form.handleChange(e, ["minOneNight"])}
        />
      </fieldset>

      {/* option 2 specify id, type, name, value props manually*/}
      <fieldset>
        <legend>Option 2</legend>
        <label htmlFor="arrival">
          {form.fields.arrival.error ? "Invalid" : ""} arrival
        </label>
        <input
          type="date"
          id="arrival"
          name="arrival"
          value={form.fields.arrival.value}
          onChange={e => form.handleChange(e, ["minOneNight"])}
        />

        <label htmlFor="departure">
          {form.fields.departure.error ? "Invalid" : ""} departure
        </label>
        <input
          type="date"
          id="departure"
          name="departure"
          value={form.fields.departure.value}
          onChange={e => form.handleChange(e, ["minOneNight"])}
        />
      </fieldset>

      {/* also from option 1 */}
      <button {...form.inputs.submit()} disabled={form.loading}>
        Submit
      </button>
    </form>
  );
};

ReactDOM.render(<App />, document.querySelector("#root"));

Documentation

useForm

  useForm(useFormParams: UserFormParams): UseForm

UseFormParams

name type description
name string Refer to one of the forms in formProviderProps
initialState object See InitialState
validators function See Validators
onSubmit function See SubmitCallback
onNotify function See NotifyCallback
InitialState

An object containing the default value of every field in a form.

Example:

useForm({
  initialState: {
    email: "",
    name: "",
    address: "",
    age: 0
  }
})

If name is defined, you can refer to initialStates.{name} in formProviderProps.

Example:

//...
<FormProvider 
  initialStates={{
    login: {
      email: "[email protected]",
      password: ""
    }
  }}
>
//...
const form = useForm({name: "login"})
console.log(form.fields.email.value) // [email protected]
Validators

This function is invoked before onChange and onSubmit. The former only runs the validations for the changed fields, while the latter runs it on the whole form. For convenience, it is also returned from useForm.

function validators(fields: object, isSubmitting: boolean): Validations
name type description
fields object An object with the same shape as initialState
submitting boolean Set to true, when called before handleSubmit
validations object See Validations
Validations

An object containing boolean expressions. Each input field must have at least a corresponding property in this object, but you can define custom ones as well.

Example:

{
  email: submitting
    ? isValidEmail(fields.email)
    : typeof fields.email === "string"
}

You can also look at the live example.

SubmitCallback

Invoked when handleSubmit is called and there were no validation issues.

function onSubmit(onSubmitParams: OnSubmitParams): void
name type description
name string Same as name in useFormParams
fields object Validated fields, same shape as initialState
setLoading function Sets the returned loading property of useForm
notify function See NotifyCallback
NotifyCallback

Invoked if there is a validation error when calling handleChange or handleSubmit. Can be manually triggered on onSubmit by calling notify.

type NotifyType = "validationErrors" | "submitError" | "submitSuccess"
function notify(type: NotifyType, reason: any): void
name type description
type string Type of notification
reason any When type is validationErrors, it is a list of field names, Otherwise you set it to whatever you want.

Example:

Look at the live example.

UseFormReturn

name type description
name string Same as in useFormParams.
fields object See FieldValuesAndErrors
hasErrors boolean For convenience. true if any of the returned fields.{name}.error is true.
handleChange function See ChangeHandler
handleSubmit function See SubmitHandler
loading boolean Controlled by setLoading in onSubmit
inputs object See InputPropGenerators
validators function See Validators
FieldValuesAndErrors

Validated field values and their errors.

Example:

  const form = useForm({initialState: {
    email: "[email protected]",
    age: -2
  }})
  console.log(form.fields)
  // {
  //  email: {
  //   value: "[email protected]",
  //   error: false
  //  },
  //  age: {
  //   value: -2,
  //   error: truefields
  //  }
  // }
  console.log(form.hasErrors) // true
ChangeHandler

You can call this two ways. Either pass an event as the first argument, or a partial fields object. With the latter, you can change multiple values at the same time. E.g.: resetting the form after submit, or any other reason you might have.

function handleChange(event: React.FormEvent, validations: string[]): void
function handleChange(fields: object, validations: string[]): void
name type description
event React.FormEvent Standard event. Using target.{name|value|checked} to infer the intention
fields object Pass a partial fields object, if you want to change multiple values at the same time
validations string[] Which validators you would like to run. If omitted, only validators with the same event/field name will be run

Example:

Look at the live example.

SubmitHandler

Call to submit the form. Before onSubmit is invoked, validators is run for every form field. If there were any errors, notify is invoked with type being validationErrors, and reason a list of form field names.

function handleSubmit(): void
InputPropGenerators

An object, containing properties with the same name as the HTML input types, with some minor differences.

For convenience, since datetime-local contains a hyphen (-) character, it is also exposed as datetimeLocal, to overcome the need of " characters, when accessing it. I.e.:

const form = useForm()
form.inputs.datetimeLocal == form.inputs["datetime-local"]

In addition to the standard input types, there is a select type also available.

Each property is a function:

function inputType(name: string, options: InputTypeOptions): InputPropGeneratorsReturn
name type description
name string The name of a field. Same as the properties of initialState
options object See InputTypeOptions

Example:

For examples of all types, you can check this test suite

InputTypeOptions

An optional object

name type description
value string Usually, when using radio buttons, values are static. (Each button in the same group must have different values)
generateProps function Provides name, value and error that can be used to generate additional props. Useful, if you want to avoid using form.fields
formName string If the input type is submit, it can be used to override the name of the form being submitted.

Example:

const form = useForm(/*...*/)
// *click on option-2*
console.log(form.fields.radio.value) // option-2
return (
  <radiogroup>
    <input {...inputs.radio('radio', { value: 'option-1' })}/>
    <input {...inputs.radio('radio', { value: 'option-2' })}/>
    {/*You can do it the "traditional" way also*/}
    <input
      {...inputs.radio('radio')} 
      value='option-3'
    />
  </radiogroup>
)
const form = useForm(/*...*/)
return(
  <div>
    <input
      {...form.inputs.email("emailField", {
        generateProps: ({error}) => ({
          className: error ? "email-error" : "email",
        })
      })}
    />
    {/*Tip: if your custom Input component takes an error prop, you can try this: */}
    <Input {...form.inputs.email("emailField", {generateProps: _ => _})}/>
    {/* This will spread error to Input as well.*/}

    {/*Or here is a more complex example for a custom Input component*/}
    <Input 
      {...form.inputs.email("emailField", {
        generateProps: ({error, name}) => ({
          error,
          label: error ? `Invalid ${name}` : name,
          placeholder: `Type ${name}`
        })
      })}
    />
  </div>
)
InputPropGeneratorsReturn

An object that can be spread on a React input like element.

name type description
name string The name of a field. Must be one of the properties in initialState
id string By default, same as name. If input type is radio, it is the same as value, to avoid problems in radio groups where id would be the same
value any The value of the field
onChange function See onChange
checked boolean If input type is checkbox, it is the same as value
onClick function If input type is submit, it is onSubmit

Example:

const form = useForm(/*...*/)

return (
  <div>
    <label htmlFor="emailField">E-mail</label>

    <input {...form.inputs.email("emailField")}/>
    {/* This is the same as */}
    <input
      name="emailField"
      id="emailField"
      type="email"
      onChange={form.handleChange}
      value={form.fields.emailField.value}
    />
  </div>
)

FormProvider

  function FormProvider(formProviderProps: FormProviderProps): JSX.Element

FormProviderProps

name type description
initialStates object Single place to define all initialState for every form. See InitialState
validators object Single place to define all validators for every form. See Validators
onSubmit function Same as onSubmit
onNotify function Same as onNotify
children ReactElement The element you would like to wrap with FormProvider

getForms

  function getForms() : Forms

Forms

An object containing all the forms' current values in FormProvider. Same shape as initialStates.


LICENSE

MIT

use-form's People

Contributors

balazsorban44 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

use-form's Issues

Reset state after a successfull form submission

Is your feature request related to a problem? Please describe.
When a user fills out a form and submits it, it is often desired that all the input fields are cleared, and the values are set back to initial/default values

Describe the solution you'd like
Such a functionality can be baked into onSubmit:

1. Provide a simple reset() function parameter, that takes no arguments, and reuses the useForm#initialState value.

const form = useForm({
  //...
  // Whatever is set here is the new value of `form.fields`, after `reset()` is called
  // If `initialState` is set async (meaning it's value changed over time), the last value will be used.
  initialState: {
    email: ""
  },
  async onSubmit({fields, reset}) {
    try {
      //... submit your form here
      reset() // when the form was submitted successfully, you can call reset() to clear all the input fields
    } catch(e) {
       // if the submission failed, handle the error here
    } 
  }
})

2. Alternatively, you could take an object as an argument, for partial resetting:

This way, the user can selectively reset certain fields, but it also introduces more complexity, while in most cases, a simple reset would be enough.

const initialState = {
  email: "",
  name: ""
}
const form = useForm({
  //...
  initialState,
  async onSubmit({fields, handleChange}) {
   try {
      //... submit your form here
      handleChange({ // when the form was submitted successfully, you can call handleChange to change any fields you would like.
        ...initialState,
        email: fields.email // Prevents the e-mail field to be cleared
      })
    } catch(e) {
       // if the submission failed, handle the error here
    }
  }
})

3. useForm() could return a state property, that is set to one of these values:

type FormState = "validationErrors" | "submitPending" | "submitError" | "submitSuccess"

This could replace/accompany useForm()#loading, as a good practice:
https://kentcdodds.com/blog/stop-using-isloading-booleans

This keeps the useForm() API clean and lower level, while also solving other potential issues by being able to track the precise state of the form at all times.

For example, useForm comes with a built-in notification handling, which may add unnecessary complexity to the API. With a simple state property, the user could simply handle notifications themselves.

Describe alternatives you've considered
A workaround with today's API could look like this:

// Option 3. could simplify this, by not having to set the `submitted` state manually.
const initialState = {
  email: "",
} // This must either be defined outside of the component or be wrapped in `React.useMemo` to be reuseable in the `useEffect code below`
const [submitted, setSubmitted] = React.useState(false)
const form = useForm({
  //...
  initialState,
  async onSubmit({fields, handleChange}) {
   try {
      //... submit your form here
      setSubmitted(true) // when the form was submitted successfully, set a submitted state
    } catch(e) {
       // if the submission failed, handle the error here
    }
  }
})
React.useEffect(() => {
  if(submitted) {
    handleChange(initialState)
    setSubmitted(false)
  }
}, [submitted, handleChange, initialState]) // You would only need to add initialState, if it was defined inside the component

UPDATE:
After an internal discussion, I landed on option 3, as it has a real potential to simplify the API by getting rid of notification handling.

Plz fix checkbox support!

  case 'checkbox': {
    const _onChange = (e) => {
      // TODO: update useForm
      onChange({ [name]: e.target.checked })
    }
    console.log(value.value)
    return (
      <FormControlLabel
        control={
          <Checkbox
            checked={value.value}
            name={name}
            onClick={_onChange}
          />}
        label={t(`${name}.label`)}
      />
    )
  }

add documentation ๐Ÿ“

  • code documentation
    • typings
    • prepare deprecation warnings for next major release
    • API reference in README
  • usage example(s) in README

Migrate to TypeScript

Is your feature request related to a problem? Please describe.
It feels like TypeScript is the de facto type safe method to write JavaScript today. Supporting it that way instead of declaration files feels like the right move.

Describe the solution you'd like
Converting the whole codebase will take some effort. Current feature additions will be stopped to avoid conflicts, but since there are declaration files already available, it should not be such a complicated task. I am considering TSDX for compilation, as I have some experience with it.

Describe alternatives you've considered
When I created this package, I just started learning about TypeScript, and at the time it seemed like a way too big of a commitment to write the whole package that way. Since than I have worked on TS projects enough to have the confidence to make that change.

investigate optimization possibilities for objects as hook dependencies

Internally, handleChange and/or handleSubmit uses fields (an object, containing all the values of a form), and since they are not deep compared, it leads to unoptimized behavior in those functions. A (temporary) solution for the problem could be similar to use-deep-compare-effect, but it is generally recommended to avoid deep comparisons in hooks. I will probably include a simple JSON.stringify(fields) kind of solution for now, but I feel that something more elegant is needed in the long run.

I am open to suggestions! ๐Ÿ™

new API for input props

My idea is to introduce a new way to connect inputs to the useForm hook.

Here is a comparison of the current and the future version:

const form = useForm({
  name: "form",
  validators: {
    email: ({email}) => // Your magical ๐Ÿง™ email validator
  }
})

return (
  <>
    <label htmlFor="email">
-      { form.fields.email.error ? "Invalid" : "" } email
+      { form.errors.email ? "Invalid" : "" } email
    </label>
    <input
-      id="email"
-      name="email"
-      value={form.fields.email.value}
-      onChange={form.handleChange}
+      { ...form.propsFor("email") }
    />
  </>
)

As you can see, it could reduce the amount of repetitive code, without losing any of the functionality.

propsFor(name, type, validations) would take three arguments:

  • name: being the only required one.
  • type: referring to the input types. If not defined, it will be inferred from the name you have given and it will use text as default
  • validations: an array of strings and each of them refers to a function in validators, that is given when registering the useForm hook. It has the same purpose as form.handleChange's second argument.

If you want more control, let's say you want a custom event handler for onChange, you could do it easily like this:

<input   
  { ...form.propsFor("email") }
  onChange={myCustomHandleChange}
/>

Note the order of the props. By defining onChange under propsFor, you will override the built-in version, and will have full control. Don't forget to propagate the actual changes to useForm inside myCustomHandleChange though. (For example, you could call form.handleChange(e) somewhere inside myCustomHandleChange)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.