Repository: Rezact/Rezact
Pull request #52 (2023-10-25 04:53:12)

Add Route Guard (beforeEach) function


Patch:
diff --git a/package.json b/package.json
index ca249b0..c2b10ec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@rezact/rezact",
-  "version": "1.0.15-beta.21",
+  "version": "1.0.15-beta.22",
   "type": "module",
   "description": "Intuitive Reactivity, Simplified State. Embrace a modern UI framework that encourages direct data mutations, offers module-level reactivity, and simplifies component design. With Rezact, you get the power of reactivity without the boilerplate. Dive into a seamless development experience tailored for today's web.",
   "main": "index.cjs",
diff --git a/src/lib/rezact/router.ts b/src/lib/rezact/router.ts
index 707eb73..e5e9240 100644
--- a/src/lib/rezact/router.ts
+++ b/src/lib/rezact/router.ts
@@ -1,6 +1,22 @@
 import { render } from "rezact";
 import { Signal } from "./signals";
 
+function isPromise(value) {
+  return Boolean(value && typeof value.then === "function");
+}
+
+function copyNextRoute(nextRoute) {
+  const pathObj = typeof nextRoute === "object";
+  const newURLObj = (pathObj
+    ? new URL(nextRoute)
+    : new URL(nextRoute, window.location.origin)) as unknown as routeIF;
+
+  newURLObj.params = nextRoute.params;
+  newURLObj.stack = nextRoute.stack;
+  newURLObj.currentNode = nextRoute.currentNode;
+  return newURLObj;
+}
+
 class RouteNode {
   handlers: any;
   children: Map<any, any>;
@@ -18,11 +34,43 @@ class RouteNode {
   }
 }
 
+interface routeIF {
+  hash: string;
+  host: string;
+  hostname: string;
+  href: string;
+  origin: string;
+  pathname: string;
+  port: string;
+  protocol: string;
+  search: string;
+  params: any;
+  stack: any;
+  currentNode: any;
+}
+
+const defaultRouteObj: routeIF = {
+  hash: "",
+  host: "",
+  hostname: "",
+  href: "",
+  origin: "",
+  pathname: "",
+  port: "",
+  protocol: "",
+  search: "",
+  params: {},
+  stack: [],
+  currentNode: null,
+};
+
 export class TrieRouter {
   root: RouteNode;
+  beforeHooks: any = [];
+  currentRoute: routeIF = { ...defaultRouteObj };
   renderFunc: any = null;
   noRoute: any = (router) => {
-    router.routeRequest("/404");
+    return router.getNextRoute("/404").currentNode;
   };
   constructor(options) {
     if (!options.render) throw new Error("render function is required");
@@ -36,17 +84,42 @@ export class TrieRouter {
         const locationHost = window.location.hostname; // Current page's hostname
         if (url.hostname !== locationHost) return;
         ev.preventDefault();
-        history.pushState({}, "", url.pathname);
-        this.routeRequest(url.pathname);
+        this.routeChanged(url.pathname);
       }
     });
     this.root = new RouteNode();
     window.onpopstate = this.routeChanged.bind(this);
   }
 
-  routeChanged() {
-    const url = window.location.pathname;
-    this.routeRequest(url);
+  runBeforeHooks(pathObj) {
+    if (!pathObj) return;
+    for (let hook of this.beforeHooks) {
+      const result = hook(pathObj, this.currentRoute);
+      if (isPromise(result)) {
+        result.then((res) => {
+          if (res === false) return;
+          if (res) return this.routeRequest(res);
+          this.routeRequest(pathObj);
+        });
+      } else {
+        if (result === false) return;
+        if (result) return this.routeRequest(result);
+        this.routeRequest(pathObj);
+      }
+    }
+  }
+
+  routeChanged(path = null) {
+    const url = path || window.location.pathname;
+
+    if (this.beforeHooks.length === 0) return this.routeRequest(url);
+
+    let pathObj = this.getNextRoute(url);
+    this.runBeforeHooks(pathObj);
+  }
+
+  beforeEach(callback) {
+    this.beforeHooks.push(callback);
   }
 
   addRoutesFromConfig(config, parentPath = "") {
@@ -72,8 +145,9 @@ export class TrieRouter {
           currentNode.dynamicChild.isDynamic = part.slice(1);
         }
         currentNode = currentNode.dynamicChild;
-      } else if (part === "*") {
+      } else if (part.startsWith("*")) {
         currentNode.wildcardHandler = new RouteNode();
+        currentNode.wildcardHandler.isDynamic = part.slice(1);
         currentNode.wildcardHandler.handlers.GET = callback;
         currentNode.wildcardHandler.nestedRoot = nestedRoot;
         return; // wildcard matches the rest of the route, so return
@@ -91,12 +165,13 @@ export class TrieRouter {
     currentNode.nestedRoot = currentNode.nestedRoot || nestedRoot;
   }
 
-  routeRequest(path) {
+  getNextRoute(path): routeIF {
     const parts = path.split("/").filter(Boolean);
     let currentNode = this.root;
 
     let params = {};
     let stack = [];
+    let partIdx = 0;
 
     for (let part of parts) {
       if (currentNode.nestedRoot) stack = [];
@@ -108,13 +183,37 @@ export class TrieRouter {
         params[currentNode.dynamicChild.isDynamic] = part;
         currentNode = currentNode.dynamicChild;
       } else if (currentNode.wildcardHandler) {
+        params[currentNode.wildcardHandler.isDynamic || "rest"] = parts
+          .slice(partIdx)
+          .join("/");
         currentNode = currentNode.wildcardHandler;
         break; // wildcard matches the rest of the route, so return
       } else {
-        return this.noRoute(this);
+        currentNode = this.noRoute(this);
+        break;
       }
+      partIdx++;
     }
 
+    const newURLObj = new URL(
+      path,
+      window.location.origin
+    ) as unknown as routeIF;
+
+    newURLObj.params = params;
+    newURLObj.stack = stack;
+    newURLObj.currentNode = currentNode;
+    return newURLObj;
+  }
+
+  routeRequest(path) {
+    const pathObj = typeof path === "object";
+    let nextRouteObj = pathObj ? path : this.getNextRoute(path);
+
+    let { stack, params, currentNode } = nextRouteObj;
+    this.currentRoute = copyNextRoute(nextRouteObj);
+    history.pushState({}, "", nextRouteObj.pathname);
+
     const handler = currentNode.handlers.GET;
     if (handler) {
       if (currentNode.nestedRoot) stack = [];
diff --git a/src/main.ts b/src/main.ts
index 8575dd3..58a015f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,3 +1,8 @@
 import { router } from "./components/appRouter";
 
-router.routeRequest(window.location.pathname);
+// router.beforeEach((to, from) => {
+//   console.log("beforeEach", { to, from });
+//   return "/login";
+// });
+
+router.routeChanged();


Test patch:
diff --git a/src/__snapshots__/router.test.js.snap b/src/__snapshots__/router.test.js.snap
index d3f956e..e62cbc1 100644
--- a/src/__snapshots__/router.test.js.snap
+++ b/src/__snapshots__/router.test.js.snap
@@ -154,4 +154,8 @@ exports[`Router Tests Suite > Nested Route Level 4 1`] = `"<h1>1. Users START</h
 
 exports[`Router Tests Suite > Nested Route Level 4 2`] = `"<h1>1. Users START</h1><p>ID: 953test</p><h1>Data Fetching</h1><p>Loading</p><span></span><!--start map--><!--end map--><!--signal--><!--start map--><h1>2. Users ID START</h1><p>ID: 953test</p><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><!--start map--><div><h1>3. Users ID Settings START</h1><p>ID: 953test</p><h1>Data Fetching</h1><p>Loading</p><span></span><!--start map--><!--end map--><!--signal--><!--start map--><h1>4. Users ID Settings Wild Card</h1><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><p>ID: 953test</p><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><!--end map--><h1>Data Fetching</h1><p>Loading</p><span></span><!--start map--><!--end map--><!--signal--><p>3. Users ID Settings END</p></div><!--end map--><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><p>2. Users ID END</p><!--end map--><h1>Data Fetching</h1><p>Loading</p><span></span><!--start map--><!--end map--><!--signal--><p>1. Users END</p>"`;
 
+exports[`Router Tests Suite > Route Guard Cancels Navigation - Simple String List Route 1`] = `"<!--start map--><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><!--end map-->"`;
+
+exports[`Router Tests Suite > Route Guard Reverts - Simple String List Route 1`] = `"<!--start map--><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><!--end map-->"`;
+
 exports[`Router Tests Suite > Simple String List Route 1`] = `"<!--start map--><h1>Simple String List</h1><!--start map--><div dataval=\\"Jack\\"><input></div><div dataval=\\"Jill\\"><input></div><div dataval=\\"John\\"><input></div><div dataval=\\"Jane\\"><input></div><!--end map--><!--start map--><div>Jack</div><div>Jill</div><div>John</div><div>Jane</div><!--end map--><!--signal--><!--end map-->"`;
diff --git a/src/router.test.js b/src/router.test.js
index 8fd3fcf..c605a2f 100644
--- a/src/router.test.js
+++ b/src/router.test.js
@@ -1,9 +1,5 @@
 import { afterAll, beforeAll, describe, expect, it } from "vitest";
-import {
-  screen,
-  waitFor,
-  waitForElementToBeRemoved,
-} from "@testing-library/dom";
+import { screen, waitFor } from "@testing-library/dom";
 import userEvent from "@testing-library/user-event";
 
 const currentFetch = global.fetch;
@@ -138,10 +134,9 @@ describe("Router Tests Suite", () => {
     const paragraphs = document.querySelectorAll("p");
     expect(paragraphs).toHaveLength(2);
     expect(paragraphs[0].textContent).toBe("ID: config123");
-    console.log(location);
   });
 
-  it("CONFIG BASED Ambiguous Route/Params Link Works 2", async () => {
+  it("CONFIG BASED - Ambiguous Route/Params Link Works 2", async () => {
     const links = await screen.findAllByRole("link", {
       name: "CONFIG BASED - Ambiguous Route/Params 2",
     });
@@ -567,6 +562,49 @@ describe("Router Tests Suite", () => {
     const routerOutlet = document.getElementById("router-outlet-test");
     expect(routerOutlet.innerHTML).toMatchSnapshot();
   });
+
+  it("Route Guard redirects", async () => {
+    router.beforeEach((to, from) => {
+      return "/login";
+    });
+    router.routeChanged("/simple-string-list");
+    await delay(100);
+    const allHeaders = await screen.findAllByRole("heading", {
+      name: /404 - Page Not Found/i,
+    });
+    expect(allHeaders).toHaveLength(1);
+
+    expect(router.currentRoute.pathname).toBe("/login");
+    router.beforeHooks = [];
+  });
+
+  it("Route Guard Reverts - Simple String List Route", async () => {
+    router.routeChanged("/simple-string-list");
+    await delay(100);
+    const allHeaders = await screen.findAllByRole("heading", {
+      name: /simple string list/i,
+    });
+    expect(allHeaders).toHaveLength(1);
+
+    const routerOutlet = document.getElementById("router-outlet-test");
+    expect(routerOutlet.innerHTML).toMatchSnapshot();
+  });
+
+  it("Route Guard Cancels Navigation - Simple String List Route", async () => {
+    router.beforeEach((to, from) => {
+      return false;
+    });
+    router.routeChanged("/data-fetching");
+    await delay(100);
+    const allHeaders = await screen.findAllByRole("heading", {
+      name: /simple string list/i,
+    });
+    expect(allHeaders).toHaveLength(1);
+
+    const routerOutlet = document.getElementById("router-outlet-test");
+    expect(routerOutlet.innerHTML).toMatchSnapshot();
+    router.beforeHooks = [];
+  });
 });
 
 function delay(n) {