npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2024 – Pkg Stats / Ryan Hefner

react-form-ctl

v2.0.1

Published

A react hook for validating forms and keeping track of state, inspired by Angulars FormControl

Downloads

20

Readme

Why use react-form-ctl?

  • No runtime dependencies, only requires React as a peer dependency
  • Fully type-safe
  • Localization ready (i18n)
  • Simple API that will be familiar to Angular developers and easy to learn for React developers
  • All state handling is done for you

Installation

npm install react-form-ctl

# or

yarn add react-form-ctl

Basic Usage

A quick example can be as easy as implementing a hook with some initial data and spreading the given properties onto a input element:

import {useFormControl} from 'react-form-ctl';

// Inside component
const {controls} = useFormControl<FormData>({
    name: ['John'],
    // ...
});

return <>
    <input
        type="text"
        {...controls.name.inputProps()}
    />

    {/* ... */}
</>

There are a number of input helpers available:

  • inputProps -> for HTML inputs
  • numberInputProps -> for HTML inputs of type number
  • checkboxProps -> for HTML checkbox inputs
  • rnInputProps -> for React Native TextInputs
  • rnNumberInputProps -> for React Native TextInputs of type number

Although if you want more granular control, you can do all the state changing by yourself, using the exposed properties:

import {useFormControl, Validators} from 'react-form-ctl';

// Inside component
const {controls} = useFormControl<FormData>({
    name: ['John', [Validators.required, Validators.minLength(3)]],
    // ...
});

return <>
    <input
        type="text"
        value={controls.name.value}
        onChange={(e) => controls.name.setValue(e.target.value)}
        onBlur={() => controls.name.markTouched()}
    />

    {/* ... */}
</>

Error Handling

In case you want to write a more specific error message for different errors, it is recommended to supply a Map with your error messages like in the following example. Note that this will only be applied to the first error supplied by the first failing Validator.

This Error-Map is also a good chance to bring your localized form data in.

import {useFormControl, extError, Validators, ErrorMappings} from 'react-form-ctl';

const {controls} = useFormControl<FormData>({
    name: ['John', [Validators.required, Validators.minLength(5)]],
    // ...
});

const errorMap: ErrorMappings = {
    required: () => 'Field is required',
    minLength: ({length, expectedLength}) => `Minimum Length: ${length}/${expectedLength}`,
    default: () => 'Unknown error' // Default mapping fires, when no mapping is given for another error
                    // If no default mapping is set, a runtime exception may be thrown for unknown exceptions
};

return <>
    {/* input field */}

    {/* Check if the property contains errors (invalid) 
        + also check if the property has been modified (edited or touched) */}
    { controls.name.invalid && (controls.name.touched || controls.name.dirty) && <div>
        {extError(errorMap, controls.name.error)}
    </div>}
</>

Alternatively, you can also get all errors manually and implement your own error handling like so:

import {useFormControl, Validators} from 'react-form-ctl';

const {controls} = useFormControl<FormData>({
    name: ['John', [Validators.required, Validators.minLength(5)]],
    // ...
});

return <>
    {/* input field */}

    { controls.name.errors?.required && <div>
        Field is required
    </div>}
    { controls.name.errors?.minLength && <div>
        Minimum Length: {controls.name.errors.minLength.length}/{controls.name.errors.minLength.expectedLength}
    </div>}
</>

Validators

There are a number of Validators already included:

  • required
  • requiredTrue
  • minLength
  • maxLength
  • numeric
  • min
  • max
  • regex
  • if (can execute validators based on a condition)

If you need more validation types (email, phone number, ...), we recommend using the Validator.js library. Check their documentation for more information about the available validators.

npm i validator && npm i -D @types/validator

# or

yarn add validator && yarn add -D @types/validator

You can then implement your own validators and pass them to the Validators array:

import {useFormControl, Validators, InternalState} from 'react-form-ctl';
import isEmail from 'validator/lib/isEmail';

// A simple custom validator
const nameNotBlacklisted = Validators.create((value: string) => {
    if(['Max', 'Anna'].includes(value)) {
        return {
            name: 'nameNotBlacklisted',
            found: value
        };
    }
});

// You can also create parametrized custom validators
const isExactAge = Validators.createParametrized((age: number) => {
    return Validators.create((value: number) => {
        if (value !== age) {
            return {
                name: 'isExactAge',
                expected: age
            };
        }
    });
});

// You can also check against other form values in your validator
const passwordRepeatMatches = Validators.create(
    (passwordRepeat: string, state: InternalState<FormData>) => {
        if (passwordRepeat !== state.password.value) {
            return {
                name: 'passwordRepeatMatches',
                expected: passwordRepeat
            };
        }
    });

// You can use the Validator.js library to write your own custom validators
const validateEmail = Validators.create((email: string) => {
    if (!isEmail(email)) {
        return {
            name: 'validateEmail',
        };
    }
});

// You can use the "if" Validator to create conditional Validators
const isSomethingTrue = ...;
const requiredIfCondition = Validators.if(() => isSomethingTrue, [Validators.required])

// Then use them like other validators inside your Validators array
const {controls} = useFormControl<FormData>({
    name: ['John', [nameNotBlacklisted]],
    age: [21, [isExactAge(42), requiredIfCondition]],
    password: [''],
    passwordRepeat: ['', [passwordRepeatMatches]],
    email: ['[email protected]', [validateEmail]]
    // ...
}, [isSomethingTrue]); // Needs to pass dependency array, because of the "if" validator

// Don't forget to also write custom error handlers if you want
const errorMap: ErrorMappings = {
    nameNotBlacklisted: ({found}) => `Name is blacklisted: ${found}`,
    isExactAge: ({expected}) => `Expected age of ${expected}`,
    passwordRepeatMatches: () => `Password does not match`,
    validateEmail: () => `Email invalid`,
    default: () => 'Unknown error'
};

API Reference

You will get back an object containing information about the general state of the form and also a more detailed information about each form field:

type State = {
    controls: {
        [FieldName: string]: { // For each passed field, you get an entry 
                               // in this object with detailed information about it

            // properties are described down below
        }
    };
    value: FormData; // Get the input data as an object
    setValue: (value: FormData) => void; // Update the whole form state at once
    reset: () => void; // Reset the form to the initial values

    valid: boolean; // If the form passed all Validators
    invalid: boolean; // The opposite of the above

    touched: boolean; // If the form data was set once
    dirty: boolean; // If the form data was updated once
    tod: boolean; // Shorthand for touched || dirty
}

For each passed field, you will get back the following:

type FieldState = {
    value: FieldType; // The value of the field
    setValue: (value: FieldType) => void; // Callback to set the value of the field
    resetValue: (value: FieldType) => void; // Callback to set the value of the field and setting touched = false & dirty = false

    valid: boolean; // If the value passes all Validators
    invalid: boolean; // The opposite of the above

    touched: boolean; // If the value was set once
    markTouched: (value?: boolean) => void; // Set the value to a desired touched state (if no parameter is given, it is set to touched = true)
    dirty: boolean; // If the value was updated once
    markDirty: (value?: boolean) => void; // Set the value to a desired dirty state (if no parameter is given, it is set to dirty = true)
    touchedOrDirty: boolean; // Shorthand for touched || dirty

    error?: { 
        name: string; // name of the error (e.g. 'required')
        // more custom error properties
    };
    errors?: {
        [ErrorName: string]: { // the property name is the name of the error (e.g. required)
            name: string; // name of the error (e.g. 'required')
            // more custom error properties
        }
    };
}

dirty vs touched

Dirty means that the value of a property has updated once. Touched means that the input element has been focused once.

A common use-case for those properties is error-handling. An error for a value should mostly only be shown, if the value is invalid AND was already changed once, or the input field has been focused and unfocused again. To implement this, you would have to write:

if (somefield.invalid && (somefield.touched || somefield.dirty)) { /* ... */ }

Because this is so common, react-form-ctl has implemented a small shortcut for that:

if (somefield.invalid && somefield.touchedOrDirty) { /* ... */ }