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

preform

v0.1.7

Published

A lightweight React library for forms

Downloads

6

Readme

$ npm install preform

Create forms in React the easy way!

Creating this library I focused on the following:

  • Efficiency (no unnecessary render calls from react)
  • Lightweight (less than 10kb)
  • Stability (no mutable state or weird breaking changes)
  • Using only React hooks, but with backwards compatibility using higher order components
  • TypeScript support

The form context

When creating a form you should first wrap the container component in the asForm function. This creates a context within which the state of all form fields will live.

import React from 'react';
import { asForm } from "preform";

const MyFormComponent = (props) => (
  <div>
    <h1>This is my form!</h1>
  </div>
);

const MyForm = asForm(MyFormComponent);

export default MyForm;

It is possible to create multiple form contexts (components wrapped in asForm) in the same page. Child components will only have access to their most direct container.

Creating an input field

Let's create a simple input field in which you can enter your email:

import React from 'react';
import { asForm, useField } from "preform";

const MyForm = (props) => {
  const { setValue, value } = useField({
    field: "firstName"
  });
  return (
    <div>
      <h1>This is my form!</h1>
      <label for="firstName">First name</label>
      <br />
      <input
        id="firstName"
        type="text"
        value={value}
        onChange={event => setValue(event.target.value)}
      />
    </div>
  );
};

export default asForm(MyForm);

So what's going on here is that when calling asForm(MyForm) a state is being created. When calling useField({ field: "firstName" }) we create an attribute inside that state labelled "firstName". useField will then return value and setValue to use this particular attribute. If you call useField({ field: "firstName" }) again inside MyForm it will return the same value as the one before. So you can think of the field as being the unique identifier of a form field.

Knowing that we'll probably create more than one input field it would be a good idea to make a seperate Input component from which will call useField.

import React from 'react';
import { asForm, useField } from "preform";

const Input = (props) => {
  const { setValue, value } = useField({
    field: props.field,
    initialValue: props.initialValue
  });
  return (
    <div>
      <label for={props.field}>{props.label}</label>
      <br />
      <input
        id={props.field}
        type={props.type}
        value={value}
        onChange={event => setValue(event.target.value)}
      />
    </div>
  )
}

const MyForm = (props) => {
  return (
    <div>
      <h1>This is my form!</h1>
      <Input
        field="firstName"
        type="text"
        label="First name"
        initialValue="Mickey"
      />
      <Input
        field="lastName"
        type="text"
        label="Last name"
        initialValue="Mouse"
      />
    </div>
  );
};

export default asForm(MyForm);

You might notice useField can also take an initialValue.

Validation

Now let's make sure the name which you enter doesn't contain any numbers. We can check this using the regex /^[^\d]*$/.test('string value'). Let's put this in a validator function.

// value is the value of the field we want to validate. values is an object containing all values in the entire form
const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';

A validator takes the value of the field and should return a falsy value if it's correct (null, undefined, '', false etc). It should return a string with a description of what went wrong if the value is invalid. The validator can also return a promise resolving or rejecting with these values.

The validator can be used as an argument in useField along with the field. useField also returns a validate and error variable which are coupled to whatever value the validator returns.

const { setValue, value, validate, error } = useField({
  field: "name",
  validator: validateName
});

validate can be called so that the current value of the field gets validated. If the field is invalid, error will be changed to an Error object with the result of validator as its message.

Let's use the code from our example to validate the first name and last name.

import React from 'react';
import { asForm, useField } from "preform";

const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';

const Input = (props) => {
  const { setValue, value, validate, error } = useField({
    field: props.field,
    initialValue: props.initialValue,
    validator: props.validator
  });
  return (
    <div>
      <label for={props.field}>{props.label}</label>
      <br />
      <input
        id={props.field}
        type={props.type}
        value={value}
        onChange={event => setValue(event.target.value)}
        onBlur={validate}
      />
      {error && (
        <i>{error.message}</i>
      )}
    </div>
  )
}

const MyForm = (props) => {
  return (
    <div>
      <h1>This is my form!</h1>
      <Input
        field="firstName"
        type="text"
        label="First name"
        initialValue="Mickey"
        validator={validateName}
      />
      <Input
        field="lastName"
        type="text"
        label="Last name"
        initialValue="Mouse"
        validator={validateName}
      />
    </div>
  );
};

export default asForm(MyForm);

Right now we're validating every field on its own (using onBlur). However, we can also instead choose to validate the whole form at a certain moment. For that we can use the useFormApi hook.

useFormApi

This hook also brings some variables with which can be used to influence the form state as a whole.

import { useFormApi } from "preform";

// Somewhere inside a component:
const {
  validate,
  formState,
  setValue
} = useFormApi();

validate

This function can be called at any time to validate all fields currently in the context. Each component which did a call to useField will be updated if needed. This function returns a promise which will resolve as the new formState.

formState

The formState is an object with the following signature:

{
  // values is a map in which all current field values are stored
  values: {
    field: value
  };
  // valid will be updated only after calling the validate function on the entire form.
  valid: boolean;
  // invalid is simply the opposite of the valid attribute
  invalid: boolean;
  // when calling validate on the entire form, it might be that one of the validators is returning a promise 
  // which takes a while to resolve or reject. During that time, loading will be true
  loading: boolean;
  // dirty means that setValue has never called (every field is still at its initial value)
  dirty: boolean;
  // pristine is simply the opposite of the dirty attribute
  pristine: boolean;
  // submitted is always true if dirty is false, but it can also become true when a submit was successful 
  // (with dirty being true)
  submitted: boolean;
  // errors is a map in which all current errors are stored
  errors: {
    field: Error
  };
}

setValue

This function can be called to set the value of one specific field. Useful if you want to do this from outside the component which called useField for that specific field.

// In here, the field 'firstName' is given the value 'Minnie'
setValue("firstName", "Minnie");

Using the useFormApi we can create submit functionality in our example

import React from 'react';
import { asForm, useField, useFormApi } from "preform";

const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';

const Input = (props) => {...}

const MyForm = (props) => {
  const { validate } = useFormApi();

  // Check the useSubmit hook to write this callback more efficiently!
  const handleSubmit = async (event) => {
    // Prevent the page from reloading
    event.preventDefault();

    const formState = await validate();
    if (formState.valid) {
      // In here you might want to make an API call or something like that
      console.log(formState.values)
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>This is my form!</h1>
      <Input
        field="firstName"
        type="text"
        label="First name"
        initialValue="Mickey"
        validator={validateName}
      />
      <Input
        field="lastName"
        type="text"
        label="Last name"
        initialValue="Mouse"
        validator={validateName}
      />
      <input type="submit" />
    </form>
  );
};

export default asForm(MyForm);

useSubmit

This function has almost the same signature as useCallback, only it makes sure the form has been validated successfully before the callback is being called. So from our example above we can rewrite this:

const handleSubmit = useCallback(async (event) => {
  // Prevent the page from reloading
  event.preventDefault();

  const formState = await validate();
  if (formState.valid) {
    // In here you might want to make an API call or something like that
    await request(formState.values);
    console.log('Done!');
  }
}, [request, validate]);

To this:

const handleSubmit = useSubmit(async (values) => {
  // In here you might want to make an API call or something like that
  await request(values);
  console.log('Done!');
}, [request]);

Extra functions

The functionality above requires a whole lot of boilerplate, and it might also be difficult to achieve using React class components. Therefore this library contains a couple of "shortcut" functions.

asField

Instead of calling useField you might want to wrap your input field component in the asField wrapper.

import { asField, useField } from "preform";

// Using useField:
const Input = (props) => {
  const { setValue, value, error, validate } = useField({
    field: props.field,
    initialValue: props.initialValue,
    validator: props.validator
  });
  return (
    <div>
      <input
        value={value}
        onChange={event => setValue(event.target.value)}
        onBlur={validate}
      />
      {error && (
        <i>{error.message}</i>
      )}
    </div>
  )
}

// Using asField:
const InputComponent = (props) => {
  return (
    <div>
      <input
        value={props.value}
        onChange={event => props.setValue(event.target.value)}
        onBlur={props.validate}
      />
      {props.error && (
        <i>{props.error.message}</i>
      )}
    </div>
  )
}
const Input = asField(InputComponent);

// Both versions can be used like this:

<Input
  field="myField"
  validator={myValidator}
  initialValue={myInitialValue}
/>

asForm props and asSubmit

When calling asForm(MyComponent) it will also pass some variables to MyComponent's props so that you don't have to call useFormApi. These are formState, validate and setValue. Besides that MyComponent will also receive the prop asSubmit. This function has the following signature:

const handleSubmit = asSubmit((values) => {
  // All fields of the form have been validated correctly when this code is being executed. Also
  // values will be provided with the current values of the form
})

Using asSubmit we can simplify our earlier version of the example a bit more:

import React from 'react';
import { asForm, useField, useFormApi } from "preform";

const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';

const Input = (props) => {...}

const MyForm = (props) => {
  const handleSubmit = props.asSubmit((values) => {
    console.log(values);
  });

  return (
    <form onSubmit={handleSubmit}>
      <h1>This is my form!</h1>
      <Input
        field="firstName"
        type="text"
        label="First name"
        initialValue="Mickey"
        validator={validateName}
      />
      <Input
        field="lastName"
        type="text"
        label="Last name"
        initialValue="Mouse"
        validator={validateName}
      />
      <input type="submit" />
    </form>
  );
};

export default asForm(MyForm);