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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@becklyn/forms

v4.0.0

Published

1. [Getting Started](#getting-started)

Downloads

194

Readme

Table of Contents

  1. Getting Started

    1. Install dependencies
    2. Create your fields
    3. Create a field component
    4. Define your initial state
    5. Create a form config
    6. Render the form
  2. Advanced usecases

    1. Conditional fields
  3. Guides

    1. Multistep

Getting Started

1. Install dependencies

npm i --save @becklyn/forms

2. Create your fields

Start by building your form fields:

interface BaseFieldConfig {
    placeholder: string;
    label?: string;
    maxLength?: number;
}

type StringFieldConfig = FormFieldConfig<"string", BaseFieldConfig, string | null>;
type NumberFieldConfig = FormFieldConfig<"number", BaseFieldConfig, number>;
type AllConfigs = StringFieldConfig | NumberFieldConfig;

interface TextProps extends BaseFieldConfig {
    id: string;
    name: string;
    value?: string | null;
    onBlur?: () => void;
    onValueChanged?: (value: string) => void;
}

const Text = ({ id, value, name, onBlur, onValueChanged, ...attributes }: TextProps) => {
    return (
        <input
            {...attributes}
            id={id}
            value={value ?? ""}
            name={name}
            onBlur={onBlur}
            type="text"
            onInput={e => onValueChanged && onValueChanged((e.target as HTMLInputElement).value)}
        />
    );
};

interface NumberProps extends BaseFieldConfig {
    id: string;
    name: string;
    value?: number;
    onBlur?: () => void;
    onValueChanged?: (value: number) => void;
}

const Number = ({ id, value, name, onBlur, onValueChanged, ...attributes }: NumberProps) => {
    return (
        <input
            {...attributes}
            id={id}
            value={value ?? ""}
            name={name}
            onBlur={onBlur}
            type="number"
            onInput={e =>
                onValueChanged && onValueChanged(parseFloat((e.target as HTMLInputElement).value))
            }
        />
    );
};

3. Create a field component

Create a single component to handle rendering all of your different input components. It will be called by the FormBuilder including the respective field's props.

const FieldComponent: FC<FormBuilderChildrenProps<AllConfigs, Record<string, any>>> = ({
    value,
    field,
    onBlur,
    onInput,
}) => {
    const id = useId();

    switch (field.type) {
        case "string": {
            const { name, fieldConfig } = field;

            return (
                <Text
                    {...fieldConfig}
                    id={id}
                    value={value}
                    name={name}
                    onBlur={onBlur}
                    onValueChanged={onInput}
                />
            );
        }
        case "number": {
            const { name, fieldConfig } = field;

            return (
                <Number
                    {...fieldConfig}
                    id={id}
                    value={value}
                    name={name}
                    onBlur={onBlur}
                    onValueChanged={onInput}
                />
            );
        }
    }
};

4. Define your initial state

After defining all fields that your forms may use you can define the initial state of your specific form in a flat config (field name -> initial value):

const initialState = {
    Firstname: generateInitialValue<StringFieldConfig>(""),
    Lastname: generateInitialValue<StringFieldConfig>(""),
    Age: generateInitialValue<NumberFieldConfig>(0),
};

5. Create a form config

Finally you can configure the structure of your form:

const userFormConfig = [
    {
        type: "row",
        content: [
            {
                type: "string",
                name: "Firstname",
                fieldConfig: {
                    placeholder: "Enter your firstname",
                    label: "Firstname",
                },
            },
            {
                type: "string",
                name: "Lastname",
                valueFn: generateValueFn<FormData>(data => data.Lastname || "default value"),
                onInput: generateOnInput<StringFieldConfig, FormData>(
                    ({ field, value, previousData }) => {
                        console.log("value", value);
                        console.log("field", field);

                        return { ...previousData };
                    }
                ),
                fieldConfig: generateFormFieldConfigFn<StringFieldConfig, FormData>(
                    ({ data, value }) => {
                        console.log("data", data);
                        console.log("value", value);

                        return {
                            placeholder: "Enter your lastname",
                            label: "Lastname",
                        };
                    }
                ),
            },
        ],
    },
    {
        type: "number",
        name: "Age",
        fieldConfig: {
            placeholder: "Enter your age",
            label: "Age",
        },
    },
] as const satisfies FormConfig<AllConfigs, FormData>;

6. Render the form

To render your form config the FormBuilder needs to be a descendant of FormProvider. An implementation could look something like this where the majority can easily be abstracted.

export const FormRenderer: React.FC = () => {
    return (
        <FormBuilder<AllConfigs, FormData>>{props => <FieldComponent {...props} />}</FormBuilder>
    );
};

export const UserFormHandler = () => {
    const { validateForm } = useForm();

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        const formErrors = validateForm();

        if (formErrors) {
            return;
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <FormRenderer />
            <button type="submit">Submit</button>
        </form>
    );
};

export const Component = () => {
    return (
        <FormProvider<AllConfigs, typeof initialState>
            config={userFormConfig}
            initialData={initialState}>
            <UserFormHandler />
        </FormProvider>
    );
};

Advanced usecases

Usage without styled components

You can instantiate the FormBuilder with your custom components that do not rely on styled-components:

export const FormRenderer: React.FC = () => {
    return (
        <FormBuilder<AllConfigs, FormData>
            Components={{
                BuilderWrapper: ({ children }: PropsWithChildren) => <div>{children}</div>,
                FieldWrapper: ({ children }: PropsWithChildren) => <div>{children}</div>,
                RowWrapper: ({ children }: PropsWithChildren) => <div>{children}</div>,
                SectionWrapper: ({ children }: PropsWithChildren) => <div>{children}</div>,
            }}>
            {props => <FieldComponent {...props} />}
        </FormBuilder>
    );
};

Conditional fields

To help out with conditional rendering, a function called eitherOr is provided.

const form = useMemo(() => {
    return [
        {
            type: "row",
            content: [
                eitherOr(
                    someCondition,
                    {
                        type: "string",
                        name: "email",
                        fieldConfig: {
                            placeholder: "Email address",
                            label: "Email",
                        },
                    },
                    {
                        type: "string",
                        name: "phone",
                        fieldConfig: {
                            placeholder: "Phone number",
                            label: "Phone",
                        },
                    }
                ),
                eitherOr(
                    someCondition && {
                        type: "string",
                        name: "phone",
                        fieldConfig: {
                            placeholder: "Phone number",
                            label: "Phone",
                        },
                    }
                ),
            ],
        },
    ] as const satisfies FormConfig<AllConfigs, FormData>;
}, [someCondition]);

Guides

Multistep

Multistep can be achieved easily and abstracted into a custom component. The concept is simple: there's one parent provider handling all data and a child provider per step reporting to the parent. While this lib handles the data, validations etc., you must still handle the multistep logic yourself.

const Step = () => {
    const { validateForm } = useForm<FormField>();

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        const formErrors = validateForm();

        if (formErrors) {
            return;
        }

        // go to next step or submit form
    };

    return (
        <form onSubmit={handleSubmit}>
            <FormRenderer />
        </form>
    );
};

const MultiStep = () => {
    // your custom hook
    const { step } = useMultiStep();

    let config, initialData;

    switch (step) {
        case 1:
            config = firstStepConfig;
            initialData = initialFirstStepState;
            break;
        case 2:
            config = secondStepConfig;
            initialData = initialSecondStepState;
            break;
        case 3:
            config = thirdStepConfig;
            initialData = initialThirdStepState;
            break;
    }

    return (
        <FormProvider<AllConfigs, typeof initialData>
            config={config}
            initialData={initialData}
            inheritData>
            <Step />
        </FormProvider>
    );
};