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

@dawiidio/form

v0.0.5

Published

Small, headless (port it to any UI framework) library for creating rich and robust forms in React. Based on `react-hook-form` and `yup` validator.

Downloads

149

Readme

@dawiidio/form

Small, headless (port it to any UI framework) library for creating rich and robust forms in React. Based on react-hook-form and yup validator.

Why?

Because I'm tired of writing the same form handling code over and over again. AND I don't like form creators based on configuration as many do now. I think forms often need additional logic, and it's better to have them in code when we can easily manipulate them, change rules, parse data, port new fields etc.

Why headless?

Because I wanted to have some nice building blocks of higher abstraction level, so I can use them always the same everywhere and port only the UI part.

Why not just use react-hook-form and yup?

Please do! This lib is just a wrapper around them, with some small additions and some sugar on top.

Porting to UI frameworks

There will be some ports to popular UI frameworks like Material-UI, Ant Design, etc. But in my work I use custom UI, so you will need wait or do it yourself. If you do, please share it with me and I'll link it here.

Porting fancy inputs

What do I mean by fancy inputs? I mean inputs that are not just simple text inputs, but some complex components like drag&drop libs, canvas editors, rich text editors, etc. With usage of useFormInputController you can easily port any of them, store their value in form state, validate them as any other input, and use them in your forms. I like this approach because it gives me encapsulation and reusability of complex components. I can write canvas editor for ML pipelines, and when someone wants to add it to the form as a part of other flow, I can easily just write simple wrapper around it and use it as a form input.

Installation

npm install @dawiidio/form

Quick example

import React, {useState} from 'react';
import {Field, Form, FormObject, FormArray, FormInput, FormArrayControls} from '@dawiidio/form';
import Input from './Input';

export interface MyFormValues {
    fieldWithoutNesting: string;
    obj: {
        someField: string;
    };
    arr: Array<{
        arrField: string;
        objNestedInArr: {
            nestedField: string;
        };
    }>;
}

// define schema for form validation
const schema = yup.object({
    fieldWithoutNesting: yup.string().required(),
    obj: yup.object({
        someField: yup.string().required()
    }),
    arr: yup.array().of(
        yup.object({
            arrField: yup.string().required(),
            objNestedInArr: yup.object({
                nestedField: yup.string().required()
            })
        })
    )
});

// default values, in real app most likely fetched from API
const defaultValues = {
    fieldWithoutNesting: 'defaultValue',
    obj: {
        someField: 'someValue'
    },
    arr: [
        {
            arrField: 'arrValue1',
            objNestedInArr: {nestedField: 'nestedValue1'}
        },
        {
            arrField: 'arrValue2',
            objNestedInArr: {nestedField: 'nestedValue2'}
        }
    ],
};

export const MyForm = () => {
    const [loading, setLoading] = useState();

    // do something with form data
    const handleSubmit = (data: MyFormValues) => {
        console.log(data);
        // send to server and disable form until response is received
        setLoading(true);
    };

    return (
        <Form<MyFormValues>
            onSubmit={handleSubmit}
            defaultValues={defaultValues}
            schema={schema}
            disabled={loading}
        >
            <FormInput name='fieldWithoutNesting' component={Input}/>
            <FormObject name='obj'>
                <FormInput name='someField' component={Input}/>
            </FormObject>
            <FormArray name='arr' initialValue={{arrField: 'Default value for new entries'}} setInitialValueOnMount>
                <FormArrayControls>
                    {() => (
                        <>
                            <FormInput name='arrField' component={Input}/>
                            <FormObject name='objNestedInArr'>
                                <FormInput name='nestedField' component={Input}/>
                            </FormObject>
                        </>
                    )}
                </FormArrayControls>
            </FormArray>
        </Form>
    );
};

Components

FYI: Docs for components were generated with LLM because I'm in rush (and lazy). If something doesn't make sense, please refer to the source code and/or create an issue.

Form

The Form component is used to create a form context using react-hook-form and yup for validation. It provides a structured way to handle form submission, validation, and error handling.

Props

| Name | Type | Default | Required | Description | |---------------|----------------------------------------------------------------------|------------------------|----------|-----------------------------------------------------------------------------| | error | string \| ReactNode | N/A | No | Error message or component to display. | | onSubmit | (data: T, event: SyntheticEvent<SubmitEvent, Event> \| undefined) => void \| Promise<void> | N/A | Yes | Function to handle form submission. | | onCancel | () => void | N/A | No | Function to handle form cancellation. | | onError | () => void | N/A | No | Function to handle form errors. | | defaultValues| DefaultValues<T> | N/A | No | Default values for the form fields. | | schema | AnyObjectSchema | N/A | Yes | Validation schema using yup. | | disabled | boolean | N/A | No | Whether the form is disabled. | | submitButton| SubmitButtonFC | DefaultSubmitButton | No | Custom submit button component. | | options | UseFormProps<T> | {} | No | Additional options for useForm. |

Usage

The Form component is used to create a form context using react-hook-form and yup for validation. It provides a structured way to handle form submission, validation, and error handling.

import React, { SyntheticEvent } from 'react';
import { Form } from '@dawiidio/form';
import { FormInput } from '@dawiidio/form';
import Input from './Input';
import * as yup from 'yup';

export interface MyFormValues {
    firstName: string;
    lastName: string;
}

const schema = yup.object({
    firstName: yup.string().required(),
    lastName: yup.string().required(),
});

const defaultValues = {
    firstName: 'John',
    lastName: 'Doe',
};

export const MyForm = () => {
    const handleSubmit = (data: MyFormValues, event: SyntheticEvent<SubmitEvent, Event> | undefined) => {
        console.log(data);
    };

    return (
        <Form<MyFormValues>
            onSubmit={handleSubmit}
            defaultValues={defaultValues}
            schema={schema}
        >
            <FormInput name="firstName" component={Input} />
            <FormInput name="lastName" component={Input} />
        </Form>
    );
};

useForm

This hook allows you to manipulate data in the form, this ability Form component lacks, because you have no access to hooks like watch or setValue

Props

| Name | Type | Default | Required | Description | |---------------|----------------------------------------------------------------------|------------------------|----------|-----------------------------------------------------------------------------| | onSubmit | (data: TFieldValues, event: SyntheticEvent<SubmitEvent, Event> \| undefined) => void \| Promise<void> | N/A | Yes | Function to handle form submission. | | onCancel | () => void | N/A | No | Function to handle form cancellation. | | onError | () => void | N/A | No | Function to handle form errors. | | defaultValues| DefaultValues<TFieldValues> | N/A | No | Default values for the form fields. | | values | DefaultValues<TFieldValues> | N/A | No | Values for the form fields. | | schema | AnyObjectSchema | N/A | Yes | Validation schema using yup. | | submitButton| SubmitButtonFC | DefaultSubmitButton | No | Custom submit button component. | | disabled | boolean | N/A | No | Whether the form is disabled. |

Usage

The useForm hook is used to create a form context using react-hook-form and yup for validation. It provides a structured way to handle form submission, validation, and error handling.

import React from 'react';
import { useForm } from '@dawiidio/form';
import { FormInput } from '@dawiidio/form';
import Input from './Input';
import * as yup from 'yup';

export interface MyFormValues {
    firstName: string;
    lastName: string;
}

const schema = yup.object({
    firstName: yup.string().required(),
    lastName: yup.string().required(),
});

const defaultValues = {
    firstName: 'John',
    lastName: 'Doe',
};

export const MyForm = () => {
    const { Form } = useForm<MyFormValues>({
        onSubmit: (data) => {
            console.log(data);
        },
        defaultValues,
        schema,
    });

    return (
        <Form>
            <FormInput name="firstName" component={Input} />
            <FormInput name="lastName" component={Input} />
        </Form>
    );
};

FormObject

The FormObject component is used to create a nested form context within a form. It allows you to group form fields under a specific context, making it easier to manage and validate nested form structures.

Props

| Name | Type | Default | Required | Description | |------------|-------------------------------------------|-----------------------------|----------|-----------------------------------------------------------------------------| | name | string | N/A | Yes | The name of the form object. This name will be used to create a nested context for the form fields within this object. | | children | React.ReactNode | N/A | Yes | The form fields or other components that will be nested within this form object. |

Usage

The FormObject component is used to create a nested form context within a form. It allows you to group form fields under a specific context, making it easier to manage and validate nested form structures.

import React from 'react';
import { FormObject } from '@dawiidio/form';
import { FormInput } from '@dawiidio/form';
import Input from './Input';

const MyForm = () => {
    return (
        <Form>
            <FormObject name="user">
                <FormInput name="firstName" component={Input} />
                <FormInput name="lastName" component={Input} />
            </FormObject>
        </Form>
    );
};

FormArray

The FormArray component is a wrapper for an array of fields. It provides context for nested fields and should not be used as a standalone component. It is designed to be used as a foundation for creating custom FormArrayControls components that contain a set of fields and buttons to add/remove/replace/update new entries.

Props

| Name | Type | Default | Required | Description | |------------------------|---------------------|---------|----------|-----------------------------------------------------------------------------| | name | string | N/A | Yes | The name of the form array field. | | minLength | number | N/A | No | Minimum number of items in the array. | | maxLength | number | N/A | No | Maximum number of items in the array. | | required | boolean | N/A | No | Whether the field is required. | | validate | Validate<any, any>| N/A | No | Validation function. | | initialValue | any | N/A | No | Initial value for new entries in the FormArray, not the existing ones. | | setInitialValueOnMount| boolean | N/A | No | Whether to set initial value on mount. If true, the form will be rendered with one empty entry taken from the initialValue prop. |

Usage

The FormArray component is used to manage an array of form fields within a form. It provides context for nested fields and should be used as a foundation for creating custom FormArrayControls components.

import React from 'react';
import { Form, FormArray, FormArrayControls, FormInput } from '@dawiidio/form';
import Input from './Input';
import * as yup from 'yup';

export interface MyFormValues {
    items: Array<{
        myField: string;
    }>;
}

const schema = yup.object({
    items: yup.array().of(
        yup.object({
            myField: yup.string().required(),
        })
    ),
});

const defaultValues = {
    items: [
        { myField: 'defaultValue1' },
        { myField: 'defaultValue2' },
    ],
};

export const MyForm = () => {
    const handleSubmit = (data: MyFormValues) => {
        console.log(data);
    };

    return (
        <Form<MyFormValues>
            onSubmit={handleSubmit}
            defaultValues={defaultValues}
            schema={schema}
        >
            <FormArray name="items" initialValue={{ myField: 'Default value for new entry' }} setInitialValueOnMount>
                <FormArrayControls>
                    {() => (
                        <FormInput name="myField" component={Input} />
                    )}
                </FormArrayControls>
            </FormArray>
        </Form>
    );
};

FormArrayControls

The FormArrayControls component is used to manage an array of form fields within a form. It provides context for nested fields and should be used as a foundation for creating custom FormArrayControls components.

Props

| Name | Type | Default | Required | Description | |-----------------|----------------------------------------------------------------------|--------------------------------------|----------|-----------------------------------------------------------------------------| | children | (controls: Omit<UseFieldArrayReturn, 'fields'>) => ReactNode | N/A | Yes | Form fields wrapped into FormInput, or your raw input components ported with useFormInputController hook. | | entryContainer| FC<PropsWithChildren> | DefaultEntryContainer | No | Container that wraps collection of items (fields), includes also action buttons if actionsVisible is true. | | fieldsContainer| FC<ContainerWithDeleteButtonInfo> | DefaultFieldsContainer | No | Container that wraps only fields passed as children. | | renderActions | (props: UseFieldArrayReturn & { removeEntry: () => void }) => ReactNode | DefaultRenderActions | No | Render function for action buttons. | | container | FormArrayControlsContainer | DefaultFormArrayControlsContainer | No | Root container that wraps all the FormArrayControls component. | | actionsVisible| boolean | true | No | If true, action buttons will be rendered. | | addButton | FormArrayControlsAddButton | DefaultFormArrayControlsAddButton | No | Add button component. |

Usage

The FormArrayControls component is used to manage an array of form fields within a form. It provides context for nested fields and should be used as a foundation for creating custom FormArrayControls components.

import React from 'react';
import { Form, FormArray, FormArrayControls, FormInput } from '@dawiidio/form';
import Input from './Input';
import * as yup from 'yup';

export interface MyFormValues {
    items: Array<{
        myField: string;
    }>;
}

const schema = yup.object({
    items: yup.array().of(
        yup.object({
            myField: yup.string().required(),
        })
    ),
});

const defaultValues = {
    items: [
        { myField: 'defaultValue1' },
        { myField: 'defaultValue2' },
    ],
};

export const MyForm = () => {
    const handleSubmit = (data: MyFormValues) => {
        console.log(data);
    };

    return (
        <Form<MyFormValues>
            onSubmit={handleSubmit}
            defaultValues={defaultValues}
            schema={schema}
        >
            <FormArray name="items" initialValue={{ myField: 'Default value for new entry' }} setInitialValueOnMount>
                <FormArrayControls>
                    {() => (
                        <FormInput name="myField" component={Input} />
                    )}
                </FormArrayControls>
            </FormArray>
        </Form>
    );
};

FormInput

The FormInput component is used to create a form input field within a form. It integrates with react-hook-form to manage form state and validation.

Props

| Name | Type | Default | Required | Description | |---------------|----------------------------------------------------------------------|---------|----------|-----------------------------------------------------------------------------| | name | string | N/A | Yes | The name of the form input field. | | label | string | N/A | No | The label for the form input field. | | component | FC<CommonFormInputProps<T>> \| ForwardRefExoticComponent<CommonFormInputProps<T>> | N/A | Yes | The input component to be rendered. | | defaultValue| T | N/A | No | The default value for the form input field. | | value | T | N/A | No | The value of the form input field. | | disabled | boolean | N/A | No | Whether the form input field is disabled. | | error | string | N/A | No | The error message for the form input field. |

Usage

The FormInput component is used to create a form input field within a form. It integrates with react-hook-form to manage form state and validation.

Below I'm showing how to use it with a custom input component, this is real implementation I use in one of my projects.

import React, {forwardRef, useId} from "react";
import styles from './Input.module.css';
import classNames from "classnames";
import {CommonFormInputProps} from "@dawiidio/form";

export interface InputProps extends CommonFormInputProps<string> {}

export const Input = forwardRef<HTMLInputElement, InputProps>(({ label, error, disabled, ...rest }, ref) => {
    const id = useId();

    return (
        <div className={classNames({
            [styles.container]: true,
            [styles.inputError]: Boolean(error),
        })}>
            <label htmlFor={id} className={classNames('body2', styles.label)}>{label}</label>
            <input id={id} className={classNames(styles.root)} {...rest} ref={ref}/>
            {error && <div className={classNames('caption1', styles.error)}>{error}</div>}
        </div>
    );
});
import React from 'react';
import { Form, FormInput } from '@dawiidio/form';
import Input from './Input';
import * as yup from 'yup';

export interface MyFormValues {
    firstName: string;
    lastName: string;
}

const schema = yup.object({
    firstName: yup.string().required(),
    lastName: yup.string().required(),
});

const defaultValues = {
    firstName: 'John',
    lastName: 'Doe',
};

export const MyForm = () => {
    const handleSubmit = (data: MyFormValues) => {
        console.log(data);
    };

    return (
        <Form<MyFormValues>
            onSubmit={handleSubmit}
            defaultValues={defaultValues}
            schema={schema}
        >
            <FormInput name="firstName" component={Input} />
            <FormInput name="lastName" component={Input} />
        </Form>
    );
};

useFormInputController

The useFormInputController hook is used to create a form input controller within a nested form context. It integrates with react-hook-form to manage form state and validation, and it supports nested form structures by using the FormObjectContext.

Props

| Name | Type | Default | Required | Description | |--------|-------------------------------------------|---------|----------|-----------------------------------------------------------------------------| | name | FieldPath<TFieldValues> | N/A | Yes | The name of the form input field. | | control | Control<TFieldValues> | N/A | Yes | The control object from react-hook-form. | | rules | RegisterOptions<TFieldValues, TName> | N/A | No | Validation rules for the form input field. | | defaultValue | TFieldValues[TName] | N/A | No | The default value for the form input field. |

Usage

The useFormInputController hook is used to create a form input controller within a nested form context. It integrates with react-hook-form to manage form state and validation, and it supports nested form structures by using the FormObjectContext. It is useful for porting custom (fancy) inputs to the form.

import React from 'react';
import {useForm, FormProvider, useFormContext} from 'react-hook-form';
import {FormObjectContext} from '@dawiidio/form';
import {useFormInputController, Form} from '@dawiidio/form';
import SomeFancyComponent from './SomeFancyComponent';

const SomeFancyComponentAsInput = ({name}) => {
    const {
        control
    } = useFormContext();
    const {field, fieldState} = useFormInputController({name, control});
    
    return <SomeFancyComponent
        // on change like that, because react-hooks-form operates on events and takes value from them    
        onChange={(value) => ({target: {value}})} 
        value={field.value}
        error={fieldState.error}
        // ... other props
    />;
};

const MyForm = () => {
    return (
        <Form>
            <SomeFancyComponentAsInput name='field'/>
        </Form>
    );
};

export default MyForm;

Sample port to custom grid and inputs

import {Grid, GridItem} from "~/components/Grid/Grid";
import React, {type FC, type PropsWithChildren} from "react";
import {Input} from "~/components/Input/Input";
import {array, object, string} from "yup";
import {
    Form,
    FormArray,
    FormArrayControls,
    type FormArrayControlsAddButton,
    FormInput,
    FormObject,
    type RenderFormArrayControlsActions,
} from "@dawiidio/form";
import {Button} from "~/components/button/Button";

const schema = object({
    notNestedField: string().required(),
    obj: object({
        objField: string().required(),
    }).required(),
    arr: array().of(object({
       arrField: string().required(),
    }))
});

const AddButton: FormArrayControlsAddButton = ({ addEntry }) => (
    <GridItem colSpan={12}>
        <Button style={{ marginTop: 15 }} onClick={addEntry}>
            Add
        </Button>
    </GridItem>
);
const EntryContainer: FC<PropsWithChildren> = ({ children }) => (
    <GridItem colSpan={12}>
        <Grid alignItems={"end"}>{children}</Grid>
    </GridItem>
);
const FieldsContainer: FC<PropsWithChildren> = ({ children }) => (
    <GridItem colSpan={10}>{children}</GridItem>
);
const renderActions: RenderFormArrayControlsActions = ({
    removeEntry,
    fields,
}) => (
    <GridItem colSpan={2}>
        <Button
            onClick={removeEntry}
            type="button"
            disabled={fields.length === 1}
        >
            Remove
        </Button>
    </GridItem>
);
const FormArrayContainer: FC<PropsWithChildren> = ({ children }) => (
    <GridItem colSpan={12}>
        <Grid>{children}</Grid>
    </GridItem>
);

const SubmitButton: FC = () => (
    <Grid justifyContent='end'>
        <GridItem colSpan={10}/>
        <GridItem colSpan={2}>
            <Button type='submit'>Submit</Button>
        </GridItem>
    </Grid>
)

export default function A() {
    return (
        <Form
            onSubmit={(data) => {
                console.log(data);
            }}
            schema={schema}
            options={{}}
            submitButton={SubmitButton}
        >
            <Grid>
                <GridItem colSpan={12}>
                    <FormInput name="notNestedField" component={Input} />
                </GridItem>
                <GridItem colSpan={12}>
                    <FormObject name="obj">
                        <FormInput name="objField" component={Input} />
                    </FormObject>
                </GridItem>
            </Grid>
            <FormArray name="arr" initialValue={{ arrField: 'Default value' }}>
                <FormArrayControls
                    addButton={AddButton}
                    entryContainer={EntryContainer}
                    fieldsContainer={FieldsContainer}
                    renderActions={renderActions}
                    container={FormArrayContainer}
                    actionsVisible
                >
                    {() => (
                        <GridItem colSpan={12}>
                            <FormInput name="arrField" component={Input} />
                            <FormObject name="obj">
                                <FormInput name="objField" component={Input} />
                            </FormObject>
                        </GridItem>
                    )}
                </FormArrayControls>
            </FormArray>
        </Form>
    );
}