nextui-org / nextui

🚀 Beautiful, fast and modern React UI library.
https://nextui.org
MIT License
21.94k stars 1.53k forks source link

[BUG] - INVALID INPUT BLOCKING FORM SUBMIT #2977

Closed Vitu-77 closed 6 months ago

Vitu-77 commented 6 months ago

NextUI Version

latest

Describe the bug

When Input prop "isInvalid" changes to true, the form submit event is being blocked and not triggered

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

  1. Create a form
  2. Add a input to the form
  3. Create a submitFunction that validates form data and set some error state(s) to true when data is invalid
  4. Add prop "isInvalid" in the Inputs based on the error values. Ex: <Input isInvalid={error.password !== null} />
  5. Submit form

When some validation error ocurs, it will change Input states to invalid and the submit function will not be trigerred anymore, and how validation is being treated inside submit function, the form state will freeze.

Expected behavior

As a user, i expect that submit function could be trigerred however the input states.

Screenshots or Videos

Form Code

image

Behavior example

https://github.com/nextui-org/nextui/assets/57647656/fc77cb22-0a1f-454e-a49d-a4cb23217471

Operating System Version

Windows

Browser

Chrome

linear[bot] commented 6 months ago

ENG-814 [BUG] - INVALID INPUT BLOCKING FORM SUBMIT

wingkwong commented 6 months ago

duplicate - https://github.com/nextui-org/nextui/issues/2844

as a workaround for the time being, you can use validate to update the state. I'm currently looking into this issue.

wingkwong commented 6 months ago

@Vitu-77 can you also paste the code here? so that I could have one more example to test.

Vitu-77 commented 5 months ago

Login Form Component:

import { useCallback, useMemo, useState } from 'react'
import { LuEye, LuEyeOff, LuLock, LuLogIn, LuUser } from 'react-icons/lu'
import { useQuery } from 'react-query'
import { Link } from 'react-router-dom'
import { z } from 'zod'
import { Button, Checkbox, Input } from '@nextui-org/react'

import { useForm } from '@core/hooks'
import { QueryKeysEnum, RoutesEnum } from '@domain/enums'
import { signIn } from '@infra/services/requests/auth'
import { SignInPayload } from '@infra/services/requests/auth.dto'

const DEFAULT_PAYLOAD: SignInPayload = {
    email: '',
    password: '',
}

const LoginForm = () => {
    const [visiblePass, setVisiblePass] = useState(false)
    const togglePassVisibility = useCallback(() => {
        setVisiblePass((prev) => !prev)
    }, [])

    const [payload, setPayload] = useState<SignInPayload>(DEFAULT_PAYLOAD)

    const query = useQuery([QueryKeysEnum.SIGN_IN, payload], {
        enabled: !!payload.email && !!payload.password,
        queryFn: () => signIn(payload),
        // onSuccess: (data) => console.log(data),
        // onError: (error) => console.log(error),
    })

    const passInputIcon = useMemo(() => {
        const Icon = visiblePass ? LuEyeOff : LuEye
        return (
            <Icon
                onClick={togglePassVisibility}
                className='text-2xl text-foreground-400 cursor-pointer hover:text-foreground-700'
            />
        )
    }, [togglePassVisibility, visiblePass])

    const [formRef, formErrors] = useForm({
        validationSchema: z.object({
            email: z
                .string()
                .min(1, 'Informe seu email ou CPF')
                .email('CPF ou email inválido'),
            password: z.string().min(1, 'Informe sua senha'),
        }),
        onSubmit: (data) => {
            if (data) {
                setPayload(data)
            }
        },
    })

    return (
        <div className='w-full flex flex-col gap-6 items-center'>
            <form ref={formRef} className='w-full flex flex-col gap-2'>
                <Input
                    defaultValue='marcelo.victor05@gmail.com'
                    name='email'
                    radius='sm'
                    isDisabled={query.isLoading}
                    isInvalid={!!formErrors?.email}
                    errorMessage={formErrors?.email}
                    label={<span className='text-foreground-400'>Email ou CPF</span>}
                    variant='bordered'
                    placeholder='Informe seu email ou CPF'
                    startContent={<LuUser className='text-lg text-foreground-200 mr-2' />}
                    labelPlacement='outside'
                />
                <Input
                    defaultValue='Arsenalvic12345'
                    name='password'
                    className='w-full'
                    radius='sm'
                    isDisabled={query.isLoading}
                    label={<span className='text-foreground-400'>Senha</span>}
                    type={visiblePass ? 'text' : 'password'}
                    variant='bordered'
                    isInvalid={!!formErrors?.password}
                    errorMessage={formErrors?.password}
                    placeholder='Entre com sua senha de acesso'
                    endContent={passInputIcon}
                    startContent={<LuLock className='text-lg text-foreground-200 mr-2' />}
                    labelPlacement='outside'
                    description={
                        <div className='w-full flex justify-end relative'>
                            <small className='text-xs text-primary cursor-pointer hover:text-primary-600 absolute right-0 top-[-64px] hover:underline'>
                                Esqueceu a senha?
                            </small>
                        </div>
                    }
                />

                <Checkbox size='md' color='primary' radius='sm' className='mb-1.5'>
                    <span className='text-foreground-500 text-sm'>
                        Manter-me conectado
                    </span>
                </Checkbox>

                <Button
                    type='submit'
                    radius='sm'
                    color='primary'
                    isLoading={query.isLoading}
                    className='flex items-center'
                >
                    Entrar
                    <LuLogIn className='text-lg' />
                </Button>
            </form>

            <span className='w-full text-center text-sm text-foreground-500'>
                Ainda não possui uma conta?{' '}
                <Link
                    className='text-primary-500 hover:underline hover:text-primary-600'
                    to={RoutesEnum.REGISTER}
                >
                    Registre-se
                </Link>
            </span>
        </div>
    )
}

export default LoginForm

My useForm Hook:

import { useCallback, useEffect, useRef, useState } from 'react'
import { ZodError, ZodObjectDef, ZodType } from 'zod'

type ZodSchema<DataType> = ZodType<DataType, ZodObjectDef>
type ValidationError<DataType> = Partial<Record<keyof DataType, string>>

type Props<DataType> = {
    validationSchema?: ZodSchema<DataType>
    onSubmit: (data: DataType | null) => any | Promise<any>
}

type Response<DataType> = [
    React.MutableRefObject<HTMLFormElement | null>,
    ValidationError<DataType> | null,
]

export default function useForm<DataType>({
    validationSchema,
    onSubmit,
}: Props<DataType>): Response<DataType> {
    const ref = useRef<HTMLFormElement | null>(null)
    const [error, setError] = useState<ValidationError<DataType> | null>(null)

    const getData = useCallback(() => {
        if (!ref.current) {
            return null
        }

        const formValue: Record<string, any> = {}
        const formData = new FormData(ref.current)

        for (const [key, value] of Array.from(formData.entries())) {
            formValue[key] = value
        }

        return validationSchema
            ? validationSchema.parse(formValue)
            : (formValue as DataType)
    }, [validationSchema])

    const handleFormSubmit = useCallback(
        (e: SubmitEvent) => {
            e.preventDefault()

            try {
                const data = getData()
                setError(null)
                return onSubmit(data)
            } catch (err) {
                const e = err as ZodError<DataType>
                const errorResponse = {} as ValidationError<DataType>
                const formError = e.errors.reduce(
                    (e, item) => ({
                        ...e,
                        [item.path[0]]: item.message,
                    }),
                    errorResponse
                )

                setError(formError)
                return onSubmit(null)
            }
        },
        [getData, onSubmit]
    )

    useEffect(() => {
        const form = ref.current

        if (form) {
            form.addEventListener('submit', handleFormSubmit)
            return () => form.removeEventListener('submit', handleFormSubmit)
        }
    }, [error, handleFormSubmit])

    return [ref, error]
}