Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💥 !Define searchParamIntegration! 💥 #291

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 78 additions & 5 deletions src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<RouterUtils>
): RouterIntegration {
let ignore = false;
Expand Down Expand Up @@ -149,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.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although url hashes in the Solid widget url are not disallowed, I wasn't sure how the hash-scrolling behavior could be carried over into this new context. Let's say we have a Solid app we want to embed into another app. The scripts get injected. This "widget" is handling the entire screen between the header and footer. Therefore, it may have <h1> links inside the page. In this case, perhaps searchParamIntegration should get some new functionality. If a Solid.js-based micro-frontend can agree to a contract with the parent/host app that the parent/host app will not be using url hashes when the Solid.js-based micro-frontend is active, then searchParamIntegration could take in an extra parameter to indicate that url hashes should not be encoded into the search param, but instead independently added/removed to url. Given this contract and some new parameters passed to searchParamIntegration, searchParamIntegration could then mostly copy the scroll behavior of pathIntegration.

},
/* 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),
Expand All @@ -167,15 +240,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 <A> link
}
}
);
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions test/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -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([
Expand Down