Skip to content

Latest commit

 

History

History
754 lines (612 loc) · 18.8 KB

README.md

File metadata and controls

754 lines (612 loc) · 18.8 KB

@undermuz/use-form

React library for build forms

Install

npm i @undermuz/use-form

NPM: @undermuz/use-form

Update

npm update @undermuz/use-form

Basic usage

Storybook

Setup form

For example, setup a login form with username and password fields with rules Rules work like: !yourFn(filedValue) && errorText, your should provide an array: [Rule1, Rule2, RuleN] Every rule is array: [yourFnArray, errorText], yourFnArray is array of functions: yourFn(filedValue: unknown) => boolean

This example uses built-in JS function Boolean to validate input's values

    const formConfig: IUseFormSettings = {
        fields: {
            username: {
                label: "Login",
                rules: [[[Boolean], "Username is required"]],
            },
            password: {
                label: "Password",
                rules: [[[Boolean], "Password is required"]],
            },
        },
    }

    ...

    const form = useForm(formConfig)

You should wrapp your inputs and components witch use form-hooks by FormContext.Provider

    <FormContext.Provider value={form}>
    ...
    </FormContext.Provider>

Connect input-like components to the form

To connect any form component (or just a component) you have to wrapp it by ConnectToForm and provide a name

Provided component have to receive value prop, onChange prop and call onChange with new value

    <ConnectToForm name="FIELD_NAME">
        {/*Your component*/}
    </ConnectToForm>

ConnectToForm provides current field's value to your component, and wait new value through onChange

Browser's input

Storybook: browsers inputs example

    type InputProps = Partial<IConnectedProps> & {
        type?: string
        placeholder?: string
    }

    //Short-version
    const FormInputV1: React.FC<InputProps> = ({
        inputProps = {}, //Provides by ConnectToForm
        ...rest
    }) => {
        return (
            <label style={styles}>
                {inputProps.label}:
                <input {..._.pick(rest, ["type", "placeholder"])} {...inputProps} />
            </label>
        )
    }

    //Full-version
    const FormInputV2: React.FC<InputProps> = (props) => {
        const {
            type = "text",
            placeholder = "",
            label, //Provides by ConnectToForm
            name, //Provides by ConnectToForm
            value, //Provides by ConnectToForm
            onChange, //Provides by ConnectToForm
            onBlur, //Provides by ConnectToForm
        } = props

        return (
            <div style={styles}>
                <label htmlFor={name}>{label}:</label>
                <input
                    type={type}
                    id={name}
                    name={name}
                    value={value}
                    placeholder={placeholder}
                    onChange={(e) => onChange?.(e.target.value)}
                    onBlur={() => onBlur?.()}
                />
            </div>
        )
    }

    ...
    
    <FormContext.Provider value={form}>
        <ConnectToForm name="username">
            <FormInputV1 placeholder="Enter your login" />
        </ConnectToForm>

        <ConnectToForm name="password">
            <FormInputV2
                type="password"
                placeholder="Enter your password"
            />
        </ConnectToForm>
    </FormContext.Provider>

Ui framework's input

Storybook: chakra-ui example

    type InputProps = Partial<IConnectedProps> & {
        type?: string
        placeholder?: string
        description?: string
    }

    const FormField: FC<PropsWithChildren<InputProps>> = (props) => {
        const {
            label,
            description = null,
            errors, //Provides by ConnectToForm
            children,
            hasError = false, //Provides by ConnectToForm
        } = props

        return (
            <FormControl isInvalid={hasError}>
                <FormLabel>{label}</FormLabel>

                {children}

                {description !== null && !hasError && (
                    <FormHelperText>{description}</FormHelperText>
                )}

                {errors?.map((errorText, index) => {
                    if (typeof errorText !== "string") {
                        return null
                    }

                    return (
                        <FormErrorMessage key={index}>{errorText}</FormErrorMessage>
                    )
                })}
            </FormControl>
        )
    }

    const FormInput: React.FC<InputProps> = (props) => {
        const {
            type = "text",
            placeholder = "",
            value, //Provides by ConnectToForm
            onChange, //Provides by ConnectToForm
            onBlur, //Provides by ConnectToForm
        } = props

        return (
            <FormField {...props}>
                <Input
                    type={type}
                    placeholder={placeholder}
                    onChange={(e) => onChange?.(e.target.value)}
                    onBlur={() => onBlur?.()}
                    value={value}
                />
            </FormField>
        )
    }

    const ErrorBlock = () => {
        const errors = useFormErrors()
        const fields = useFormFields()

        return (
            <>
                {Object.keys(errors).map((name) => (
                    <>
                        <p key={name}>{fields?.[name] || name}:</p>
                        <ul>
                            {errors[name].map((error, i) => (
                                <li key={i}>{error as string}</li>
                            ))}
                        </ul>
                    </>
                ))}
            </>
        )
    }

    ...

    <VStack alignItems={"flex-start"}>
        <ConnectToForm name="username">
            <FormInput placeholder="Enter your login" />
        </ConnectToForm>

        <ConnectToForm name="password">
            <FormInput
                type="password"
                placeholder="Enter your password"
            />
        </ConnectToForm>

        <FormSubmit
            as={Button}
            onSend={onSend}
            onSucceed={onSucceed}
        >
            {(status: EnumFormSubmitStatus) => {
                if (status === EnumFormSubmitStatus.Sending) {
                    return "Sending..."
                }

                return "Send"
            }}
        </FormSubmit>

        <IfForm hasErrors>
            <ErrorBlock />
        </IfForm>
    </VStack>

Third-party components

Storybook: Date-picker example

    const formConfig: IUseFormSettings = {
        fields: {
            date: {
                label: "Date picker",
                rules: [[[Boolean], "Date is required"]],
                initialValue: new Date(),
            },
            rangeDates: {
                label: "Date picker: Range",
                rules: [[[Boolean], "Username is required"]],
                initialValue: [new Date(), new Date()],
            },
        },
    }

    const form = useForm(formConfig)
    
    ...

    type InputProps = Partial<IConnectedProps> & {
        type?: string
        placeholder?: string
        description?: string
    }

    const FormField: FC<PropsWithChildren<InputProps>> = (props) => {
        const {
            label,
            description = null,
            errors, //Provides by ConnectToForm
            children,
            hasError = false, //Provides by ConnectToForm
        } = props

        return (
            <FormControl isInvalid={hasError}>
                <FormLabel>{label}</FormLabel>

                {children}

                {description !== null && !hasError && (
                    <FormHelperText>{description}</FormHelperText>
                )}

                {errors?.map((errorText, index) => {
                    if (typeof errorText !== "string") {
                        return null
                    }

                    return (
                        <FormErrorMessage key={index}>{errorText}</FormErrorMessage>
                    )
                })}
            </FormControl>
        )
    }

    const FormDatePicker: React.FC<InputProps & { isRange?: boolean }> = (
        props
    ) => {
        const {
            isRange = false,
            name, //Provides by ConnectToForm
            value, //Provides by ConnectToForm
            onChange, //Provides by ConnectToForm
        } = props

        return (
            <FormField {...props}>
                {!isRange && (
                    <SingleDatepicker
                        name={name}
                        date={value}
                        onDateChange={(date) => onChange?.(date)}
                    />
                )}

                {isRange && (
                    <RangeDatepicker
                        name={name}
                        selectedDates={value}
                        onDateChange={(date) => onChange?.(date)}
                    />
                )}
            </FormField>
        )
    }

    ...

    <ConnectToForm name="date">
        <FormDatePicker />
    </ConnectToForm>

    <ConnectToForm name="rangeDates">
        <FormDatePicker isRange />
    </ConnectToForm>

Input's states

    const Input: React.FC<IConnectedProps> = ({
        inputProps = {}, //Provides by ConnectToForm
        label, //Provides by ConnectToForm
        errors, //Provides by ConnectToForm
        isSucceed, //Provides by ConnectToForm
        hasError, //Provides by ConnectToForm
        isFocused, //Provides by ConnectToForm
        isTouched, //Provides by ConnectToForm
        isFilled, //Provides by ConnectToForm
        isDisabled //Provides by ConnectToForm
        ...rest // You've provided
    }) => {
        return (
            <label>
                {label}

                <input
                    {..._.pick(rest, ["type", "placeholder", "etc"])}
                    {...inputProps}
                    className={isSucceed ? "succeed" : hasError ? "has-error" : "default"}
                />

                {/*  Other states */}
                {isFocused && "Tip: type something funny"}
                {isTouched && "You've already touched this field"}
                {isFilled && "You've already filled this field"}
                {isDisabled && "This field is disabled"}

                {/* Field errors */}
                {hasError && <>
                    <span>Errors:</span>
                    <ul>
                        {errors.map((error: string, i: number) => (
                            <li key={i}>{error}</li>
                        ))}
                    </ul>
                </>}
            </label>
        )
    }

Form's states

    <IfForm>
        <p>Show when form is default</p>
    </IfForm>

    <IfForm isSuccess>
        <p>Form has been sent success</p>
    </IfForm>

    <IfForm isCanceling>
        <p>Form has sent unsuccess</p>
    </IfForm>

    <IfForm isSending>
        <p>Form is sending now</p>
    </IfForm>

    <IfForm hasErrors>
        <p>Form has errors</p>
    </IfForm>

You can get form's values and errors directly through form variable:

    const form = useForm(/*Form config*/)

    const { values, errors } = form

    useEffect(() => {
        console.log("[Form][Values]", values)
    }, [values])

    useEffect(() => {
        console.log("[Form][Errors]", errors)
    }, [errors])

Or by context inside FormContext.Provider:

    const { values, errors } = useFormContext()

    useEffect(() => {
        console.log("[Form][Values]", values)
    }, [values])

    useEffect(() => {
        console.log("[Form][Errors]", errors)
    }, [errors])

Submit

Create callbacks

    const form = useForm(/*Form config*/)

    ...

    const onSend = useCallback(async (values: IValues) => {
        console.log("Login data", values)

        await sendValuesToTheServer(values)
    }, [])

    const onSucceed = useCallback(() => {
        console.log("Login completed")
    }, [])

    const onError = useCallback(() => {
        console.log("Login failed")
    }, [])

    const submit = useFormSubmit(onSend, onSucceed, onError)

Get submit callback by hook

    const submit = useFormSubmit(onSend, onSucceed, onError)

    ...

    <Button disabled={form.isSending || form.isCanceling || form.hasErrors} onClick={submit}>
        Submit
    </Button>

OR Get submit by component

    //Component version

    <FormSubmit onSend={onSend} onSucceed={onSucceed} onError={onError}>
        {(status: EnumFormSubmitStatus) => {
            if (status === EnumFormSubmitStatus.Sending) {
                return "Sending..."
            }

            if (status === EnumFormSubmitStatus.Canceling) {
                return "Failed"
            }

            if (status === EnumFormSubmitStatus.Succeed) {
                return "Succeed"
            }

            return "Submit"
        }}
    </FormSubmit>

Controlled form

You can control form's values from outside by providing value and onChange to useForm's config

const [value, onChange] = useState<IValues>(() => {
    return {
        username: "",
        password: ""
    }
})

const form = useForm({
    fields: {
        username: {
            label: "Login",
            rules: [[[Boolean], "Username is required"]],
        },
        password: {
            label: "Password",
            rules: [[[Boolean], "Password is required"]],
        },
    },
    value,
    onChange
})

You can get more control

/*
    FORM_ACTIONS = {
        SET_VALUES
        SET_VALUE
        SET_TESTS
        SET_TOUCHED_FIELD
        SET_TOUCHED
        SET_ERRORS
        SET_FIELDS
        SET_VALIDATE
        SET_IS_SENDING
        SET_IS_CANCELING
        SET_IS_SUCCESS
        SET_SEND_ERROR
        VALIDATE_FORM
        SEND_FORM
    }
*/

const [formConfig, formState] = useFormCoreParams({
    fields: {
        username: {
            label: "Login",
            rules: [[[Boolean], "Username is required"]],
        },
        password: {
            label: "Password",
            rules: [[[Boolean], "Password is required"]],
        },
    },
    options: {
        middlewares: [
            /* --> */createYourCustomMiddleware()/* <-- */
        ]
    }
})

const form = useFormCore(formConfig, formState)

const setValue = useCallback((values: any) => {
    if (isDebug) console.log("[useCustomForm][setValue]", values)

    formState.dispatch({
        type: FORM_ACTIONS.SET_VALUES,
        payload: {
            values,
            /* --> */yourCustomPayload: "some-additional-info"/* <-- */
        },
    })
}, [])

You can get even more control

    const useCustomFormState = (props: IFormConfig): FormState => {
        const initialState = useMemo(() => getInitialState(props), [])

        const middlewares = useMemo(
            () => [
                ...(props?.middlewares || []),

                /* --> */YOUR_CUSTOM_MIDDLEWARE(props)/* <-- */,

                /*  You can remove default middlewares: */
                //createValidating(props),
                //createSend(props),
            ],
            []
        )

        const [state, dispatch, store] = useReducer<IFormState>(
            formReducer,
            initialState,
            middlewares
        )

        return { state, dispatch, store }
    }

    const useCustomFormParams = (
        formSettings: IUseFormSettings
    ): [IFormConfig, FormState] => {
        const formConfig = useFormConfigBySettings(formSettings)
        const formState = useCustomFormState(formConfig)

        return [formConfig, formState]
    }

    ...
        const [formConfig, formState] = useCustomFormParams({
            fields: {
                username: {
                    label: "Login",
                    rules: [[[Boolean], "Username is required"]],
                },
                password: {
                    label: "Password",
                    rules: [[[Boolean], "Password is required"]],
                },
            }
        })

        const form = useFormCore(formConfig, formState)
    ...

Examples

const Input: React.FC<IConnectedProps> = ({
    type = "text",
    placeholder = "",
    onChange,
    label,
    value,
}) => {
    return (
        <>
            <label>{label}</label>
            <InputGroup fullWidth size="Small">
                <input
                    type={type}
                    placeholder={placeholder}
                    onChange={(e) => onChange?.(e.target.value)}
                    value={value}
                />
            </InputGroup>
        </>
    )
}

const ErrorBlock = () => {
    const errors = useFormErrors()
    const fields = useFormFields()

    return (
        <>
            {Object.keys(errors).map((name) => (
                <>
                    <p key={name}>{fields?.[name] || name}:</p>
                    <ul>
                        {errors[name].map((error: string, i: number) => (
                            <li key={i}>{error}</li>
                        ))}
                    </ul>
                </>
            ))}
        </>
    )
}

const LoginForm = () => {
    const form = useForm({
        fields: {
            username: {
                label: "Login",
                rules: [[[Boolean], "Username is required"]],
            },
            password: {
                label: "Password",
                rules: [[[Boolean], "Password is required"]],
            },
        },
    })

    const onSend = useCallback((values: IValues) => {
        console.log("Login data", values)
    }, [])

    const onSucceed = useCallback(() => {
        console.log("Login completed")
    }, [])

    return (
        <FormContext.Provider value={form}>
            <ConnectToForm name="username">
                <Input placeholder="Enter your login" />
            </ConnectToForm>

            <ConnectToForm name="password">
                <Input type="password" placeholder="Enter your password" />
            </ConnectToForm>

            <FormSubmit onSend={onSend} onSucceed={onSucceed}>
                {(status: EnumFormSubmitStatus) => {
                    if (status === EnumFormSubmitStatus.Sending) {
                        return "Sending..."
                    }

                    return "Send"
                }}
            </FormSubmit>

            <IfForm hasErrors>
                <ErrorBlock />
            </IfForm>
        </FormContext.Provider>
    )
}