Repository: Rezact/Rezact
Pull request #93 (2024-01-16 03:01:40)

Support route change hooks with hash and query


Patch:
diff --git a/package-lock.json b/package-lock.json
index 48bd97b..8bd346e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@rezact/rezact",
-  "version": "1.0.15-beta.33",
+  "version": "1.0.15-beta.40",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@rezact/rezact",
-      "version": "1.0.15-beta.33",
+      "version": "1.0.15-beta.40",
       "license": "MIT",
       "dependencies": {
         "@mdx-js/rollup": "^2.3.0",
@@ -23,7 +23,7 @@
         "acorn-jsx": "^5.3.2",
         "acorn-walk": "^8.2.0",
         "cross-fetch": "^4.0.0",
-        "happy-dom": "^12.1.6",
+        "happy-dom": "12.10.3",
         "magic-string": "^0.30.1",
         "path": "^0.12.7",
         "typescript": "^5.1.6",
@@ -2679,9 +2679,9 @@
       }
     },
     "node_modules/happy-dom": {
-      "version": "12.1.6",
-      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.1.6.tgz",
-      "integrity": "sha512-5ul/0itJ6HJszSJAc9NbwTrTAY1wwEqxS92zUb8j+FBA62g0BplpBA1xjox1gecvCRw16lUfZo4fjVL0P2h6Cg==",
+      "version": "12.10.3",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz",
+      "integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==",
       "dev": true,
       "dependencies": {
         "css.escape": "^1.5.1",
@@ -8219,9 +8219,9 @@
       "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw=="
     },
     "happy-dom": {
-      "version": "12.1.6",
-      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.1.6.tgz",
-      "integrity": "sha512-5ul/0itJ6HJszSJAc9NbwTrTAY1wwEqxS92zUb8j+FBA62g0BplpBA1xjox1gecvCRw16lUfZo4fjVL0P2h6Cg==",
+      "version": "12.10.3",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz",
+      "integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==",
       "dev": true,
       "requires": {
         "css.escape": "^1.5.1",
diff --git a/package.json b/package.json
index 0f9ddfc..4dc6e4a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@rezact/rezact",
-  "version": "1.0.15-beta.40",
+  "version": "1.0.15-beta.41",
   "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",
@@ -36,7 +36,7 @@
     "acorn-jsx": "^5.3.2",
     "acorn-walk": "^8.2.0",
     "cross-fetch": "^4.0.0",
-    "happy-dom": "^12.1.6",
+    "happy-dom": "12.10.3",
     "magic-string": "^0.30.1",
     "path": "^0.12.7",
     "typescript": "^5.1.6",
diff --git a/src/examples/Layout/nestedLayout.tsx b/src/examples/Layout/nestedLayout.tsx
index fc5fb5b..e6768a3 100644
--- a/src/examples/Layout/nestedLayout.tsx
+++ b/src/examples/Layout/nestedLayout.tsx
@@ -180,6 +180,12 @@ export function MyLayout({ router }: routerProp) {
         <li>
           <a href="#testing-hashroute">Hashtag route</a>
         </li>
+        <li>
+          <a href="?test=123">Search route</a>
+        </li>
+        <li>
+          <a href="?test=456#testing-hashroute2">Search and Hashtag route</a>
+        </li>
       </ul>
       <button onClick={() => router.back()}>Back</button>
       <button onClick={() => router.forward()}>Forward</button>
diff --git a/src/lib/rezact/index.ts b/src/lib/rezact/index.ts
index a1d1fdf..f518a3d 100644
--- a/src/lib/rezact/index.ts
+++ b/src/lib/rezact/index.ts
@@ -198,6 +198,15 @@ export function render(root, tagName, attributes: any = {}) {
     appendChild(root, elm);
   }
   afterRenderHooks.forEach((func) => func());
+  if (location.hash) {
+    const elm = document.querySelector(location.hash);
+    if (elm && elm.scrollIntoView)
+      elm.scrollIntoView({
+        behavior: "smooth",
+        block: "start",
+        inline: "start",
+      });
+  }
 }
 export const xFragment = [];
 
diff --git a/src/lib/rezact/router.ts b/src/lib/rezact/router.ts
index 48f699f..1c043ac 100644
--- a/src/lib/rezact/router.ts
+++ b/src/lib/rezact/router.ts
@@ -113,6 +113,8 @@ export class TrieRouter {
   afterHooks: any = [];
   previousRoute: routeIF = { ...defaultRouteObj };
   currentRoute: routeIF = { ...defaultRouteObj };
+  nextHash: string = "";
+  nextSearch: string = "";
   renderFunc: any = null;
   popState: boolean = false;
   replaceState: boolean = false;
@@ -125,7 +127,6 @@ export class TrieRouter {
     if (options.noRoute) this.noRoute = options.noRoute;
     document.body.addEventListener("click", (ev: any) => {
       if (ev.target.nodeName === "A") {
-        if (ev.target.href.includes("#")) return;
         if (!ev.target.href) return;
         if (ev.target.target) return;
         const url = new URL(ev.target.href);
@@ -135,7 +136,7 @@ export class TrieRouter {
         if (url.hostname !== locationHost) return;
 
         ev.preventDefault();
-        this.routeChanged(url.pathname);
+        this.routeChanged(url);
       }
     });
     this.root = new RouteNode();
@@ -188,8 +189,19 @@ export class TrieRouter {
 
   routeChanged(path = null, replace = false) {
     this.replaceState = replace;
+    let url = path;
     if (path instanceof PopStateEvent) this.popState = true;
-    const url = path || window.location.pathname;
+    if (path === null) {
+      url = window.location.pathname;
+      const targetURL = new URL(location.href);
+      this.nextHash = targetURL.hash;
+      this.nextSearch = targetURL.search;
+    }
+    if (path instanceof URL) {
+      url = path.pathname;
+      this.nextHash = path.hash;
+      this.nextSearch = path.search;
+    }
 
     let pathObj = this.getNextRoute(url);
     this.runBeforeHooks(pathObj);
@@ -334,8 +346,8 @@ export class TrieRouter {
 
     const newURLObj = new URL(path, window.location.href) as unknown as routeIF;
 
-    newURLObj.hash = window.location.hash;
-    newURLObj.search = window.location.search;
+    newURLObj.hash = this.nextHash;
+    newURLObj.search = this.nextSearch;
     newURLObj.route = route;
     newURLObj.builtPath = builtPath;
     newURLObj.params = params;


Test patch:
diff --git a/src/router.test.js b/src/router.test.js
index 4c0ed56..2798005 100644
--- a/src/router.test.js
+++ b/src/router.test.js
@@ -22,7 +22,11 @@ beforeAll(() => {
   };
   global.history = {
     pushState: (...args) => {
-      location.pathname = new URL(args[2], location.origin).pathname;
+      const targetURL = new URL(args[2], location.origin);
+      location.pathname = targetURL.pathname;
+      location.search = targetURL.search;
+      location.hash = targetURL.hash;
+      location.href = targetURL.href;
       historyState.push(args);
     },
   };
@@ -835,16 +839,21 @@ describe("Router Tests Suite", () => {
     expect(beforeEachGuardTest.count).toBe(2);
   });
 
-  it("Hashtag in route bypasses router behavior (allows browser to scroll)", async () => {
+  it("Hashtag in route pushes state to location", async () => {
     expect(location.href).toBe("http://localhost:3000/simple-string-list");
     const links = await screen.findAllByRole("link", {
       name: /Hashtag route/i,
     });
-    expect(links[0]._url.hash).toBe("#testing-hashroute");
+
+    expect(links[0].href).toBe(
+      "http://localhost:3000/simple-string-list#testing-hashroute"
+    );
     await user.click(links[0]);
     await delay(100);
-    expect(location.href).toBe("http://localhost:3000/simple-string-list");
-    expect(beforeEachGuardTest.count).toBe(2);
+    expect(location.href).toBe(
+      "http://localhost:3000/simple-string-list#testing-hashroute"
+    );
+    expect(beforeEachGuardTest.count).toBe(3);
   });
 
   it("Router afterEach Hook", async () => {
@@ -852,7 +861,7 @@ describe("Router Tests Suite", () => {
       expect(from.pathname).toBe("/simple-string-list");
       expect(to.pathname).toBe("/data-fetching");
     });
-    router.routeChanged("/data-fetching");
+    router.routeChanged("/data-fetching?test=123");
     await delay(100);
     const allHeaders = await screen.findAllByRole("heading", {
       name: /data fetching/i,
@@ -860,8 +869,38 @@ describe("Router Tests Suite", () => {
     expect(allHeaders).toHaveLength(1);
 
     expect(router.currentRoute.pathname).toBe("/data-fetching");
+
     router.afterHooks = [];
-    expect(beforeEachGuardTest.count).toBe(3);
+    expect(beforeEachGuardTest.count).toBe(4);
+  });
+
+  it("Search Query in route pushes state to location", async () => {
+    expect(location.href).toBe(
+      "http://localhost:3000/data-fetching#testing-hashroute"
+    );
+    const links = await screen.findAllByRole("link", {
+      name: /Search route/i,
+    });
+
+    await user.click(links[0]);
+    await delay(100);
+    expect(location.href).toBe(
+      "http://localhost:3000/simple-string-list?test=123"
+    );
+    expect(beforeEachGuardTest.count).toBe(5);
+  });
+
+  it("Both Search Query and hashtag in route pushes state to location", async () => {
+    const links = await screen.findAllByRole("link", {
+      name: /Search and hashtag route/i,
+    });
+
+    await user.click(links[0]);
+    await delay(100);
+    expect(location.href).toBe(
+      "http://localhost:3000/simple-string-list?test=456#testing-hashroute2"
+    );
+    expect(beforeEachGuardTest.count).toBe(6);
   });
 });