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

hookform-input

v2.0.1

Published

Smart, polymorphic and typesafe Input component for react-hook-form

Downloads

180

Readme

Introduction

Typesafe and simple implementation of polymorphic Smart component component for react-hook-form package.

Motivation

If you are here I suppose you use react-hook-form to work with your forms. Have you heard about Smart components? It's really cool approach - magic 🪄 just happens.

Main purpose of this project is to make this pattern easy to integrate with your current codebase by combining polymoprhic and Smart Component approaches.

Getting started

npm install hookform-input

yarn add hookform-input

pnpm add hookform-input

Bare minimum to start with!

import { FormInput } from 'hookform-input';
import { FormProvider, useForm } from 'react-hook-form';

const NameForm = () => {
    const form = useForm();

    const onSubmit = (values) => {
        console.log(values);
    };

    return (
        <FormProvider {...formData}>
            <form onSubmit={form.handleSubmit(onSubmit)}>
                <FormInput name="username" />

                <Button type="submit">Submit</Button>
            </form>
        </FormProvider>
    );
};

Core

To achieve maximum compatibility with UI libraries FormInput is making usage of controlled version of input via useController hook and/or* Context API exposed by react-hook-form.

As everything in apart of some pros there are also cons coming with each decision:

Pros:

  • more flexible API
  • you can use any component that supports value and onChange props (or their equivalents) as input prop.

Cons:

  • using FormProvider might lead to potential performance issues in more complex forms due to more re-renders*

depends on what variant of FormInput you are using

Smart & Polymorphic approach 🧠

Polymorphic components

In a few words, it is a component that lets us specify which React element we want to use for its root. If you’ve used some UI library, such as Mantine or Material UI, you’ve already encountered a polymorphic component.

Smart components

According to react-hook-form docs:

"This idea here is that you can easily compose your form with inputs."

However, it's not that easy to achieve in a real-world application. This package is here to help you with that. The example from the docs is working perfectly fine but has some limitations eg. doesn't supports nested component fields. Another problem is lack of typescript support for name field which is really annoying.

API

FormInputBare

This is a generic component so you can provide your own type for Form and input component props. In this variant the the Form type is being read base on the control prop so its not required to provide it.

We get full type safety for the input component props and our field name.

{
  /**
   * The component used for the root node. Either a string to use a HTML element or a component.
   */
  input?: Input;
  /**
        @string name of the field in form
      */
  name: Path<Form>;
  /**
       @object control object from useForm hook
     */
  control: Control<Form>;
  /**
      @string optional field from form fields to display error message
    */
  alternativeErrorKeys?: Path<Form>[];
  /**
      @boolean if true will log to console input changes with detailed information
    */
  debug?: boolean;
  /**
      @string in case your component uses different key than value eg. "checked" for checkbox
      @default "value"
    */
  valueKey?: string;
  /**
      @string key to use for adapter
    */
    adapterKey?: keyof FormInputAdapterKeys;
};

FormInputAdapterKeys is a global interface

And additional props supported by the specified input component. Also if you want to pass some additional props directly to the useController hook each of its props is available with _controller prefix eg. _controllerRules.

FormInput

Basically it's a re-export of FormInputBare with predefined control props by subscribing to the FormProvider context by useFormContext hook.

FormInput share the same API as FormInputBare except the control prop which is omitted.

Input factory

You might not want to pass input prop manually all the time over and over again. Especially when you work in larger projects with a lot of forms. Probably you have some kind of Input component that you use in your forms or that is coming from a UI library of your choice.

This is why we have a factory function that allows you to create a new component with predefined input prop.

Usage

Simple forms

import { FormInput, FormInputBare } from "hookform-input";
import { useForm } from "react-hook-form";

const NameForm = () => {
  const form = useForm({
    defaultValues: {
      username: "",
    },
  });

  const onSubmit = (values) => {
    console.log(values);
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <FormInputBare name="username" control={form.control} />
      <Button type="submit">Submit</Button>
    </form>
    // OR
    <FormProvider {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormInput name="username" />
        <Button type="submit">Submit</Button>
      </form>
    </FormProvider>
  );
};

Factory

type TestInputProps = {
  value?: number;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  // this will be still required
  randomProp: string;
};

const TestInput = ({ value, onChange, randomProp }: TestProps) => {
  return <input value={value} onChange={onChange} />;
};

const TestFormInput = createFormInput(TestInput)
const TestFormInputBare = createFormInput(TestInput, {
    bare: true,
    defaultProps: {
        randomProp: "default"
    }
})

// Usage
<TestFormInput<TestForm>
  randomProp="text"
  name="nest"
/>

<TestFormInputBare
  randomProp="text"
  name="nest"
  control={form.control}
/>

Global Adapters

To make integration with different UI libraries easier, you can use the formInputAdapters object to register custom input adapters. It is simple implementation of Adapter Pattern that allows you to transform the props passed to the hookform-input component to match the expected format of the UI library or your actual components that you are using without unnessesary refactor or adjustments.

For example - Mantine is working without any extra work. Even things like displaying error message are handled correctly because it's TextInput component receives error as a string prop.

Material UI on the other hand has slightly different - to display error message we have to pass error as a boolean and helperText string props, thats why Adapters comes helpful.

Usage with Material UI

import { formInputAdapters } from 'hookform-input';

type MuiTextFieldProps = Omit<FormInputForwardedProps, 'error'> & {
    error?: boolean;
    helperText?: string;
};

declare global {
    interface FormInputAdapterKeys {
        'MUI-TextField': MuiTextFieldProps;
    }
}

formInputAdapters.register({
    key: 'MUI-TextField',
    transformFn: (props, originalProps) => ({
        ...props,
        error: !!props.error,
        helperText: props.error ?? originalProps?.description,
    }),
});

Now, when you use the hookform-input with the MUI-TextField key, the props will be transformed to match the expected format.

import TextField from '@mui/material/TextField';
import { useForm } from 'react-hook-form';

const Component = () => {
    const form = useForm({
        defaultValues: {
            text: 'some value',
        },
    });

    const onSubmit = (data) => {
        console.log(data);
    };

    return (
        <form onSubmit={format.handleSubmit(onSubmit)}>
            <FormInputBare name="text" input={TextField} adapterKey="MUI-TextField" control={form.control} />

            <button type="submit">Submit</button>
        </form>
    );
};

by making usage of factories we can get rid of repeating input and adapterKey props.

import TextField from '@mui/material/TextField';

const MaterialFormInput = createFormInput(TextField, {
    bare: true,
    defaultProps: {
        adapterKey: 'MUI-TextField',
    },
});

/* code */

return (
        <form onSubmit={format.handleSubmit(onSubmit)}>
            <MaterialFormInput name="text" control={form.control} />
            <button type="submit">Submit</button>
        </form>
    );