0.19.1 • Published 2 years ago

react-form-with-constraints v0.19.1

Weekly downloads
477
License
MIT
Repository
github
Last release
2 years ago

react-form-with-constraints

npm version Build Status codecov gzip size

Simple form validation for React

Check the changelog for breaking changes and fixes between releases.

Introduction: what is HTML5 form validation?

⚠️ Client side validation is cosmetic, you should not rely on it to enforce security

<form>
  <label for="email">Email:</label>
  <input type="email" id="email" required>
  <button>Submit</button>
</form>

input required input type="email"

The required HTML5 attribute specifies that the user must fill in a value, type="email" checks that the entered text looks like an email address.

Resources:

What react-form-with-constraints brings

  • Minimal API and footprint
  • Unobtrusive: easy to adapt regular React code
  • Control HTML5 error messages: <FieldFeedback when="valueMissing">My custom error message</FieldFeedback>
  • Custom constraints: <FieldFeedback when={value => ...}>
  • Warnings and infos: <FieldFeedback ... warning>, <FieldFeedback ... info>
  • Async validation
  • No dependency beside React (no Redux, MobX...)
  • Re-render only what's necessary
  • Easily extendable
  • Support for React Native with npm package react-form-with-constraints-native
  • Bootstrap 4 styling with npm package react-form-with-constraints-bootstrap4
  • Material-UI integration with npm package react-form-with-constraints-material-ui
  • ...
<input type="password" name="password"
       value={this.state.password} onChange={this.handleChange}
       required pattern=".{5,}" />
<FieldFeedbacks for="password">
  <FieldFeedback when="valueMissing" />
  <FieldFeedback when="patternMismatch">
    Should be at least 5 characters long
  </FieldFeedback>
  <FieldFeedback when={value => !/\d/.test(value)} warning>
    Should contain numbers
  </FieldFeedback>
  <FieldFeedback when={value => !/[a-z]/.test(value)} warning>
    Should contain small letters
  </FieldFeedback>
  <FieldFeedback when={value => !/[A-Z]/.test(value)} warning>
    Should contain capital letters
  </FieldFeedback>
</FieldFeedbacks>

Examples

Other examples inside the examples directory.

How it works

The API works the same way as React Router v4:

<Router>
  <Route exact path="/" component={Home} />
  <Route path="/news" component={NewsFeed} />
</Router>

It is also inspired by AngularJS ngMessages.

If you had to implement validation yourself, you would end up with a global object that tracks errors for each field. react-form-with-constraints works similarly. It uses React context to share the FieldsStore object across FieldFeedbacks and FieldFeedback.

API

The API reads like this: "for field when constraint violation display feedback", example:

<FieldFeedbacks for="password">
  <FieldFeedback when="valueMissing" />
  <FieldFeedback when="patternMismatch">Should be at least 5 characters long</FieldFeedback>
</FieldFeedbacks>
for field "password"
  when constraint violation "valueMissing"    display <the HTML5 error message (*)>
  when constraint violation "patternMismatch" display "Should be at least 5 characters long"

(*) element.validationMessage

Async support works as follow:

<FieldFeedbacks for="username">
  <Async
    promise={checkUsernameAvailability} /* Function that returns a promise */
    then={available => available ?
      <FieldFeedback key="1" info style={{color: 'green'}}>Username available</FieldFeedback> :
      <FieldFeedback key="2">Username already taken, choose another</FieldFeedback>
      // Why key=*? Needed otherwise React gets buggy when the user rapidly changes the field
    }
  />
</FieldFeedbacks>

Trigger validation:

class MyForm extends React.Component {
  async handleChange(e) {
    const target = e.target;

    // Validates only the given fields and returns Promise<Field[]>
    const fields = await this.form.validateFields(target);

    const fieldIsValid = fields.every(field => field.isValid());
    if (fieldIsValid) console.log(`Field '${target.name}' is valid`);
    else console.log(`Field '${target.name}' is invalid`);

    if (this.form.isValid()) console.log('The form is valid');
    else console.log('The form is invalid');
  }

  async handleSubmit(e) {
    e.preventDefault();

    // Validates the non-dirty fields and returns Promise<Field[]>
    const fields = await this.form.validateForm();

    // or simply this.form.isValid();
    const formIsValid = fields.every(field => field.isValid());

    if (formIsValid) console.log('The form is valid');
    else console.log('The form is invalid');
  }

  render() {
    return (
      <FormWithConstraints
        ref={formWithConstraints => this.form = formWithConstraints}
        onSubmit={this.handleSubmit} noValidate
      >
        <input
          name="username"
          onChange={this.handleChange}
          required minLength={3}
        />
        <FieldFeedbacks for="username">
          <FieldFeedback when="tooShort">Too short</FieldFeedback>
          <Async
            promise={checkUsernameAvailability}
            then={available => available ?
              <FieldFeedback key="1" info style={{color: 'green'}}>Username available</FieldFeedback> :
              <FieldFeedback key="2">Username already taken, choose another</FieldFeedback>
            }
          />
          <FieldFeedback when="*" />
        </FieldFeedbacks>
      </FormWithConstraints>
    );
  }
}
  • FieldFeedbacks

    • for: string => reference to a name attribute (e.g <input name="username">), should be unique to the current form
    • stop?: 'first' | 'first-error' | 'first-warning' | 'first-info' | 'no' => when to stop rendering FieldFeedbacks, by default stops at the first error encountered (FieldFeedbacks order matters)

    Note: you can place FieldFeedbacks anywhere, have as many as you want for the same field, nest them, mix them with FieldFeedback... Example:

    <input name="username" ... />
    
    <FieldFeedbacks for="username" stop="first-warning">
      <FieldFeedbacks>
        <FieldFeedback ... />
        <Async ... />
        <FieldFeedbacks stop="first-info">
          ...
        </FieldFeedbacks>
      </FieldFeedbacks>
    
      <FieldFeedback ... />
      <Async ... />
    </FieldFeedbacks>
    
    <FieldFeedbacks for="username" stop="no">
      ...
    </FieldFeedbacks>
  • FieldFeedback

    • when?:
      • ValidityState as a string => HTML5 constraint violation name
      • '*' => matches any HTML5 constraint violation
      • 'valid' => displays the feedback only if the field is valid
      • (value: string) => boolean => custom constraint
    • error?: boolean => treats the feedback as an error (default)
    • warning?: boolean => treats the feedback as a warning
    • info?: boolean => treats the feedback as an info
    • children => what to display when the constraint matches; if missing, displays the HTML5 error message if any
  • Async<T> => Async version of FieldFeedback, similar API as react-promise

    • promise: (value: string) => Promise<T> => a promise you want to wait for
    • pending?: React.ReactNode => runs when promise is pending
    • then?: (value: T) => React.ReactNode => runs when promise is resolved
    • catch?: (reason: any) => React.ReactNode => runs when promise is rejected
  • FormWithConstraints

    • validateFields(...inputsOrNames: Array<Input | string>): Promise<Field[]> => Should be called when a field changes, will re-render the proper FieldFeedbacks (and update the internal FieldsStore). Without arguments, all fields ($('[name]')) are validated.

    • validateForm(): Promise<Field[]> => Should be called before to submit the form. Validates only all non-dirty fields (won't re-validate fields that have been already validated with validateFields(...)), If you want to force re-validate all fields, use validateFields() without arguments.

    • isValid(): boolean => should be called after validateForm() or validateFields(), tells if the fields are valid

    • hasFeedbacks(): boolean => tells if the fields have any kind of feedback

    • resetFields(...inputsOrNames: Array<Input | string>): Promise<Field[]> => Resets the given fields and re-render the proper FieldFeedbacks. Without arguments, all fields ($('[name]')) are reset.

    • Field =>

      {
        name: string;
        validations: { // FieldFeedbackValidation[]
          key: number;
          type: 'error' | 'warning' | 'info' | 'whenValid';
          show: boolean | undefined;
        }[];
        isValid: () => boolean
      }
  • Input

    If you want to style <input>, use <Input> instead: it will add classes has-errors, has-warnings, has-infos and/or is-valid on <input> when the field is validated.

    Example: <Input name="username" /> can generate <input name="username" class="has-errors has-warnings">

    FYI react-form-with-constraints-bootstrap4 and react-form-with-constraints-material-ui already style the fields to match their respective frameworks.

Browser support

You can use HTML5 attributes like type="email", required, pattern..., in this case a recent browser is needed,...

<label htmlFor="username">Username</label>
<input type="email" name="username" id="username"
       value={this.state.username} onChange={this.handleChange}
       required />
<FieldFeedbacks for="username">
  <FieldFeedback when="*" />
</FieldFeedbacks>

...or ignore them and rely on when functions:

<label htmlFor="username">Username</label>
<input name="username" id="username"
       value={this.state.username} onChange={this.handleChange} />
<FieldFeedbacks for="username">
  <FieldFeedback when={value => value.length === 0}>Please fill out this field.</FieldFeedback>
  <FieldFeedback when={value => !/\S+@\S+/.test(value)}>Invalid email address.</FieldFeedback>
</FieldFeedbacks>

In the last case you will have to manage translations yourself (see SignUp example).

react-form-with-constraints needs a polyfill such as core-js or babel-polyfill to support IE 11 and lower. See also React JavaScript Environment Requirements.

Notes

0.19.0

2 years ago

0.19.1

2 years ago

0.19.0-beta.2

2 years ago

0.19.0-beta.1

2 years ago

0.17.0

3 years ago

0.18.0-beta.2

3 years ago

0.18.0-beta.1

3 years ago

0.16.1

3 years ago

0.18.0

3 years ago

0.16.1-beta.1

4 years ago

0.16.0-beta.1

4 years ago

0.16.0

4 years ago

0.15.2

4 years ago

0.15.1

4 years ago

0.15.0

4 years ago

0.14.3-beta.6

4 years ago

0.14.3-beta.4

4 years ago

0.14.3-beta.5

4 years ago

0.14.3-beta.3

4 years ago

0.14.3-beta.2

4 years ago

0.14.3-beta.1

4 years ago

0.14.1

4 years ago

0.14.2

4 years ago

0.14.1-beta.1

5 years ago

0.14.0

5 years ago

0.13.0

5 years ago

0.13.0-beta.4

5 years ago

0.13.0-beta.3

5 years ago

0.13.0-beta.2

5 years ago

0.13.0-beta.1

5 years ago

0.12.0

5 years ago

0.11.0

5 years ago

0.11.0-beta.2

5 years ago

0.11.0-beta.1

5 years ago

0.10.0-beta.7

6 years ago

0.10.0-beta.6

6 years ago

0.10.0

6 years ago

0.10.0-beta.5

6 years ago

0.10.0-beta.4

6 years ago

0.10.0-beta.3

6 years ago

0.10.0-beta.2

6 years ago

0.10.0-beta.1

6 years ago

0.9.3

6 years ago

0.9.2

6 years ago

0.9.1

6 years ago

0.9.0

6 years ago

0.9.0-beta.1

6 years ago

0.8.0

6 years ago

0.8.0-beta.9

6 years ago

0.8.0-beta.8

6 years ago

0.8.0-beta.7

6 years ago

0.8.0-beta.6

6 years ago

0.8.0-beta.5

6 years ago

0.8.0-beta.4

6 years ago

0.8.0-beta.3

6 years ago

0.8.0-beta.2

6 years ago

0.8.0-beta.1

6 years ago

0.7.0

6 years ago

0.7.0-beta.4

6 years ago

0.7.0-beta.3

6 years ago

0.7.0-beta.2

6 years ago

0.7.0-beta.1

6 years ago

0.6.4

6 years ago

0.6.3

6 years ago

0.6.2

7 years ago

0.6.1

7 years ago

0.6.0

7 years ago

0.6.0-beta.5

7 years ago

0.6.0-beta.4

7 years ago

0.6.0-beta.3

7 years ago

0.6.0-beta.2

7 years ago

0.6.0-beta.1

7 years ago

0.5.2

7 years ago

0.5.1

7 years ago

0.5.0

7 years ago

0.4.1

7 years ago

0.4.0

7 years ago

0.3.1

7 years ago

0.3.0

7 years ago

0.2.3

7 years ago

0.2.2

7 years ago

0.2.1

7 years ago

0.2.0

7 years ago

0.1.3

7 years ago

0.1.2

7 years ago

0.1.1

7 years ago

0.1.0

7 years ago