From aeb6c078a61b51eb5f0aa06b1803dad5486a1968 Mon Sep 17 00:00:00 2001 From: devinrhode2 Date: Tue, 22 Aug 2023 21:33:57 -0500 Subject: [PATCH 1/3] Improve readability of `createIntegration` --- src/integration.ts | 13 ++++++++++++- src/types.ts | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/integration.ts b/src/integration.ts index 7d301ed71..e57aac709 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -32,6 +32,11 @@ function scrollToHash(hash: string, fallbackTop?: boolean) { } } +/** + * Store location history in a local variable. + * + * (other router integrations "store" state as urls in browser history) + */ export function createMemoryHistory() { const entries = ["/"]; let index = 0; @@ -75,10 +80,16 @@ export function createMemoryHistory() { }; } +type NotifyLocationChange = (value?: string | LocationChange) => void; + +type CreateLocationChangeNotifier = ( + notify: NotifyLocationChange +) => /* LocationChangeNotifier: */ () => void; + export function createIntegration( get: () => string | LocationChange, set: (next: LocationChange) => void, - init?: (notify: (value?: string | LocationChange) => void) => () => void, + init?: CreateLocationChangeNotifier, utils?: Partial ): RouterIntegration { let ignore = false; diff --git a/src/types.ts b/src/types.ts index 78d23d58c..376981d19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -132,6 +132,7 @@ export interface RouteContext { } export interface RouterUtils { + /** This produces the `href` attribute shown in the browser. */ renderPath(path: string): string; parsePath(str: string): string; go(delta: number): void; From 40aedb2ac40abef1c9d95f0c23700353da9b8e2d Mon Sep 17 00:00:00 2001 From: devinrhode2 Date: Tue, 22 Aug 2023 21:34:41 -0500 Subject: [PATCH 2/3] Edit comments in hashIntegration's `parsePath` util --- src/integration.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/integration.ts b/src/integration.ts index e57aac709..09e44343d 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -178,15 +178,15 @@ export function hashIntegration() { go: delta => window.history.go(delta), renderPath: path => `#${path}`, parsePath: str => { + // Get everything after the `#` (by dropping everything before the `#`) const to = str.replace(/^.*?#/, ""); - // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to - // `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash - // to the current path so we can handle these in-page anchors correctly. if (!to.startsWith("/")) { + // We got an in-page heading link. + // Append it to the current path to maintain correct browser behavior. const [, path = "/"] = window.location.hash.split("#", 2); return `${path}#${to}`; } - return to; + return to; // Normal Solidjs link } } ); From d36f655710d61cfb5b90befe63d4630a61f61b0e Mon Sep 17 00:00:00 2001 From: devinrhode2 Date: Wed, 23 Aug 2023 09:06:52 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=92=A5=20!Define=20`searchParamIntegr?= =?UTF-8?q?ation`!=20=F0=9F=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/integration.ts | 62 ++++++++++++++++++++++++++++++++++++++++ test/integration.spec.ts | 24 ++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/integration.ts b/src/integration.ts index 09e44343d..d9ead65df 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -160,6 +160,68 @@ export function pathIntegration() { ); } +/** + * If your Solidjs "app" is really just a "widget" or "micro-frontend" that will be embedded into a larger app, + * you can use this router integration to embed your entire url into a *single* search parameter. + * + * This is done by taking your "app"s normal url, and passing it through `encodeURIComponent` and `decodeURIComponent`. + * + * If your widget has search params, they will not leak into the parent/host app, and your solid app should not pickup parents search params, unless your Solid code is reading from `window.location`. + */ +// This implementation was based on pathIntegration, with many ideas and concepts taken from hashIntegration. +export function searchParamIntegration( + /** + * Let's say you are building a chat widget that will be integrated into a larger app. + * + * You want to pass in a globally unique search param key here, perhaps "supportChatWidgetUrl" + */ + widgetUrlsSearchParamName: string +) { + function getWidgetsUrl(searchParams: URLSearchParams): string { + return decodeURIComponent( + searchParams.get(widgetUrlsSearchParamName) || encodeURIComponent("/") + ); + } + + const createLocationChangeNotifier: CreateLocationChangeNotifier = notify => { + return bindEvent(window, "popstate", () => notify()); + }; + + return createIntegration( + () => ({ + value: getWidgetsUrl(new URLSearchParams(window.location.search)), + state: history.state + }), + ({ value, replace, state }) => { + if (replace) { + window.history.replaceState(state, "", value); + } else { + window.history.pushState(state, "", value); + } + // Because the above `pushState/replaceState` call should never change `window.location.hash`, + // this behavior from `pathIntegration` doesn't make sense in the context of `searchParamIntegration`: + // scrollToHash(window.location.hash.slice(1), scroll); + // Perhaps when a widget loads, it should run the `el.scrollIntoView` function itself. + }, + /* init */ + createLocationChangeNotifier, + { + go: delta => window.history.go(delta), + renderPath: path => { + const params = new URLSearchParams(window.location.search); + params.set(widgetUrlsSearchParamName, encodeURIComponent(path)); + // Starts with `?` because we don't want to change the path at all + // This degrades gracefully if javascript is disabled + return `?${params.toString()}`; + }, + parsePath: str => { + const url = new URL(str, "https://github.com/"); + return getWidgetsUrl(url.searchParams); + } + } + ); +} + export function hashIntegration() { return createIntegration( () => window.location.hash.slice(1), diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 9f6b18532..bd4ea909d 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -1,4 +1,28 @@ import { hashIntegration } from "../src/integration"; +import { searchParamIntegration } from "../src/integration"; + +describe("query integration should", () => { + const widgetUrl = `/add-account?type=dex#/ugh`; + const encodedPath = encodeURIComponent(widgetUrl); + test.each([ + ["http://localhost/", "/"], + ["http://localhost//#/practice", "/"], + ["http://localhost/base/#/practice", "/"], + ["http://localhost/#/practice#some-id", "/"], + ["file:///C:/Users/Foo/index.html#/test", "/"], + [`http://localhost/?carniatomon=${encodedPath}`, widgetUrl], + [`http://localhost/?carniatomon=${encodedPath}#/practice`, widgetUrl], + [`http://localhost/base/?carniatomon=${encodedPath}#/practice`, widgetUrl], + [`http://localhost/?carniatomon=${encodedPath}#/practice#some-id`, widgetUrl], + [`file:///C:/Users/Foo/index.html?carniatomon=${encodedPath}#/test`, widgetUrl] + ])(`parse paths (case '%s' as '%s')`, (urlString, expected) => { + const parsed = searchParamIntegration( + 'carniatomon' + ).utils!.parsePath!(urlString); + expect(parsed).toBe(expected); + }); +}); + describe("Hash integration should", () => { test.each([