亲爱的前端爱好者。
我正在使用 Shadcn 组件的 NextJS Web 中工作,这是 Radix UI 之上的另一个高级层。我正在尝试构建一个页面构建器,它可以从 Strapi(CMS)动态加载组件并显示它们,因此代码将非常抽象。这就是为什么我偶然发现了一个奇怪的错误。
上下文:我有一个打开表单的按钮,其中有一个可以打开另一个表单的继续按钮。
预期:单击“继续”按钮后,当前对话框应关闭,并打开由“继续”按钮触发的下一个对话框。
发生了什么:单击“继续”按钮后,当前对话框关闭,然后下一个对话框打开,但随后立即关闭。
我是如何做到的:我向表单中的按钮传递一个triggerDialog,它应该是该按钮当前所在的父对话框的触发器。单击此按钮时,除了打开新对话框之外,还将触发其父对话框关闭它。
对话框
'use client'
import * as React from 'react'
import { useEffect } from 'react'
import { Button as ShadcnButton } from '@/components/ui/button'
import { buttonDialogVariants } from './button'
import { ButtonButtonComponent, FormResponseDataObject } from '@/api/generated'
import { cn } from '@/lib/utils'
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import { FormComponent } from '../form/form'
import { formSchema } from '../form/form'
export interface ButtonProps extends ButtonButtonComponent {
asChild?: boolean
className?: string
formObject?: any
triggerDialog?: any
}
export const ButtonDialog = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, formObject, type, ...props }, ref) => {
const [open, setOpen] = React.useState(false)
const submitType = type === 'submit' ? 'submit' : 'button'
const selfID = React.useRef(Math.random().toString(36).substring(7)).current;
useEffect(() => {
console.log('button dialog on mount: ' + selfID + " state: " + open)
}, [])
const setOpenChange = (bool: boolean, msg?: string) => {
if (props.form?.data?.attributes?.title === "Just one step to go") {
setOpen(true)
}
if (formObject) {
if (formSchema.safeParse(formObject.getValues()).success) {
setOpen(bool)
} else {
setOpen(false)
}
} else {
setOpen(bool)
}
}
// this is a button to open another form and all data from this form
// should be passed to the new form
return (
<Dialog open={open} onOpenChange={setOpenChange}>
<DialogTrigger asChild={true}>
<ShadcnButton
type={submitType}
className={cn(buttonDialogVariants({ variant, size, className }))}
ref={ref}
>
{props.label}
</ShadcnButton>
</DialogTrigger>
<DialogContent className='flex flex-row flex-wrap gap-4 rounded-xl'>
{props.form?.data && (
<FormComponent
testingData={open}
triggerDialog={setOpenChange}
data={formObject?.getValues()}
{...(props.form.data as FormResponseDataObject)}
className='w-full'
/>
)}
</DialogContent>
</Dialog>
)
}
)
ButtonDialog.displayName = 'ButtonDialog'
表格
'use client'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { FormResponseDataObject } from '@/api/generated'
import { FormFieldComponent } from '@/components/form/fields/form-field'
import { Form } from '@/components/ui/form'
import { FormService } from '@/api/generated'
import { Title } from '../basic'
import { GroupOfButtons } from '../button/group-of-buttons'
import { DynamicZone } from '../dynamic-zone'
type Props = FormResponseDataObject & {
className?: string
data?: any
triggerDialog?: any
testingData?: any
}
export const formSchema = z
.object({
name: z.string({
required_error: 'Please enter your name',
}),
email: z.string().email({
message: 'Please enter a valid email',
}),
message: z.string(),
dialCode: z.string(),
phone: z
.string()
.min(9, {
message: 'Your phone number must be at least 9 digits',
})
.max(10, {
message: 'Your phone number must be at most 10 digits',
}),
field: z.any(),
url: z.string().url(),
file: z.any(),
invoicesEstimation: z.any(),
accountingSystem: z.any(),
})
.partial()
export const FormComponent = ({ id, className, data, ...props }: Props) => {
const [formData, setFormData] = useState<FormResponseDataObject | undefined>(undefined)
// generate a random id for debugging
const selfID = React.useRef(Math.random().toString(36).substring(7)).current;
useEffect(() => {
const fetchFormStructure = async () => {
const form = await FormService.getFormsId({ id, populate: 'fields,buttons.form' })
setFormData(form.data)
}
fetchFormStructure()
console.log('form component on mount: ' + selfID)
console.log(props.testingData)
}, [id])
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: data,
})
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log('Form submitted ', selfID)
console.log(data)
props.triggerDialog?.(false, "fudge you")
}
return (
<div className={cn(className, 'flex flex-row')}>
<div className='flex flex-col gap-8 w-full'>
<div className='flex flex-col'>
{formData?.attributes?.title && <Title title={formData.attributes.title} size='h3' />}
{formData?.attributes?.description && (
<p className='text-start md:text-center text-grey-600'>{formData.attributes.description}</p>
)}
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col gap-4'>
{formData?.attributes?.fields?.map(field => {
if (!field) {
return null
}
const { id, ...props } = field
return <FormFieldComponent key={id} form={form} {...props} />
})}
<GroupOfButtons
triggerDialog={props.triggerDialog}
formObject={form}
className='mt-10 w-full'
buttons={formData?.attributes?.buttons}
/>
</form>
</Form>
{formData?.attributes?.media && <DynamicZone components={formData.attributes.media} />}
</div>
</div>
)
}
表单中的按钮组(加载按钮对话框)
'use client'
import { cn } from '@/lib/utils'
import { Button } from './button'
import { ButtonVideo } from './button-video'
import { ButtonDialog } from './button-dialog'
import { ButtonGroupOfButtonsComponent } from '@/api/generated'
type Props = ButtonGroupOfButtonsComponent & {
className?: string
triggerDialog?: any
formObject?: any
}
export const GroupOfButtons = ({ className, buttons, formObject, ...props }: Props) => {
return (
<div className={cn('flex flex-wrap justify-start gap-4 md:justify-between md:gap-8', className)}>
{buttons?.map((button, index) => {
if (!button) {
return null
}
if (button.form?.data) {
return (
<ButtonDialog
formObject={formObject}
key={index}
// triggerDialog={props.triggerDialog}
{...button}
/>
)
}
if (button.link?.includes('youtube')) {
return <ButtonVideo key={index} {...button} />
}
return (
<Button key={index} {...button}>
{button.label}
</Button>
)
})}
</div>
)
}
在记录控制台日志以进行调试时,我没有注意到第二个对话框根本会关闭,但它确实会关闭。
除了传递父对话框的触发器(也许是一些对话框上下文)之外,也许您对如何实现这一点有更好的想法......?但我不想让它变得复杂,我想知道为什么这个方法一开始不起作用。
非常感谢!
我尝试记录日志以进行调试。我尝试查看 Radix UI 的源代码,发现它们使用了一些对话框上下文,但具有正确的引用,因此它不应关闭所有打开的对话框。
我找到了答案: 该问题是事件传播处理不当。我遇到了同样的问题,发现解决方案是将
onClick={(e) => e.stopPropagation()}
添加到每个 <DialogOverlay />
(如果还没有,您需要将其添加到您的 ButtonDialog
组件中)、<DialogTrigger>
和 <DialogContent>
元素。但仅限于内部对话框(您的ButtonDialog
)。