Repository: Rezact/Rezact (ts)

Issue:
Add Router afterEach hook
https://router.vuejs.org/guide/advanced/navigation-guards.html#Global-After-Hooks

Pull request:
close #56 add router afterEach hook
close #56 

Patch:
diff --git a/src/lib/rezact/router.ts b/src/lib/rezact/router.ts
index 1daaedb..62f9eee 100644
--- a/src/lib/rezact/router.ts
+++ b/src/lib/rezact/router.ts
@@ -107,6 +107,8 @@ const defaultRouteObj: routeIF = {
 export class TrieRouter {
   root: RouteNode;
   beforeHooks: any = [];
+  afterHooks: any = [];
+  previousRoute: routeIF = { ...defaultRouteObj };
   currentRoute: routeIF = { ...defaultRouteObj };
   renderFunc: any = null;
   popState: boolean = false;
@@ -123,8 +125,11 @@ export class TrieRouter {
         if (!ev.target.href) return;
         if (ev.target.target) return;
         const url = new URL(ev.target.href);
-        const locationHost = window.location.hostname; // Current page's hostname
+
+        // allow external links to bypass the router
+        const locationHost = window.location.hostname;
         if (url.hostname !== locationHost) return;
+
         ev.preventDefault();
         this.routeChanged(url.pathname);
       }
@@ -134,25 +139,29 @@ export class TrieRouter {
     window.addEventListener("popstate", function (event) {
       that.routeChanged(event);
     });
-    // window.onpopstate = this.routeChanged.bind(this);
   }
 
-  runBeforeHooks(pathObj) {
+  async 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);
-        });
+        const res = await result;
+        if (res === false) return;
+        if (res) return this.routeRequest(res);
       } else {
         if (result === false) return;
         if (result) return this.routeRequest(result);
-        this.routeRequest(pathObj);
       }
     }
+    this.routeRequest(pathObj);
+  }
+
+  runAfterHooks(pathObj) {
+    if (!pathObj) return;
+    for (let hook of this.afterHooks) {
+      hook(pathObj, this.previousRoute);
+    }
   }
 
   routeChanged(path = null, replace = false) {
@@ -170,6 +179,10 @@ export class TrieRouter {
     this.beforeHooks.push(callback);
   }
 
+  afterEach(callback) {
+    this.afterHooks.push(callback);
+  }
+
   addRoutesFromConfig(config, parentPath = "") {
     config.forEach((route) => {
       const currentPath = `${parentPath}${route.path}`;
@@ -194,31 +207,32 @@ export class TrieRouter {
       this.root.meta = opts.meta;
     }
 
+    const setupNode = (node, opts, part) => {
+      node.title = opts.title;
+      node.meta = opts.meta;
+      node.partName = `/${part}`;
+    };
+
     for (let part of parts) {
       if (part.startsWith(":")) {
         if (!currentNode.dynamicChild) {
           currentNode.dynamicChild = new RouteNode();
-          currentNode.dynamicChild.title = opts.title;
-          currentNode.dynamicChild.meta = opts.meta;
-          currentNode.dynamicChild.partName = `/${part}`;
+          setupNode(currentNode.dynamicChild, opts, part);
           currentNode.dynamicChild.isDynamic = part.slice(1);
         }
         currentNode = currentNode.dynamicChild;
       } else if (part.startsWith("*")) {
         currentNode.wildcardHandler = new RouteNode();
-        currentNode.wildcardHandler.title = opts.title;
-        currentNode.wildcardHandler.meta = opts.meta;
-        currentNode.wildcardHandler.partName = `/${part}`;
+        setupNode(currentNode.wildcardHandler, opts, part);
         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
       } else {
         if (!currentNode.children.has(part)) {
           currentNode.children.set(part, new RouteNode());
-          currentNode.children.get(part).title = opts.title;
-          currentNode.children.get(part).meta = opts.meta;
-          currentNode.children.get(part).partName = `/${part}`;
+          setupNode(currentNode.children.get(part), opts, part);
         }
         currentNode = currentNode.children.get(part);
       }
@@ -301,12 +315,13 @@ export class TrieRouter {
     return newURLObj;
   }
 
-  routeRequest(path) {
+  async routeRequest(path) {
     path = this.popState ? window.location.pathname : path;
     const pathObj = typeof path === "object";
     let nextRouteObj = pathObj ? path : this.getNextRoute(path);
 
     let { currentNode } = nextRouteObj;
+    this.previousRoute = this.currentRoute;
     this.currentRoute = copyNextRoute(nextRouteObj, this);
     !this.popState &&
       !this.replaceState &&
@@ -318,7 +333,8 @@ export class TrieRouter {
     if (handler) {
       if (currentNode.nestedRoot) this.currentRoute.stack = [];
       this.currentRoute.stack.push(currentNode);
-      this.renderFunc(this.currentRoute);
+      await this.renderFunc(this.currentRoute);
+      this.runAfterHooks(nextRouteObj);
     } else {
       return this.noRoute(this);
     }


Test patch:
diff --git a/src/router.test.js b/src/router.test.js
index 3690ea7..8e6bca9 100644
--- a/src/router.test.js
+++ b/src/router.test.js
@@ -734,12 +734,24 @@ describe("Router Tests Suite", () => {
     expect(routerOutlet.innerHTML).toMatchSnapshot();
   });
 
+  const beforeEachGuardTest = {
+    guard1: false,
+    guard2: false,
+  };
+
   it("Route Guard redirects", async () => {
-    router.beforeEach((to, from) => {
+    router.beforeEach(async (to, from) => {
+      beforeEachGuardTest.guard1 = true;
       expect(from.pathname).toBe("/simple-string-list");
       expect(to.pathname).toBe("/simple-string-list");
       return "/login";
     });
+    router.beforeEach(async (to, from) => {
+      beforeEachGuardTest.guard2 = true;
+      expect(from.pathname).toBe("/simple-string-list");
+      expect(to.pathname).toBe("/simple-string-list");
+      return "/this-guard-should-not-run";
+    });
     router.routeChanged("/simple-string-list");
     await delay(100);
     const allHeaders = await screen.findAllByRole("heading", {
@@ -749,6 +761,9 @@ describe("Router Tests Suite", () => {
 
     expect(router.currentRoute.pathname).toBe("/login");
     router.beforeHooks = [];
+
+    expect(beforeEachGuardTest.guard1).toBe(true);
+    expect(beforeEachGuardTest.guard2).toBe(false);
   });
 
   it("Route Guard Reverts - Simple String List Route", async () => {
@@ -780,6 +795,22 @@ describe("Router Tests Suite", () => {
     expect(routerOutlet.innerHTML).toMatchSnapshot();
     router.beforeHooks = [];
   });
+
+  it("Router afterEach Hook", async () => {
+    router.afterEach((to, from) => {
+      expect(from.pathname).toBe("/simple-string-list");
+      expect(to.pathname).toBe("/data-fetching");
+    });
+    router.routeChanged("/data-fetching");
+    await delay(100);
+    const allHeaders = await screen.findAllByRole("heading", {
+      name: /data fetching/i,
+    });
+    expect(allHeaders).toHaveLength(1);
+
+    expect(router.currentRoute.pathname).toBe("/data-fetching");
+    router.afterHooks = [];
+  });
 });
 
 function delay(n) {