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

@os-design/form

v1.0.72

Published

Create forms in React and React Native much faster and easier.

Downloads

117

Readme

@os-design/form NPM version BundlePhobia

Create forms in React and React Native much faster and easier.

Features:

  • 🤓 Rerenders only updated fields, not the whole form.
  • 📱 Use with any design system or native components. Supports React Native.
  • 2️⃣ Tiny size (~2 KB). Zero dependencies.
  • 📙 Lots of useful features.

Installation

Install the package using the following command:

yarn add @os-design/form

🙈 Simple form

Let's assume that we want to build a form for creating blog posts. To create a form, use the useForm hook and pass the initial data to it. This hook returns the Field component, which should be used to render each of your input. To get all the form data use form.values.getAll().

import { useForm } from '@os-design/form';

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      content: '',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    // To get all the form data
    console.log(form.values.getAll());
  }, []);

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='content' render={(props) => <TextArea {...props} />} />
      <Button onClick={onSubmit}>Add</Button>
    </>
  );
};

The props is an object that contains 2 properties: value and onChange, so you can just pass all the properties of this object to your input component for simplicity.

⚠️ The type of onChange callback is (value: T) => void. If your input component has another type, you should pass this callback manually. For example, the onChange callback in the native input component has the type (e: ChangeEvent<HTMLInputElement>) => void (Ant Design's input component has the same type).

<Field
  name='title'
  render={({ value, onChange }) => (
    <Input value={value} onChange={(e) => onChange(e.target.value)} />
  )}
/>

🙉 Complex form

Let's look at another example. We need to add multiple fields with options to the form (e.g. min and max). These options should be stored in the options form property.

interface ComplexFormData {
  name: string;
  options: {
    min: number | null;
    max: number | null;
  };
}

We can add a Field with the name options and render 2 fields at once, but in this case, every time one of these options changes, all the option fields will be rerendered. It's not good for performance.

To solve this problem and update only those options that have changed, we need to add separate Field components and specify the path to the field in the name property. For example, in our case we have to add 2 fields with the following names: options.min and options.max (the properties are separated by a dot).

const Form: React.FC = () => {
  const initValues = useMemo<ComplexFormData>(
    () => ({
      name: '',
      options: { min: null, max: null },
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  return (
    <>
      <Field name='name' render={(props) => <Input {...props} />} />
      <Field
        name='options.min'
        render={(props) => <InputNumber {...props} />}
      />
      <Field
        name='options.max'
        render={(props) => <InputNumber {...props} />}
      />
      <Button onClick={onSubmit}>Add</Button>
    </>
  );
};

The path can also contain an index of array. For example, options.items.0.status.

❗ Showing errors

If the user entered incorrect values, the form should display errors. To support errors, you need to make 2 steps: pass the errors to the form and display them next to the input components.

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      content: '',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    form.errors.set('title', 'The title is too long 😔');
  }, []);

  return (
    <>
      <Field
        name='title'
        render={(props, { error }) => (
          <FormItem error={error}>
            <Input {...props} />
          </FormItem>
        )}
      />
      <Field
        name='content'
        render={(props, { error }) => (
          <FormItem error={error}>
            <TextArea {...props} />
          </FormItem>
        )}
      />
      <Button onClick={onSubmit}>Add</Button>
    </>
  );
};

If you want to display an error message not next the input component (e.g. if an error is not related to the field), you can create a separate component and place it wherever you want.

// Component to display errors unrelated to any fields
const Error: React.FC = () => {
  const { useError } = useExistingForm();
  const error = useError('_error'); // Use any name that is not used by any field
  return error ? <Alert type='error'>{error}</Alert> : null;
};

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      content: '',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    // Set the error
    form.errors.set<any>('_error', 'The server went on vacation 🌴');

    // Return only data (without the `_error` prop)
    console.log(form.values.getAll()); // { title: '', content: '' }
  }, []);

  // Wrap your form in a `FormProvider`
  // to pass the `form` to child components
  return (
    <FormProvider form={form}>
      <>
        <Field
          name='title'
          render={(props, { error }) => (
            <FormItem error={error}>
              <Input {...props} />
            </FormItem>
          )}
        />
        <Field
          name='content'
          render={(props, { error }) => (
            <FormItem error={error}>
              <TextArea {...props} />
            </FormItem>
          )}
        />

        <Error />

        <Button onClick={onSubmit}>Add</Button>
      </>
    </FormProvider>
  );
};

🪆 Getting the form in child components

To pass the form to the child components, you have to:

  1. Wrap your form in a FormProvider.
  2. Use the useExistingForm hook in the child components.
interface FormData {
  title: string;
  content: string;
}

interface BaseFormProps {
  children: React.ReactNode;
}

const BaseForm: React.FC<BaseFormProps> = ({ children }) => {
  const { Field } = useExistingForm<FormData>();

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='content' render={(props) => <TextArea {...props} />} />
      {children}
    </>
  );
};

const FormCreate: React.FC = () => {
  const initValues = useMemo<FormData>(
    () => ({
      title: '',
      content: '',
    }),
    []
  );

  const { form } = useForm(initValues);

  const onCreate = useCallback(() => {
    console.log('Creating...', form.values.getAll());
  }, []);

  return (
    <FormProvider form={form}>
      <BaseForm>
        <Button onClick={onCreate}>Create</Button>
      </BaseForm>
    </FormProvider>
  );
};

const FormUpdate: React.FC = () => {
  const initValues = useMemo<FormData>(
    () => ({
      title: 'Title',
      content: 'Content',
    }),
    []
  );

  const { form } = useForm(initValues);

  const onUpdate = useCallback(() => {
    console.log('Updating...', form.values.getAll());
  }, []);

  return (
    <FormProvider form={form}>
      <BaseForm>
        <Button onClick={onUpdate}>Update</Button>
      </BaseForm>
    </FormProvider>
  );
};

🚫 Blocking the button until the form is changed

Let's upgrade our form and allow the user to click on the Save button only if one of the field values has been changed. To implement it, let's pass the modified flag to the disabled property of the button.

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: 'Title',
      content: 'Content',
    }),
    []
  );

  const { Field, form, modified } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='content' render={(props) => <TextArea {...props} />} />
      <Button disabled={!modified} onClick={onSubmit}>
        Save
      </Button>
    </>
  );
};

🎯 Getting only changed values

If the form has many fields, it is better to send only the changed values to the server (not all the form data). To do this, you need to determine which fields have been changed using the modifiedFields and get only them.

modifiedFields is an array with the names of the fields that have been changed. If the form has 2 fields: title, content and each of them has been changed, then modifiedFields will be equal to ['title', 'content'].

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      content: '',
    }),
    []
  );

  const { Field, form, modifiedFields } = useForm(initValues);

  const onSubmit = useCallback(() => {
    const onlyChangedValues = modifiedFields.reduce(
      (acc, field) => ({ ...acc, [field]: form.values.get(field) }),
      {}
    );
    console.log(onlyChangedValues);
  }, [modifiedFields]);

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='content' render={(props) => <TextArea {...props} />} />
      <Button onClick={onSubmit}>Save</Button>
    </>
  );
};

Note that modifiedFields can contain paths (e.g. options.min), so the above implementation of onlyChangedValues only works for forms without nested data. Create your own implementation if you want to support nested data.

↩️ Resetting a field to its initial value

Let's assume the user changed his profile data in the form, but then decided to rollback one of the field values. It would be really cool if the user could just click the button next to the input component and the field value would return to the initial value.

You can easily add this feature as follows:

const ResetButton: React.FC<ButtonProps> = (props) => (
  <Button type='ghost' size='small' wide='never' {...props}>
    <RollbackIcon />
  </Button>
);

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      name: 'Kate',
      password: 'secret',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  return (
    <>
      <Field
        name='name'
        render={(props, { modified, reset }) => (
          <Input
            {...props}
            right={modified && <ResetButton onClick={reset} />}
          />
        )}
      />
      <Field
        name='password'
        render={(props, { modified, reset }) => (
          <InputPassword
            {...props}
            right={modified && <ResetButton onClick={reset} />}
          />
        )}
      />
      <Button onClick={onSubmit}>Save</Button>
    </>
  );
};

To reset the entire form (e.g. when the modal with the form is closed), call form.reset().

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: 'Title',
      content: 'Content',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onReset = useCallback(() => form.reset(), [form]);

  return (
    <Form>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='content' render={(props) => <TextArea {...props} />} />
      <Button onClick={onReset}>Reset</Button>
    </Form>
  );
};

📢 Rerendering fields if some data has been changed

The fields are rerendered ONLY when the value or error has been changed. If your field depends on additional data, then it will not be rerendered when this data has been updated.

// ❌ Incorrect because the input will not be rerendered when the `number` is updated.
// The text in the input will always be `Number is 0`.
const Form: React.FC = () => {
  const initValues = useMemo(() => ({ name: '' }), []);

  const { Field } = useForm(initValues);

  const [number, setNumber] = useState(0);

  return (
    <Form>
      <Field
        name='name'
        render={(props) => (
          <>
            <Input {...props} />
            <div>Number is {number}</div>
          </>
        )}
      />
      <Button onClick={() => setNumber((v) => v + 1)}>Increment</Button>
    </Form>
  );
};

To solve this issue, you need to pass additional data on which the field depends (in our case, the variable number) to the data property and get them from the third argument of the render function.

// ✅ Correct
const Form: React.FC = () => {
  const initValues = useMemo(() => ({ name: '' }), []);

  const { Field } = useForm(initValues);

  const [number, setNumber] = useState(0);

  return (
    <Form>
      <Field
        name='name'
        data={number}
        render={(props, _, n) => (
          <>
            <Input {...props} />
            <div>Number is {n}</div>
          </>
        )}
      />
      <Button onClick={() => setNumber((v) => v + 1)}>Increment</Button>
    </Form>
  );
};

🎚️ Transforming field values

The entered value in the field can be transformed. For example, the email address should always be entered in lowercase. To implement it, you should set a transformer to the field. This transformer is called before the onChange handler is called. The type of the received and returned value must be the same.

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      email: '',
      password: '',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  return (
    <>
      <Field
        name='email'
        transformer={(value) => value.toLowerCase()} // Make email in lowercase
        render={(props) => <Input {...props} />}
      />
      <Field name='password' render={(props) => <InputPassword {...props} />} />
      <Button onClick={onSubmit}>Sign In</Button>
    </>
  );
};

🎛️ Changing another fields

In my practice, this came in handy in 2 cases:

  1. To generate a field value by specific field. For example, to generate a meta title of a blog post by its title.
  2. To reset some field values if a specific field is changed.

Let's consider the first case. To implement it, use the useTransformer hook.

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      metaTitle: '',
    }),
    []
  );

  const { Field, form, useTransformer } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  useTransformer('title', (value) => ({
    // Update any number of fields, not just one.
    // You can use a path (e.g. `'options.min': 0`).
    metaTitle: `The length of the title is ${value.length}`,
  }));

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='metaTitle' render={(props) => <Input {...props} />} />
      <Button onClick={onSubmit}>Save</Button>
    </>
  );
};

👀 Tracking field changes

You can track changes for a specific field using the useValue hook.

For example, if the url slug is generated by the title on the server side, you need to send a request every time the title field changes and update the url slug field after receiving a response.

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      urlSlug: '',
    }),
    []
  );

  const { Field, form, useValue } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  const title = useValue('title'); // You can use a path (e.g. `options.min`)

  useEffect(() => {
    // Send a request to the server
    // to generate the url slug by title
    setTimeout(() => {
      // Update the url slug field
      form.values.set('urlSlug', title.length.toString());
    }, 1000);
  }, [title]);

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='urlSlug' render={(props) => <Input {...props} />} />
      <Button onClick={onSubmit}>Add</Button>
    </>
  );
};

You can also track changes to all fields, for example, for logging.

const Form: React.FC = () => {
  const initValues = useMemo(
    () => ({
      title: '',
      content: '',
    }),
    []
  );

  const { Field, form } = useForm(initValues);

  const onSubmit = useCallback(() => {
    console.log(form.values.getAll());
  }, []);

  useEffect(() => {
    const subscription = form.values.subscribeToAll((name, value) => {
      console.log(`${name} = ${value}`);
    });
    return () => subscription.unsubscribe();
  }, []);

  return (
    <>
      <Field name='title' render={(props) => <Input {...props} />} />
      <Field name='content' render={(props) => <TextArea {...props} />} />
      <Button onClick={onSubmit}>Save</Button>
    </>
  );
};

That's all for now. I hope this library will help you create any forms faster and keep your code clean. 😉