Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem displaying custom AlertDialog (shadcn) in Next 14 / app router #20

Open
dandubya opened this issue Jan 19, 2025 · 4 comments
Open

Comments

@dandubya
Copy link

As the title says, I'm using Next 14 w/ the app router, and want to protect a specific page from having unsaved changes accidentally discarded when the user navigates away.

I am wrapping all child elements in my layout.tsx file with the NavigationGuardProvider element.

I have a page/component where the user can modify data. Let's call it EditView.tsx.

EditView.tsx includes the component whose source is below (as SaveAlertDialog.tsx), added as:

<SaveAlertDialog changed={dataChanged} />

dataChanged is a useState<boolean> in the EditView page component whose value is initially false; when the user changes data, I have a callback that sets dataChanged to true using setDataChanged(true). This is working, because I have on-screen elements that toggle on/off (like a save button) when dataChanged is true.

As you can see, my SaveAlertDialog component fully encapsulates the logic of using the NavigationGuardContext via the hook useNavigationGuard, and takes a boolean to indicate whether the data on the page has changed. The intent is to open the AlertDialog when the navGuard decides a user is navigating away and the data is in a changed state.

I've verified that I'm getting updates to the changed variable. But it doesn't appear that navGuard.active ever toggles to true.

'use client';

import {
    AlertDialog,
    AlertDialogAction,
    AlertDialogCancel,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
  } from "@/components/shared/ui/alert-dialog";
import { useNavigationGuard } from "next-navigation-guard";
import { useEffect, useState } from "react";
  
export default function SaveAlertDialog({
  changed,
} : {
  changed: boolean,
}) {
    const [enabled, setEnabled] = useState(changed);

    const navGuard = useNavigationGuard({
      enabled, 
      confirm: undefined
    });

    useEffect(() => {
      setEnabled(changed);
    },[changed])

    return (
      <AlertDialog open={navGuard.active}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>The data will be lost.</AlertDialogTitle>
            <AlertDialogDescription>
              Are you sure you want to leave the page?
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel onClick={navGuard.reject}>
              No
            </AlertDialogCancel>
            <AlertDialogAction className=" text-white"  onClick={navGuard.accept}>
              Yes
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    );
  };

I've looked over the example app provided with this module, and I can't see anything obvious that I'm doing differently.

The current symptom is that a built-in browser confirmation dialog displays whenever the user tries to navigate away and the data has changed. I want it to display the custom AlertDialog component instead.

I've tried instantiating useNavigationGuard both with and without the confirm callback prop.

I'd appreciate any advice on getting this working.

Thanks in advance!

@thatzacdavis
Copy link

I also hit this when attempting to fork the library for Next13.

@abdullah-bahraq
Copy link

abdullah-bahraq commented Jan 23, 2025

I've implemented it like this in my project, and it works. It might help you
i used dialog instead of alert from shadcn

const EditOrder = ({ data, orderId }: Props) => {
  const [enabled, setEnabled] = React.useState(false);

  const navGuard = useNavigationGuard({ enabled });

  const onSubmit = async (data: OrderFormValues) => {
    setEnabled(false);

      try {
       ...
      } catch (error: any) {
        setEnabled(true);
       ...
      }
    });
  };

  return (
    <>
      <OrderForm
        ...
        onChange={(data) => {
          data && setEnabled(true);
        }}
      />

      <Dialog
        open={navGuard.active}
        onOpenChange={(open) => {
          if (!open) {
            navGuard.reject();
          } else {
            navGuard.accept();
          }
        }}
      >
        <DialogContent>
          <DialogHeader className="gap-3">
            <DialogTitle>{t('Orders.Dialog.title')}</DialogTitle>
            <DialogDescription>
              {t('Orders.Dialog.description')}
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button
              onClick={() => {
                setEnabled(false);
                navGuard.reject();
                formRef.current?.requestSubmit();
              }}
            >
              {t('Orders.Dialog.confirm')}
            </Button>
            <Button variant="link" onClick={navGuard.accept}>
              {t('Orders.Dialog.discard')}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
};

export default EditOrder;

@jackstenglein
Copy link

I was also facing this and then realized that navGuard.active is only triggered when doing a client-side navigation. You can see this in the demo as well: https://layerxcom.github.io/next-navigation-guard. Toggle the Use Async Confirm button and then close the tab. The browser-default dialog will pop up instead of the custom confirmation.

Based on the MDN beforeunload event docs, there's no way around this limitation. However, you can still ensure that all links on your page are using client-side routing, so that your custom dialog appears when clicking those.

@thatzacdavis
Copy link

@jackstenglein that is exactly what I found out as well. Ended up having to build a wrapper component for both internal and external links and firing our own modal dialog instead. I don't think the approach used in this library is valid anymore unfortunately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants