GithubHelp home page GithubHelp logo

wsmd / react-use-form-state Goto Github PK

View Code? Open in Web Editor NEW
937.0 17.0 55.0 917 KB

๐Ÿ“„ React hook for managing forms and inputs state

Home Page: http://react-use-form-state.now.sh

License: MIT License

JavaScript 90.13% TypeScript 9.87%
reactjs react hook react-hooks react-library

react-use-form-state's Introduction


react-use-form-state

Current Release Downloads CI Build Coverage Status Licence

๐Ÿ“– Table of Contents

Motivation

Managing form state in React can be a bit unwieldy sometimes. There are plenty of great solutions already available that make managing forms state a breeze. However, many of those solutions are opinionated, packed with tons of features that may end up not being used, and/or require shipping a few extra bytes!

Luckily, the recent introduction of React Hooks and the ability to write custom hooks have enabled new possibilities when it comes sharing state logic. Form state is no exception!

react-use-form-state is a small React Hook that attempts to simplify managing form state, using the native form input elements you are familiar with!

Getting Started

To get it started, add react-use-form-state to your project:

npm install --save react-use-form-state

Please note that react-use-form-state requires react@^16.8.0 as a peer dependency.

Examples

Basic Usage

import { useFormState } from 'react-use-form-state';

export default function SignUpForm({ onSubmit }) {
  const [formState, { text, email, password, radio }] = useFormState();

  function handleSubmit(e) {
    // ...
  }

  return (
    <form onSubmit={handleSubmit}>
      <input {...text('name')} />
      <input {...email('email')} required />
      <input {...password('password')} required minLength="8" />
      <input {...radio('plan', 'free')} />
      <input {...radio('plan', 'premium')} />
    </form>
  );
}

From the example above, as the user fills in the form, the formState object will look something like this:

{
  values: {
    name: 'Mary Poppins',
    email: '[email protected]',
    password: '1234',
    plan: 'free',
  },
  touched: {
    name: true,
    email: true,
    password: true,
    plan: true,
  },
  validity: {
    name: true,
    email: true,
    password: false,
    plan: true,
  },
  errors: {
    password: 'Please lengthen this text to 8 characters or more',
  },
  clear: Function,
  clearField: Function,
  reset: Function,
  resetField: Function,
  setField: Function,
}

Initial State

useFormState takes an initial state object with keys matching the names of the inputs.

export default function RentCarForm() {
  const [formState, { checkbox, radio, select }] = useFormState({
    trip: 'roundtrip',
    type: ['sedan', 'suv', 'van'],
  });
  return (
    <form>
      <select {...select('trip')}>
        <option value="roundtrip">Same Drop-off</option>
        <option value="oneway">Different Drop-off</option>
      </select>
      <input {...checkbox('type', 'sedan')} />
      <input {...checkbox('type', 'suv')} />
      <input {...checkbox('type', 'van')} />
      <button>Submit</button>
    </form>
  );
}

Global Handlers

useFormState supports a variety of form-level event handlers that you could use to perform certain actions:

export default function RentCarForm() {
  const [formState, { email, password }] = useFormState(null, {
    onChange(e, stateValues, nextStateValues) {
      const { name, value } = e.target;
      console.log(`the ${name} input has changed!`);
    },
  });
  return (
    <>
      <input {...text('username')} />
      <input {...password('password')} />
    </>
  );
}

Advanced Input Options

useFormState provides a quick and simple API to get started with building a form and managing its state. It also supports HTML5 form validation out of the box.

<input {...password('password')} required minLength="8" />

While this covers that majority of validation cases, there are times when you need to attach custom event handlers or perform custom validation.

For this, all input functions provide an alternate API that allows you attach input-level event handlers such as onChange and onBlur, as well as providing custom validation logic.

export default function SignUpForm() {
  const [state, { text, password }] = useFormState();
  return (
    <>
      <input {...text('username')} required />
      <input
        {...password({
          name: 'password',
          onChange: e => console.log('password input changed!'),
          onBlur: e => console.log('password input lost focus!'),
          validate: (value, values, e) => validatePassword(value),
          validateOnBlur: true,
        })}
      />
    </>
  );
}

Custom Input Validation

The example above demonstrates how you can determine the validity of an input by passing a validate() method. You can also specify custom validation errors using the same method.

The input is considered valid if this method returns true or undefined.

Any truthy value other than true returned from this method will make the input invalid. This returned value is used as a custom validation error that can be retrieved from state.errors.

For convenience, empty collection values such as empty objects, empty arrays, empty maps, empty sets are not considered invalidation errors, and if returned the input will be valid.

<input
  {...password({
    name: 'password',

    // can also return objects, arrays, etc, for more complex error objects
    validate: (value, values, event) => {
      if (!value.trim()) {
        return 'Password is required';
      }
      if (!STRONG_PASSWORD_REGEX.test(value)) {
        return 'Password is not strong enough';
      }
    },
  })}
/>

If the input's value is invalid based on the rules specified above, the form state will look similar to this:

{
  validity: {
    password: false,
  },
  errors: {
    password: 'Password is not strong enough',
  }
}

If the validate() method is not specified, useFormState will fallback to the HTML5 constraints validation to determine the validity of the input along with the appropriate error message.

Check If the Form State Is Pristine

useFormState exposes a pristine object, and an isPristine() helper via formState that you can use to check if the user has made any changes.

This can be used for a Submit button, to disable it, if there are no actual changes to the form state:

function PristineForm() {
  const [formState, { text, password }] = useFormState();
  return (
    <div>
      <input {...text('username')} />
      <input {...password('password')} />
      <button disabled={formState.isPristine()} onClick={handleSubmit}>
        Login
      </button>
    </div>
  );
}

Checking if a field is pristine is done with simple equality ===, with some exceptions. This can be overridden per field by providing a custom compare function.

Note that a compare function is required for raw inputs, otherwise, if not specified, the pristine value of a raw input will always be set to false after a change.

<input
  {...raw({
    name: 'userObj',
    compare(initialValue, value) {
      // returns a boolean indicating if the changed value is equal to the initial value
      return isEqualDeep(initialValue, value);
    },
  })}
/>

Without Using a <form /> Element

useFormState is not limited to actual forms. It can be used anywhere inputs are used.

function LoginForm({ onSubmit }) {
  const [formState, { email, password }] = useFormState();
  return (
    <div>
      <input {...email('email')} required />
      <input {...password('password')} required minLength="8" />
      <button onClick={() => onSubmit(formState)}>Login</button>
    </div>
  );
}

Labels

As a convenience, useFormState provides an optional API that helps with pairing a label to a specific input.

When formOptions.withIds is enabled, a label can be paired to an input by using input.label(). This will populate the label's htmlFor attribute for an input with the same parameters.

const [formState, { label, text, radio }] = useFormState(initialState, {
  withIds: true, // enable automatic creation of id and htmlFor props
});

return (
  <form>
    <label {...label('name')}>Full Name</label>
    <input {...text('name')} />

    <label {...label('plan', 'free')}>Free Plan</label>
    <input {...radio('plan', 'free')} />

    <label {...label('plan', 'premium')}>Premium Plan</label>
    <input {...radio('plan', 'premium')} />
  </form>
);

Note that this will override any existing id prop if specified before calling the input functions. If you want the id to take precedence, it must be passed after calling the input types like this:

<input {...text('username')} id="signup-username" />

Custom Controls

useFormState provides a raw type for working with controls that do not use React's SyntheticEvent system. For example, controls like react-select or react-datepicker have onChange and value props that expect a custom value instead of an event.

To use this, your custom component should support an onChange() event which takes the value as a parameter, and a value prop which is expected to contain the value. Note that if no initial value is given, the component will receive a value prop of an empty string, which might not be what you want. Therefore, you must provide an initial value for raw() inputs when working with custom controls.

import DatePicker from 'react-datepicker';

function Widget() {
  const [formState, { raw }] = useFormState({ date: new Date() });
  return (
    <>
      <DatePicker {...raw('date')} />
    </>
  );
}

You can also provide an onChange option with a return value in order to map the value passed from the custom control's onChange to a different value in the form state.

function Widget() {
  const [formState, { raw }] = useFormState({ date: new Date() });
  return (
    <>
      <DatePicker
        {...raw({
          name: 'date',
          onChange: date => date.toString();
        })}
        value={new Date(formState.date)}
      />
    </>
  );
}

Note that onChange() for a raw value must return a value.

Many raw components do not support onBlur() correctly. For these components, you can use touchOnChange to mark a field as touched when it changes instead of on blur:

function Widget() {
  const [formState, { raw }] = useFormState({ date: new Date() });
  return (
    <>
      <CustomComponent
        {...raw({
          name: 'date',
          touchOnChange: true,
        })}
      />
    </>
  );
}

Updating Fields Manually

There are cases where you may want to update the value of an input manually without user interaction. To do so, the formState.setField method can be used.

function Form() {
  const [formState, { text }] = useFormState();

  function setNameField() {
    // manually setting the value of the "name" input
    formState.setField('name', 'Mary Poppins');
  }

  return (
    <>
      <input {...text('name')} readOnly />
      <button onClick={setNameField}>Set Name</button>
    </>
  );
}

Please note that when formState.setField is called, any existing errors that might have been set due to previous interactions from the user will be cleared, and both of the validity and the touched states of the input will be set to true.

It's also possible to clear a single input's value or to reset it to its initial value, if provided, using formState.clearField and formState.resetField respectively.

As a convenience you can also set the error value for a single input using formState.setFieldError.

Resetting The Form State

The form state can be cleared or reset back to its initial state if provided at any time using formState.clear and formState.reset respectively.

function Form() {
  const [formState, { text, email }] = useFormState({
    email: '[email protected]',
  });
  return (
    <>
      <input {...text('first_name')} />
      <input {...text('last_name')} />
      <input {...email('email')} />
      <button onClick={formState.clear}>Clear All Fields</button>
      <button onClick={formState.reset}>Reset to Initial State</button>
    </>
  );
}

Working with TypeScript

When working with TypeScript, the compiler needs to know what values and inputs useFormState is expected to be working with.

For this reason, useFormState accepts an optional type argument that defines the state of the form and its fields which you could use to enforce type safety.

interface LoginFormFields {
  username: string;
  password: string;
  remember_me: boolean;
}

const [formState, { text }] = useFormState<LoginFormFields>();
                                          ยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏยฏ
// OK
<input {...text('username')} />
formState.values.username

// Error
formState.values.doesNotExist
<input {...text('doesNotExist')} />

By default, useFormState will use the type any for the form state and its inputs if no type argument is provided. Therefore, it is recommended that you provide one.

By default, the errors property will contain strings. If you return complex error objects from custom validation, you can provide an error type:

interface I18nError {
  en: string;
  fr: string;
}

interface LoginFormErrors {
  username?: string | I18nError;
  password?: string;
}

const [formState, { text }] = useFormState<LoginFormFields, LoginFormErrors>();

formState.errors.username; // Will be undefined, a string, or an I18nError.

API

import { useFormState } from 'react-use-form-state';

function FormComponent()
  const [formState, inputs] = useFormState(initialState, formOptions);
  return (
    // ...
  )
}

initialState

useFormState takes an optional initial state object with keys as the name property of the form inputs, and values as the initial values of those inputs (similar to defaultValue/defaultChecked).

formOptions

useFormState also accepts an optional form options object as a second argument with following properties:

formOptions.onBlur

A function that gets called upon any blur of the form's inputs. This functions provides access to the input's blur SyntheticEvent

const [formState, inputs] = useFormState(null, {
  onBlur(e) {
    // accessing the inputs target that triggered the blur event
    const { name, value, ...target } = e.target;
  },
});

formOptions.onChange

A function that gets triggered upon any change of the form's inputs, and before updating formState.

This function gives you access to the input's change SyntheticEvent, the current formState, the next state after the change is applied.

const [formState, inputs] = useFormState(null, {
  onChange(e, stateValues, nextStateValues) {
    // accessing the actual inputs target that triggered the change event
    const { name, value, ...target } = e.target;
    // the state values prior to applying the change
    formState.values === stateValues; // true
    // the state values after applying the change
    nextStateValues;
    // the state value of the input. See Input Types below for more information.
    nextStateValues[name];
  },
});

formOptions.onTouched

A function that gets called after an input inside the form has lost focus, and is marked as touched. It will be called once throughout the component life cycle. This functions provides access to the input's blur SyntheticEvent.

const [formState, inputs] = useFormState(null, {
  onTouched(e) {
    // accessing the inputs target that triggered the blur event
    const { name, value, ...target } = e.target;
  },
});

formOptions.onClear

A function that gets called after calling formState.clear indicating that all fields in the form state are cleared successfully.

const [formState, inputs] = useFormState(null, {
  onClear() {
    // form state was cleared successfully
  },
});

formState.clear(); // clearing the form state

formOptions.onReset

A function that gets called after calling formState.reset indicating that all fields in the form state are set to their initial values.

const [formState, inputs] = useFormState(null, {
  onReset() {
    // form state was reset successfully
  },
});
formState.reset(); // resetting the form state

formOptions.validateOnBlur

By default, input validation is performed on both of the change and the blur events. Setting validateOnBlur to true will limit input validation to be only performed on blur (when the input loses focus). When set to false, input validation will only be performed on change.

formOptions.withIds

Indicates whether useFormState should generate and pass an id attribute to its fields. This is helpful when working with labels.

It can be one of the following:

A boolean indicating whether input types should pass an id attribute to the inputs (set to false by default).

const [formState, inputs] = useFormState(null, {
  withIds: true,
});

Or a custom id formatter: a function that gets called with the input's name and own value, and expected to return a unique string (using these parameters) that will be as the input id.

const [formState, inputs] = useFormState(null, {
  withIds: (name, ownValue) =>
    ownValue ? `MyForm-${name}-${ownValue}` : `MyForm-${name}`,
});

Note that when withIds is set to false, applying input.label() will be a no-op.

[formState, inputs]

The return value of useFormState. An array of two items, the first is the form state, and the second an input types object.

Form State

The first item returned by useFormState.

const [formState, inputs] = useFormState();

An object containing the form state that updates during subsequent re-renders. It also include methods to update the form state manually.

formState = {
  // an object holding the values of all input being rendered
  values: {
    [name: string]: string | string[] | boolean,
  },

  // an object indicating whether the value of each input is valid
  validity: {
    [name: string]?: boolean,
  },

  // an object holding all errors resulting from input validations
  errors: {
    [name: string]?: any,
  },

  // an object indicating whether the input was touched (focused) by the user
  touched: {
    [name: string]?: boolean,
  },

  // an object indicating whether the value of each input is pristine
  pristine: {
    [name: string]: boolean,
  },

  // whether the form is pristine or not
  isPristine(): boolean,

  // clears all fields in the form
  clear(): void,

  // clears the state of an input
  clearField(name: string): void,

  // resets all fields the form back to their initial state if provided
  reset(): void,

  // resets the state of an input back to its initial state if provided
  resetField(name: string): void,

  // updates the value of an input
  setField(name: string, value: string): void,

  // sets the error of an input
  setFieldError(name: string, error: string): void,
}

Input Types

The second item returned by useFormState.

const [formState, input] = useFormState();

An object with keys as input types. Each type is a function that returns the appropriate props that can be spread on the corresponding input.

The following types are currently supported:

Type and Usage State Shape
<input {...input.email(name: string)} /> { [name: string]: string }
<input {...input.color(name: string)} /> { [name: string]: string }
<input {...input.password(name: string)} /> { [name: string]: string }
<input {...input.text(name: string)} /> { [name: string]: string }
<input {...input.url(name: string)} /> { [name: string]: string }
<input {...input.search(name: string)} /> { [name: string]: string }
<input {...input.number(name: string)} /> { [name: string]: string }
<input {...input.range(name: string)} /> { [name: string]: string }
<input {...input.tel(name: string)} /> { [name: string]: string }
<input {...input.radio(name: string, ownValue: string)} /> { [name: string]: string }
<input {...input.checkbox(name: string, ownValue: string)} /> { [name: string]: Array<string> }
<input {...input.checkbox(name: string)} /> { [name: string]: boolean }
<input {...input.date(name: string)} /> { [name: string]: string }
<input {...input.month(name: string)} /> { [name: string]: string }
<input {...input.week(name: string)} /> { [name: string]: string }
<input {...input.time(name: string)} /> { [name: string]: string }
<select {...input.select(name: string)} /> { [name: string]: string }
<select {...input.selectMultiple(name: string)} /> { [name: string]: Array<string> }
<textarea {...input.textarea(name: string)} /> { [name: string]: string }
<label {...input.label(name: string, value?: string)} /> N/A โ€“ input.label() is stateless and thus does not affect the form state
<CustomControl {...input.raw(name: string)} /> { [name: string]: any }

Input Options

Alternatively, input type functions can be called with an object as the first argument. This object is used to extend the functionality of the input. This includes attaching event handlers and performing input-level custom validation.

<input
  {...input.text({
    name: 'username',
    validate: value => validateUsername(value),
    validateOnBlur: true,
  })}
/>

The following options can be passed:

key Description
name: string Required. The name of the input.
value: string The input's own value. Only required by the radio input, and optional for the checkbox input.
onChange(e): void Optional. A change event handler that's called with the input's change SyntheticEvent.
onBlur(e): void Optional. A blur event handler that's called with the input's blur SyntheticEvent.
validate(value, values, e): any Optional (required for raw inputs). An input validation function that determines whether the input value is valid. It's called with the input value, all input values in the form, and the change/blur event (or the raw value of the control in the case of .raw()). The input is considered valid if this method returns true or undefined. Any truthy value other than true returned from this method will make the input invalid. Such values are used a custom validation errors that can be retrieved from state.errors. HTML5 validation rules are ignored when this function is specified.
compare(initialValue, value): any Optional (required for raw inputs). A comparison function that determines whether the input value is pristine. It's called with the input's initial value, and the input's current value. It must return a boolean indicating whether the form is pristine.
validateOnBlur: boolean Optional. Unspecified by default. When unspecified, input validation is performed on both of the change and the blur events. Setting validateOnBlur to true will limit input validation to be only performed on blur (when the input loses focus). When set to false, input validation will only be performed on change.
touchOnChange: boolean Optional. false by default. When false, the input will be marked as touched when the onBlur() event handler is called. For custom controls that do not support onBlur, setting this to true will make it so inputs will be marked as touched when onChange() is called instead.

License

MIT

react-use-form-state's People

Contributors

danielbuechele avatar dependabot[bot] avatar derrickbeining avatar dimaqq avatar fabianolothor avatar gcampes avatar gregfenton avatar gregleveque avatar jgierer12 avatar julesblm avatar jwalton avatar maygo avatar wouterraateland avatar wsmd avatar wswoodruff avatar xxczaki avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-use-form-state's Issues

TypeError: raw is not a function

I am using a custom AutoComplete extending react-select like so:
const [formState, { raw, select }] = useFormState({ members: [] });

<AutoComplete ... {...raw({ name: 'members' })} />

And getting the following error:
Error: Uncaught TypeError: raw is not a function Script: http://localhost:3000/static/js/1.chunk.js, Line: 174414, Column: 5 TypeError: raw is not a function at AddToGroupForm (AddToGroupForm.jsx:84)

Is this perhaps user error? I'm trying to get this working with multiSelect - single select with my component and select works just fine, but I seem to need raw in order to capture the changes for multi.

reset form

Hello!
Is there an easier way to cleanup the state? It would be good to have it specially when you submit the form.

Question/Suggestion: "touched" changes

I'd like a live comparison (as I type) of the formState to do things like enable/disable submit buttons, etc, without waiting for a blur event to trigger the actual formState.touched fields. Likewise, a global "isTouched", "isValid" (or similar), and such would be extremely handy - else this will be a super-common function that will need to be done each time.

I'd like to propose one of two options:

  • I submit a PR to the core project
  • I create an "extended" version that wraps your library, with a few quality-of-life adjustments such as those mentioned. It would match 1:1 your library signature, while either extending the formState object, or adding a 3rd extended array element in the return. This is obviously the easier one out of the gate for me, but more fragile, less easy to keep in sync with yours, etc.

That all said, I love what you've done, and the incredibly elegant interface it exposes!

Validate all inputs without touch

So imagine a scenario where a user clicks on the submit button without touching any of the fields, and since formState.validity object is empty, I am not able to verify the fields and also can't show the correct error message for each of them.

I think all of the fields should run the validation method on each render. There also could be a function to validate all the fields so we can use it in submissions.

validity

Are you can explain better in docs how the validity works and how i can customize it?

Custom Input Component

Suppose that I have a custom input component like TagSelector๏ผš

type TagSelectorProps = {
  values: string[]
  onChange: (values: string[]) => any
}

function TagSelector(props: TagSelectorProps) {}

And,

function MyForm() {
  // I think we should expose a 'onChange' method ๐Ÿ™‹โ€โ™‚๏ธ
  // So that we can call `onChange` manually
  const [formState, { onChange }] = useFormState({ tags: [] })

  return (
    <div>
      <TagSelector onChange={onChange} values={formState.values.tags} />
   </div>
  )
}

Using with react-datepicker

There are components like react-datepicker where their onChange handler is not passing the event object as the first argument. For example, in ReactDatepicker's case, the onChange handler is something like: (Date, event) => void.

So when the form handler is trying to access the e.target.value, it's going to throw an error, because target is undefined.

Maybe the ability to override the onChange handler or to manually set a value would resolve this issue.

Can't set error from the onChange handler

I have a form onChange handler that sends the data to a backend. Based on that response, a field may become invalid.

Here is the sample code:

const saveFieldAsync = (name, value) => {
  const req = createRequest(name, value);
  axios(req).catch(e => {
    // I need to update the form state for the invalid field
  });
};

const [formState, formInputs] = useFormState(null, {
  onChange: ({ target: { name, value } }) => saveFieldAsync(name, value),
});

However, in order to do that I need the setFieldError passed to onChange. Technically, this is possible if you don't have a form-wide onChange but you attach manually an onChange handle for each input.

Here is the simplest solution I could come up with:
steliyan@f359f7b

I've tried making the formOptions to accept a function instead of an object, but the code got really ugly. You have a better understanding of the code, I'm pretty sure you can come with something better than me.

And last but not least, congrats on your work on this library! :)

Greetings, Steliyan.

Reset form (not clear form)

Having a reset method which resets (not clears) everything including error, touched etc to the original values.

I pre-fill a form (settings) with values from the db using the default state. The user can change fields but also reset changes. The current clear method clears all the preset values instead of resetting to the original values.

The work around I'm using is to update the key to re-render the form but its a bit slow to re-render... e.g. from #33 https://codesandbox.io/s/x3kznwm6lo

Related to:
#33 (reset form)

Make red on error

So, I wrote some code to make the label red whenever there is an error and the input has been touched:

    const redLabel = (...args) => {
        const field = args.slice(0, args.length - 1)
        const label_text = args[args.length - 1]
        return <label {...label(...field)}><span style={{color: formState.touched[field] && !formState.validity[field] ? 'red' : 'black'}}>{label_text}</span></label>
    }

Not sure if this is something that could be included in the project, but I thought it might be useful.

Here is the full code for context:

import { useFormState } from 'react-use-form-state'
import React from 'react'

export function Form() {
    const [formState, { label, text, email, password, radio }] = useFormState()
    const redLabel = (...args) => {
        const field = args.slice(0, args.length - 1)
        const label_text = args[args.length - 1]
        return <label {...label(...field)}><span style={{color: formState.touched[field] && !formState.validity[field] ? 'red' : 'black'}}>{label_text}</span></label>
    }

    return <form onSubmit={e => e.preventDefault() || console.log(formState)}>
        {redLabel('name', 'Full Name')}
        <input {...text('name')} />
        <br />

        {redLabel('email', "Email")}
        <input {...email('email')} />
        <br />

        {redLabel('password', 'Password')}
        <input {...password('password')} required minLength="8" />
        <br />

        Plan:
        <br />
        
        {redLabel('plan', 'free', 'Free Plan')}
        <input {...radio('plan', 'free')} />
        <br />
        {redLabel('plan', 'free', 'Premium Plan')}
        <input {...radio('plan', 'premium')} />
        <br />
        <button type="submit">Submit</button>
    </form>
}

label tag with problem

label tag is not running like expected.

// This code
<label {...label('name')}>Full Name</label>
<input {...text('name')} />

// Results
<label>Full Name</label>
<input name='name' type='text' value} />

// Expect
<label for='name'>Full Name</label>
<input name='name' type='text' value} />

something is broken ):

first of all, pretty impressive lib, i'm totally hooked (no pun intended) on the API you designed, it really seems flawless.

but i couldn't make it work:

  1. i'm on react and react-dom on v16.7.0-alpha.2
  2. i've tried with or without initial state object
  3. on the moment i add the declaration line, it crashes

do you have any idea of what this could be?

screen shot 2019-01-04 at 2 57 11 am

Request: include useEffect to reset (with new initial data) form on change of initialValue

Example

const MyComponent = () => {
  let [ item, setItem ] = useState({ foo: 'bar' })
  let [ formState ] = useFormState(item)

  setTimeout(() => {
   // this should trigger a component re-render 
   //which will fire a new "item"  value into the useFormState initialValue
   setItem({ fo: 'fum' }) 
  }, 2000)

  return (
    <pre>
     { 
       JSON.stringify(formState, null, 2)
       // this will continue to show the form state as originally 
       // set, instead of updating with new form data
     }
   </pre>
  )
}

Expected Behavior
When changing initialValue (e.g. as the product of another hook), I'd expect the form to reset itself (including untouched state) to the new base data.

Current Behavior
When changing initialValue from another hook, the form is left unchanged. This makes it difficult to dynamically load a form without using component hacks to force a re-instantiation of the useFormState hook

Workaround Hacks

  1. Throwing a useEffect into your component that watches for changes to [what should be] the form initialState, that manually does setFields based on the new object structure. Risks are that if the form structure changes at all, you may carry old data values along with the new (e.g. it may have old fields that were left in the form body because the new form state didn't have those attributes), as well the fact that they'll all be marked as touched instead of untouched.

Steps Forward

  • do not agree that changes to initialValue should be dynamically set form data
  • agree and will modify code to match
  • agree but would welcome a PR (I'd be happy to take a stab at it)

P.S. - @wsmd Love this library and the elegant interface - keep up the great work!

Typescript parameter

First of all: Amazing, tiny lib! No dependencies, native typescript support, this is amazing. Thank you so much! So happy I don't need to code it myself xD ๐Ÿ™โค๏ธ

Small issue with typescript:

import React from 'react'
import { useFormState } from 'react-use-form-state'

type Form = {
	name: string
	password: string
}

const SomeForm: React.FC = () => {

	const [form, { text, password }] = useFormState<Form>()

	const _save = async () => {
		console.log(form)
	}

	return <div>
		<input {...text('name')} /> // This throws type error
		<input {...text({ name: 'name' })} /> // Ok
		<input {...password({ name: 'password' })} />
		<button onClick={_save}>Save</button>
	</div>
}

export default SomeForm

My tsconfig:

{
  "compilerOptions": {
    "target": "ESNEXT",
    "module": "commonjs",
    "jsx": "react",
     "removeComments": true,
    "noEmit": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "alwaysStrict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Values for input.checkbox other than strings

Looks like the only accepted value types for input.checkbox is a string value. Is there a reason for this? Could it be possible to support other values like numbers, objects, or arrays? If not, then maybe it should be documented that value must be a string and to recommend serializing values into a string before hand.

Supported number type for FormValue?

When my form values includes some number, the editor report 'type number is not assignable to string'. The code like this:

type FormValues = { status: number[] }
const [{ values }, { radio }] = useFormState<FormValues>({ status: [0, 1, 2, 3] }) // can not be assigned

The declaration file may not support the number type. Consider about supports, or any other solution?

dynamically add fields

Is it possible to dynamically add fields? If so, would you happen to have an example? If not, it would be a cool enhancement.

Form re-renders all of it's children even if the value hasn't changed

Hi there!

Great job with the lib, however I ran into something when implementing this.

The first this is that I am putting a console.log inside each form element to see how many times they re-render. And let's say I have a form like this:

  • checkbox 1
  • checkbox 2
  • radio 1
  • radio 2
  • radio 3

Every time I click a radio button, all of the elements, including the checkboxes will re-render. Even if I wrap them in React.memo. What's the reasoning behind this?

useFormState API change suggestion

Currently the suggested use of this API is as follows:

  const [formState, { text, email, password, radio }] = useFormState();
  return (
    <form onSubmit={() => console.log(formState)}>
      <input {...text('name')} />
      <input {...email('email')} required />
      <input {...password('password')} required minLength="8" />
      <input {...radio('plan', 'free')} />
      <input {...radio('plan', 'premium')} />
    </form>
  );

What I don't like about this is the duplicated { text, email, password, radio } list. You have to repeat every input type you're using there. When you add or remove inputs in your form you possibly have to edit this list. I think adding or removing inputs should only involve adding/removing the <input> line.

To achieve that we can use the current API as follows:

  const [formState, inputs] = useFormState();
  return (
    <form onSubmit={() => console.log(formState)}>
      <input {...inputs.text('name')} />
      <input {...inputs.email('email')} required />
      <input {...inputs.password('password')} required minLength="8" />
      <input {...inputs.radio('plan', 'free')} />
      <input {...inputs.radio('plan', 'premium')} />
    </form>
  );

At this point, it seems to make sense to combine formState and inputs so that useFormState returns only one object:

  const form = useFormState();
  return (
    <form onSubmit={() => console.log(form.state)}>
      <input {...form.text('name')} />
      <input {...form.email('email')} required />
      <input {...form.password('password')} required minLength="8" />
      <input {...form.radio('plan', 'free')} />
      <input {...form.radio('plan', 'premium')} />
    </form>
  );

This also makes it clear which form each input belongs to, if you happen to have multiple forms within the same component.

I think this would be a nicer API. What do you think?

I know this would be a breaking change so I understand if you're not willing to implement this.

RFC: Add `id` to input props

First off, amazing work on this library, it has now fully replaced Formik for me!

Do you think it would be a good idea (perhaps with an opt-out option) to also set the input's id to the same value as name? I find myself constantly having to add the id in order to make it work with <label for="...">. On the other hand, setting it automatically may clash with other ids the developer wants to use throughout the page.

An alternative solution would be to generate a randomized id and expose it as part of the formState, or as a third top-level return value from useFormState().

If you think this would be a good addition I'd love to submit a PR for it.

Type definition of useFormState requires index signature

With the following:

interface ConnectFormState {
  user: string
  host: string
  password: string
  database: string
  port: number
}

const form = useFormState<ConnectFormState>()

I get:

TS2344: Type 'ConnectFormState' does not satisfy the constraint '{ [key: string]: string | number | string[]; }'.
  Index signature is missing in type 'ConnectFormState'.

I don't want to add an index signature to my form state.

Changing useFormState definition to the following seems to fix this:

export function useFormState<
  // old: T extends { [key: string]: string | string[] | number }
  T extends { [key in keyof T]: string | string[] | number }
>(

And it still disallows properties not of type string | string[] | number e.g. booleans.

Setup formState.validity onMount

Hi,

Love the lib. One enhancement I'd like to suggest if it is possible: it would be nice if there is a mount useEffect(()=>op(), []) that sets up the initial validity values from the HTML5 input.validity for each input('fieldName'). Currently validity values are set/updated when the user touches the input field.

For example, I have an Add/Edit dialog with a couple of required fields. On Add it is an empty initial state, on Edit the fields are pre-populated. I can't see if the formState is valid until the user clicks on each of the fields. I do not want to assume that if the formState.validity value is missing then it is not valid.

Would only work on formState.values.fieldName that have a corresponding input('fieldName'). For example, the Edit has a DB id I'd like to pass into the formState but not use in the form. I prefer not excluding in the initialState and then later add it back when I have to persist the change. So the on mount validity check would ignore this since there is no input('dbId').

Should work with & without a form tag.

Also maybe a formState.isValid boolean value or function that is true when all formState.validity values are true.

[bug] useFormState callback cache causes stale closures

The implementation of useFormState is problematic because it caches callback functions passed to inputs.text et. al. This causes functions like onChange, validate, and onBlur to run with stale closures, namely, the closure with which they are initialised. This can be fixed simply by removing const callbacks = useCache(); from the implementation.

useFormState should updated cache when onChange handler is changed

Here's an example of a component which lets you allocate an SMS number from Twilio. There's an "area code" field and a "country" field. When you change either of these, we send off a fetch call to twilio to get available phone numbers, and then when that call comes back with a list of numbers we set some call setPhoneNumbers to set the list of available phone numbers so you can pick one:

interface NumbersState {
    loading: boolean;
    numbers: apiV2.AvailableSms[] | undefined;
    loadId: number;
}

let nextLoadId = 0;

export function SmsPicker(props: {
    onChange: (value: string) => void;
    onBlur: (value: string) => void;
    value: string;
}) {
    const defaultNumbers = props.value ? [{ number: props.value }] : undefined;

    const [phoneNumbers, setPhoneNumbers] = React.useState<NumbersState>({
        loading: false,
        numbers: defaultNumbers,
        loadId: -1,
    });

    const fetchNumbers = (areaCode: string, country: string) => {
        // Every time we load, bump the `loadId`.  When we type "613" into
        // the area code box, we'll actually send a request for "6", then
        // another for "61" and finally a last one for "613".  When the
        // request for "6" comes back, we don't want to call
        // `setPhoneNumbers({ loading: false })`, because then while the
        // user is trying to pick a number, the list could change on them.
        // Instead, we want to keep showing a spinner until the "613" reply
        // comes in and just ignore the "stale" replies.

        const loadId = nextLoadId++;
        setPhoneNumbers({ loading: true, numbers: defaultNumbers, loadId });

        Promise.resolve()
            .then(async () => {
                // Not the real twilio API call.  :P
                const numbers = await twilio.getAvailableNumbers({
                    country,
                    areaCode,
                });

                if (phoneNumbers.loadId === loadId) {
                    setPhoneNumbers({
                        loading: false,
                        numbers: numbers.concat(defaultNumbers || []),
                        loadId,
                    });
                }
            })
            .catch(err => {
                // Blah blah blah
            });
    };

    const [formState, { text, raw }] = useFormState<{
        country: string;
        areaCode: string;
        selectedNumber: string;
    }>(
        {
            country: 'US',
            areaCode: '',
            selectedNumber: '',
        },
        {
            onChange(_e, oldValues, nextValues) {
                const { areaCode, country } = nextValues;
                if (areaCode !== oldValues.areaCode || country !== oldValues.country) {
                    fetchNumbers(areaCode, country);
                }
            },
        }
    );

    return (
        <section>
            <label>
                Country:
                <ReactSelect
                    options={COUNTRY_CODE_OPTIONS}
                    isMulti={false}
                    {...raw<string>({
                        name: 'country',
                        validate: value => value ? undefined : 'Required',
                    })}
                />
            </label>

            <label>
                Area Code:
                <input {...text('areaCode')} />
            </label>

            <label>
                SMS Number:
                {phoneNumbers.loading ? (
                    <Spinner />
                ) : phoneNumbers.numbers ? (
                    /* A phone number picker goes here - react-select or something... */
                ) : (
                    'Enter a Country and Area Code to search for a number'
                )}
            </label>
        </section>
    );
}

The problem is... this doesn't work. :(

The onChange option we pass into useFormState gets cached, so even though we pass a different onChange later, the new onChange gets ignored. onChange relies on fetchNumbers, which relies on phoneNumbers. This means every time we call into onChange, it calls the cached version with the cached fetchNumbers and the cached phoneNumbers, so every time fetchNumbers tries to do this bit:

if (phoneNumbers.loadId === loadId) {

phoneNumbers.loadId will always be -1.

We could improve on this by using useCallback() to make it so onChange and fetchNumbers only update when phoneNumbers changes, but if the onChange we pass into useFormState() changes, then useFormState() should probably use the new onChange handler instead of using the cached one.

[feature request] Array field support

Let's say you have a field that is named: Requests:

type Requests = Array<{url: string, name: string}>

Is there a way to do something like this natively with the lib?

Of course, there are workarounds using 2 levels of formState:

  • Create one formState per row and add a global onChange listener to change the formState above
  • Use raw so you can trigger onChange manually for the above formState - this way you lose local validation

I feel the lib could support these cases natively

Thanks in advance

npm update or install not updating

npm i -D react-use-form-state

results

+ [email protected]
updated 1 package and audited 37440 packages in 61.758s
found 63 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details

Not updating do v0.8.0.

About validate input validation function

validate(value: string, values: StateValues): boolean

Optional. An input validation function that gets passed the input value and all input values in the state. It's expected to return a boolean indicating whether the input's value is valid. HTML5 validation rules are ignored when this function is specified.

I was interested in this function as a way to determine the origin of the validation error. Right now with a boolean flag it's not possible to distinguish.

Maybe extend the validity shape like so:

"validity": {
    "name": {
         someValidationRule: true|false,
         otherValidationRule: true|false
    }
}

Thank you for the great lib :)

[bug] setField() doesn't invoke formState's onChange() method

I try to update field manually and subscribe to onChange() method in formState object, but after I apply setField() the values of formState iteself is changing and onChange() doesn't executes

const [formState, { text, radio, label }] = useFormState<IProfilesFilter>(params, {
    onChange(
      event: React.ChangeEvent<InputElement>,
      old: StateValues<IProfilesFilter>,
      newValues: StateValues<IProfilesFilter>,
    ) {
      event.preventDefault();
      console.log(newValues);
    },
  });

  const handleSelect = (newValue: string): void => {
    formState.setField("selected", [...selectedItems, newValue]);
  };

Setting state values manually

First of all, thanks a lot for this amazing work, @wsmd ! Huge time saver, very performant and extremely clean API, you really nailed it ๐Ÿ˜„

The only issue I have with this hook is the fact that you only pass down formState.current through the hook, meaning we're locked if we need to set state manually.

I'm creating a form in which I ask for an URL, fetch its meta title via a cloud function and then return it to automatically add it to the title input (which can also be edited by the user). I can run the fetchUrl through the onBlur option inside the URL input, but then I have no means of updating the title inside formState ๐Ÿ˜”

Is there any way we could return the whole formState object (in v.1) in the hook or is it too dangerous / wouldn't work? It'd be something like this:

// inside useFormState.js
return [
    formState,
    {
      ...inputPropsCreators,
      [LABEL]: (name, ownValue) => getIdProp('htmlFor', name, ownValue),
    },
  ];

// inside YourComponentForm.jsx
const [{ current, setValues }, input] = useFormState()

For the time being, I'll manage the title input outside of useFormState, unfortunately ๐Ÿ˜ญ

Validate on submit

Thank for creating this fantastic library! I'd like to suggest adding the ability to trigger validation of the form. Specifically when a form is submitted you'd typically want to indicate to the user if he/she is missing some required fields (and maybe there's other stuff to be validated too). Currently if the user hasn't touched a field and tries to submit, that field won't be validated.

A simple and flexible solution would be to expose a method which could be called to trigger validation. Maybe that has some overlap with #52.

onSubmit check

Consider an email/password form. I type in the email, TAB, type in the password, ENTER.
We get this state from onSubmit callback:
submit
Which is logical since pressing ENTER doesn't trigger a blur event. Is it a good idea to let users implement this themselves instead of just being able to write:

  const hasClientErrors = !all(v => v === true, values(formState.validity))
  const onSubmit = () => {
    if (hasClientErrors) return
    onSubmit()
  }

EDIT: actually it's about using onKeyPress prop on controls so we can trigger validity check on ENTER. Maybe it would be nice to have it included with props builders <input {...email('email')} />

<select multiple> support

Either I'm missing the explanation on how to do this, but it seems as though it's currently not possible to create a select form element with a multiple attribute. Maybe an additional input.selectMultiple function should be added?

Is redux supported? How?

I'm trying to use this library in the following context:

  • there's a record that has many fields
  • I'm displaying a form with inputs for these fields
  • user would be editing some fields
  • async refresh process may change (other or same) field data

I wanted to get to the point where:

  • pristine fields would be updated only by async process
  • touched fields would be updated only by user

I figured I'd try redux, with data model like so:

// store state
{ record: {f1: "a", f2: "b", ...},
  overlay: {f2: "bbb", ...}}
// map-to-props
displayed = {...record, ...overlay}

At the moment I can't figure out how to marry react-use-form-state API to redux:

  • onChange is pre-field, and happens before state is changed
  • issuing redux action directly from formState gives me a react warning*

(*)

Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

Am I missing something?
Is this library meant to keep data in component state only?

Can't find setFieldError method in formState

ver 0.10.4

hello, i had to set error at particular situation. so i read Doc and found setFieldError(setFieldError(name: string, error: string): void) Function.
but i couldn't find setFieldError method in the code

can you check for me? my version is resent

send InitialState in the input

Hi, I am using apollo-client and I don't know how to update the initialState of the hook when is inside the Query component, there is a way to do that?

...
const StartService: React.FunctionComponent<Props> = props => {
  const [formState, { text, select }] = useFormState();

  const withId = props.id;

  const result = {};

  return (
    <User>
      {({ data, loading }) => {
        const { me } = data || {};
        if (loading) return <Loading />;
        return (
          <Query
            query={GET_SERVICE_QUERY}
            variables={{ id: withId }}
            skip={!withId}
          >
            {({ data }) => {
              const { service } = data || {};
              if(service) {
                // Here I have the data to send to the initialState
              }
...

Btw I really like this project, very lightweight, thanks a lot!
A newbie

Question: access to formState in validate methods

Hi, I'm trying to validate two custom input fields for the start time and end time. The start time shouldn't be later than the end time and vice versa. When perform custom input validation, I can easily access the values of other input fields, but I cannot get information about their validation status.

This code gives the very first value of a formState object that's not updated:

function Form(eventData) {
  const [formState, { raw }] = useFormState({
    start: eventData.startTime,
    end: eventData.endTime,
  });
  return <>
    <TimePicker 
      {...raw({
        name: 'start',
        validate: (value, values) => {
          if (formState.validity.end !== false) { // <-- validity always undefined
            // ...
          } else {
            // ...
          }
        }
      )} 
    />
    {/* second TimePicker */}
  </>
}

It seems that the assigned validate function relies on closure from the first component render. Is there a workaround for getting the most recent formState object state? Thanks!

Improve types for BaseInputProps

The type definition for BaseInputProps types the name as string. However, we could type this more strictly: https://github.com/wsmd/react-use-form-state/blob/master/src/index.d.ts#L135

-interface BaseInputProps {
+interface BaseInputProps<T> {
  id: string;
  onChange(event: any): void;
  onBlur(event: any): void;
  value: string;
-  name: string;
+  name: keyof T;
  type: string;
}

I would create a PR, but the version published to NPM differs from the one here on GitHub, so I am not sure if all changes are yet pushed to GitHub

Trigger all validation rules at once

Hi,

At this moment, user needs to visit a field at least once or that would not be reported as an error. How could i manually perform a validation on all fields before/after render?

My Bests,
Hung Tran

[bug] validate called with undefined on blur for raw (and perhaps other) inputs

version 0.10.0

I believe I've encountered a bug and have opened a corresponding pull request with a potential solution (see #73).

Basically, for raw, I'm finding that my validate function is being invoked with undefined on blur, even when I set validateOnBlur to false, which is causing run-time errors.

So the problem appears to be two-fold.

  1. validate shouldn't be getting undefined passed to it
  2. validate shouldn't be called on blur when validateOnBlur is false (the default setting, I believe).

Please review my PR whenever you have a moment.

type safety for Inputs?

on large forms, it's easy to mess up when dealing with strings that are namespaced (like address.street.lat), would be nice that the Inputs declaration had a generic Inputs<T> so it can accept the name as being part of the "keyof T" in useFormState, so [FormState<T>, Inputs] -> [FormState<T>, Inputs<T>]

Extend `FormOptions` to receive global `validateOnBlur` value

It'd be useful to set a global validateOnBlur method, validating on every input makes code a little noisy.

By the way, the real problem is that I am having some perf issues on a big form and adding validateOnBlur is making the form much lighter.

I can try to reprocude in a code sandbox and open another issue but nevertheless a global validateOnBlur would make sense for the lib I think

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.