Skip to content

Commit

Permalink
feat(search-params): enable arrays in useSearchParams
Browse files Browse the repository at this point in the history
  • Loading branch information
markojerkic committed Oct 12, 2024
1 parent 6dd0473 commit b4f7544
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 15 deletions.
20 changes: 13 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ declare module "solid-js/web" {
response: {
status?: number;
statusText?: string;
headers: Headers
headers: Headers;
};
router? : {
router?: {
matches?: OutputMatch[];
cache?: Map<string, CacheEntry>;
submission?: {
Expand All @@ -18,14 +18,17 @@ declare module "solid-js/web" {
dataOnly?: boolean | string[];
data?: Record<string, any>;
previousUrl?: string;
}
};
serverOnly?: boolean;
}
}

export type Params = Record<string, string>;
export type Params = Record<string, string | string[]>;

export type SetParams = Record<string, string | number | boolean | null | undefined>;
export type SetParams = Record<
string,
string | string[] | number | number[] | boolean | boolean[] | null | undefined
>;

export interface Path {
pathname: string;
Expand Down Expand Up @@ -227,9 +230,12 @@ export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<
export type RouterResponseInit = Omit<ResponseInit, "body"> & { revalidate?: string | string[] };
// export type CustomResponse<T> = Response & { customBody: () => T };
// hack to avoid it thinking it inherited from Response
export type CustomResponse<T> = Omit<Response, "clone"> & { customBody: () => T; clone(...args: readonly unknown[]): CustomResponse<T> };
export type CustomResponse<T> = Omit<Response, "clone"> & {
customBody: () => T;
clone(...args: readonly unknown[]): CustomResponse<T>;
};

/** @deprecated */
export type RouteLoadFunc = RoutePreloadFunc;
/** @deprecated */
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
29 changes: 24 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { createMemo, getOwner, runWithOwner } from "solid-js";
import type { MatchFilter, MatchFilters, Params, PathMatch, RouteDescription, SetParams } from "./types.ts";
import type {
MatchFilter,
MatchFilters,
Params,
PathMatch,
RouteDescription,
SetParams
} from "./types.ts";

const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
const trimPathRegex = /^\/+|(\/)\/+$/g;
export const mockBase = "http://sr"
export const mockBase = "http://sr";

export function normalizePath(path: string, omitSlash: boolean = false) {
const s = path.replace(trimPathRegex, "$1");
Expand Down Expand Up @@ -41,7 +48,13 @@ export function joinPaths(from: string, to: string): string {
export function extractSearchParams(url: URL): Params {
const params: Params = {};
url.searchParams.forEach((value, key) => {
params[key] = value;
if (key in params) {
params[key] = Array.isArray(params[key])
? ([...params[key], value] as string[])
: ([params[key], value] as string[]);
} else {
params[key] = value;
}
});
return params;
}
Expand Down Expand Up @@ -153,10 +166,16 @@ export function createMemoObject<T extends Record<string | symbol, unknown>>(fn:
export function mergeSearchString(search: string, params: SetParams) {
const merged = new URLSearchParams(search);
Object.entries(params).forEach(([key, value]) => {
if (value == null || value === "") {
if (value == null || value === "" || (value instanceof Array && !value.length)) {
merged.delete(key);
} else {
merged.set(key, String(value));
if (value instanceof Array) {
value.forEach(v => {
merged.append(key, String(v));
});
} else {
merged.set(key, String(value));
}
}
});
const s = merged.toString();
Expand Down
3 changes: 1 addition & 2 deletions test/route.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { vi } from 'vitest'
import { vi } from "vitest";
import { createBranch, createBranches, createRoutes } from "../src/routing.js";
import type { RouteDefinition } from "../src/index.js";

Expand Down Expand Up @@ -174,7 +174,6 @@ describe("createRoutes should", () => {
expect(match).not.toBeNull();
expect(match.path).toBe("/foo/123/bar/solid.html");
});

});

describe(`expand optional parameters`, () => {
Expand Down
77 changes: 76 additions & 1 deletion test/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
joinPaths,
resolvePath,
createMemoObject,
expandOptionals
expandOptionals,
mergeSearchString,
extractSearchParams
} from "../src/utils";

describe("resolvePath should", () => {
Expand Down Expand Up @@ -86,6 +88,79 @@ describe("resolvePath should", () => {
});
});

describe("mergeSearchString should", () => {
test("return empty string when current and new params are empty", () => {
const expected = "";
const actual = mergeSearchString("", {});
expect(actual).toBe(expected);
});

test("return new params when current params are empty", () => {
const expected = "?foo=bar";
const actual = mergeSearchString("", { foo: "bar" });
expect(actual).toBe(expected);
});

test("return current params when new params are empty", () => {
const expected = "?foo=bar";
const actual = mergeSearchString("?foo=bar", {});
expect(actual).toBe(expected);
});

test("return merged params when current and new params are not empty", () => {
const expected = "?foo=bar&baz=qux";
const actual = mergeSearchString("?foo=bar", { baz: "qux" });
expect(actual).toBe(expected);
});

test("return ampersand-separated params when new params is an array", () => {
const expected = "?foo=bar&foo=baz";
const actual = mergeSearchString("", { foo: ["bar", "baz"] });
expect(actual).toBe(expected);
});

test("return ampersand-separated params when current params is an array of numbers", () => {
const expected = "?foo=1&foo=2";
const actual = mergeSearchString("", { foo: [1, 2] });
expect(actual).toBe(expected);
});

test("return empty string when new is an empty array", () => {
const expected = "";
const actual = mergeSearchString("", { foo: [] });
expect(actual).toBe(expected);
});

test("return empty string when current is present and new is an empty array", () => {
const expected = "";
const actual = mergeSearchString("?foo=2&foo=3", { foo: [] });
expect(actual).toBe(expected);
});
});

describe("extractSearchParams should", () => {
test("return empty object when URL has no search params", () => {
const url = new URL("http://localhost/");
const expected = {};
const actual = extractSearchParams(url);
expect(actual).toEqual(expected);
});

test("return search params as object", () => {
const url = new URL("http://localhost/?foo=bar&baz=qux");
const expected = { foo: "bar", baz: "qux" };
const actual = extractSearchParams(url);
expect(actual).toEqual(expected);
});

test("return search params as object with array values", () => {
const url = new URL("http://localhost/?foo=bar&foo=baz");
const expected = { foo: ["bar", "baz"] };
const actual = extractSearchParams(url);
expect(actual).toEqual(expected);
});
});

describe("createMatcher should", () => {
test("return empty object when location matches simple path", () => {
const expected = { path: "/foo/bar", params: {} };
Expand Down

0 comments on commit b4f7544

Please sign in to comment.