Repository: Rezact/Rezact (ts)

Issue:
Router onBeforeLeave Hook
This will provide a way to cancel navigation if a user may lose progress on their work if they navigate away, such as filling out a form
```javascript
router.route.onBeforeLeave((to, from) => return false)
```

Pull request description:
close #63 add onBeforeLeave Hook function to router
close #63 

```tsx
import { MyLayout } from "../Layout/nestedLayout";

export function Page({ router }) {
  router.onBeforeLeave(() => {
    if ($chkVal === false) return false;
  });

  let $chkVal = false;
  return (
    <>
      <h1>On Before Leave</h1>
      <div>
        <input id="allow-leaving" type="checkbox" value={$chkVal} />
        <label for="allow-leaving">Allow Leaving This Route</label>
      </div>
    </>
  );
}

export const Layout = MyLayout;

```

Patch:
diff --git a/src/components/appRouter.ts b/src/components/appRouter.ts
index 9be0dea..54aa40e 100644
--- a/src/components/appRouter.ts
+++ b/src/components/appRouter.ts
@@ -139,6 +139,11 @@ const routes = [
     component: () => import("src/components/HomePage"),
     title: "Testing Root Title",
   },
+  {
+    path: "/onBeforeLeave",
+    component: () => import("src/examples/OnBeforeLeave/OnBeforeLeave"),
+    title: "Testing On Before Leave Title",
+  },
   {
     path: "/users2",
     component: () => import("src/examples/NestedRoutes/Users"),
diff --git a/src/examples/Layout/nestedLayout.tsx b/src/examples/Layout/nestedLayout.tsx
index 3dacd6b..30c0795 100644
--- a/src/examples/Layout/nestedLayout.tsx
+++ b/src/examples/Layout/nestedLayout.tsx
@@ -165,6 +165,9 @@ export function MyLayout({ router }: routerProp) {
         <li>
           <a href="/jsx-signals">JSX Signals</a>
         </li>
+        <li>
+          <a href="/onBeforeLeave">On Before Leave Test</a>
+        </li>
         <li>
           <a href="/asdfasdfasdfasdf">A Route that does not exist</a>
         </li>
diff --git a/src/examples/OnBeforeLeave/OnBeforeLeave.tsx b/src/examples/OnBeforeLeave/OnBeforeLeave.tsx
new file mode 100644
index 0000000..9b727d4
--- /dev/null
+++ b/src/examples/OnBeforeLeave/OnBeforeLeave.tsx
@@ -0,0 +1,20 @@
+import { MyLayout } from "../Layout/nestedLayout";
+
+export function Page({ router }) {
+  router.onBeforeLeave(() => {
+    if ($chkVal === false) return false;
+  });
+
+  let $chkVal = false;
+  return (
+    <>
+      <h1>On Before Leave</h1>
+      <div>
+        <input id="allow-leaving" type="checkbox" value={$chkVal} />
+        <label for="allow-leaving">Allow Leaving This Route</label>
+      </div>
+    </>
+  );
+}
+
+export const Layout = MyLayout;
diff --git a/src/examples/OnBeforeLeave/index.html b/src/examples/OnBeforeLeave/index.html
new file mode 100644
index 0000000..cb2b361
--- /dev/null
+++ b/src/examples/OnBeforeLeave/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite + Rezact + TS</title>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script type="module" src="./main.ts"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/examples/OnBeforeLeave/main.ts b/src/examples/OnBeforeLeave/main.ts
new file mode 100644
index 0000000..f1e7565
--- /dev/null
+++ b/src/examples/OnBeforeLeave/main.ts
@@ -0,0 +1,5 @@
+import { render } from "rezact";
+import { Page } from "./OnBeforeLeave";
+
+const app = document.getElementById("app");
+render(app, Page);
diff --git a/src/lib/rezact/router.ts b/src/lib/rezact/router.ts
index 62f9eee..b7a36c6 100644
--- a/src/lib/rezact/router.ts
+++ b/src/lib/rezact/router.ts
@@ -27,6 +27,7 @@ function copyNextRoute(nextRoute, router) {
   newURLObj.reload = () => location.reload();
   newURLObj.push = (url) => router.routeChanged(url);
   newURLObj.replace = (url) => router.routeChanged(url, true);
+  newURLObj.onBeforeLeave = (callback) => (router.onBeforeLeaveFunc = callback);
 
   newURLObj.params = nextRoute.params;
   newURLObj.stack = nextRoute.stack;
@@ -83,6 +84,7 @@ interface routeIF {
   reload?: () => void;
   push?: (url: string) => void;
   replace?: (url: string) => void;
+  onBeforeLeave?: (callback: () => void) => void;
 }
 
 const defaultRouteObj: routeIF = {
@@ -106,6 +108,7 @@ const defaultRouteObj: routeIF = {
 
 export class TrieRouter {
   root: RouteNode;
+  onBeforeLeaveFunc: any = null;
   beforeHooks: any = [];
   afterHooks: any = [];
   previousRoute: routeIF = { ...defaultRouteObj };
@@ -139,10 +142,28 @@ export class TrieRouter {
     window.addEventListener("popstate", function (event) {
       that.routeChanged(event);
     });
+
+    window.addEventListener("beforeunload", function (e) {
+      if (that.onBeforeLeaveFunc) {
+        const result = that.onBeforeLeaveFunc({}, that.currentRoute);
+        if (result === false) {
+          e.preventDefault();
+          e.returnValue = "You will lose any unsaved changes.";
+          return "You will lose any unsaved changes.";
+        }
+      }
+    });
   }
 
   async runBeforeHooks(pathObj) {
     if (!pathObj) return;
+
+    if (this.onBeforeLeaveFunc) {
+      const result = this.onBeforeLeaveFunc(pathObj, this.currentRoute);
+      if (result === false) return;
+      if (result) return this.routeRequest(result);
+    }
+
     for (let hook of this.beforeHooks) {
       const result = hook(pathObj, this.currentRoute);
       if (isPromise(result)) {
@@ -169,8 +190,6 @@ export class TrieRouter {
     if (path instanceof PopStateEvent) this.popState = true;
     const url = path || window.location.pathname;
 
-    if (this.beforeHooks.length === 0) return this.routeRequest(url);
-
     let pathObj = this.getNextRoute(url);
     this.runBeforeHooks(pathObj);
   }
@@ -316,6 +335,7 @@ export class TrieRouter {
   }
 
   async routeRequest(path) {
+    this.onBeforeLeaveFunc = null;
     path = this.popState ? window.location.pathname : path;
     const pathObj = typeof path === "object";
     let nextRouteObj = pathObj ? path : this.getNextRoute(path);


Test patch:
diff --git a/src/router.test.js b/src/router.test.js
index 8e6bca9..3f86ad5 100644
--- a/src/router.test.js
+++ b/src/router.test.js
@@ -678,6 +678,34 @@ describe("Router Tests Suite", () => {
     expect(jsxSignals).not.toMatchSnapshot();
   });
 
+  it("On Before Leave Route Click", async () => {
+    const links = await screen.findAllByRole("link", {
+      name: "On Before Leave Test",
+    });
+    await user.click(links[0]);
+    await delay(100);
+    const allHeaders = await screen.findAllByRole("heading", {
+      name: /On Before Leave/i,
+    });
+    expect(allHeaders).toHaveLength(1);
+  });
+
+  it("MDX Route Click Cannot Navigate because onBeforeLeave Hook stops it", async () => {
+    const links = await screen.findAllByRole("link", {
+      name: "MDX",
+    });
+    await user.click(links[0]);
+    await delay(100);
+    const allHeaders = await screen.queryAllByRole("heading", {
+      name: /Hello World Test asdf/i,
+    });
+    expect(allHeaders).toHaveLength(0);
+
+    const checkBox = screen.getByLabelText("Allow Leaving This Route");
+    expect(checkBox).toBeDefined();
+    await user.click(checkBox);
+  });
+
   it("MDX Route Click", async () => {
     const links = await screen.findAllByRole("link", {
       name: "MDX",