Reputation: 11
Dear frontend enthusiasts.
I am working in a NextJS web that uses Shadcn components, which is another high level layer on top of Radix UI. I am trying to build a page builder that would dynamically load components from Strapi (a CMS), and display them, thus the code will be quite abstract. Which is why I have stumbled upon a weird bug.
Context: I have a button that opens a form, in which there is a Continue button that would open another form.
Expected: After clicking Continue button, the current dialog should close and the next one, which is triggered by the Continue button, would open.
What happens: After clicking Continue button, the current dialog closes and then next one opens but then closes immediately afterward.
How I do it: I pass in a triggerDialog to the button in the form, which should be the trigger to the parent dialog that button is currently in. On clicking this button, beside opening new dialog, it will trigger its parent dialog and close it.
the dialog
'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'
the form
'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>
)
}
the group of buttons in the form (that loads button dialog)
'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>
)
}
When logging the console logs for debugging, i did not notice that the second dialog would close at all, but it does.
Perhaps you have a better idea of how to achieve this beside passing down the trigger of the parent dialog, maybe some dialogs context...? But I did not want to complicate it and I wonder why this method does not work in the first place.
Thank you very much!
I tried logging to debug. I tried looking at the source code of Radix UI and noticed they use some dialog context, but with proper references so it should not close all opened dialogs.
Upvotes: 1
Views: 4884
Reputation: 11
I found an answer:
The issue is incorrectly handled event propogation. I had the same issue and found that the solution is to add onClick={(e) => e.stopPropagation()}
to each the <DialogOverlay />
(you'll need to add this to your ButtonDialog
component if you haven't already), <DialogTrigger>
and <DialogContent>
elements. But only on the internal dialog (your ButtonDialog
).
Upvotes: 1