Skip to content

Commit b4f7544

Browse files
committed
feat(search-params): enable arrays in useSearchParams
1 parent 6dd0473 commit b4f7544

File tree

4 files changed

+114
-15
lines changed

4 files changed

+114
-15
lines changed

src/types.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ declare module "solid-js/web" {
55
response: {
66
status?: number;
77
statusText?: string;
8-
headers: Headers
8+
headers: Headers;
99
};
10-
router? : {
10+
router?: {
1111
matches?: OutputMatch[];
1212
cache?: Map<string, CacheEntry>;
1313
submission?: {
@@ -18,14 +18,17 @@ declare module "solid-js/web" {
1818
dataOnly?: boolean | string[];
1919
data?: Record<string, any>;
2020
previousUrl?: string;
21-
}
21+
};
2222
serverOnly?: boolean;
2323
}
2424
}
2525

26-
export type Params = Record<string, string>;
26+
export type Params = Record<string, string | string[]>;
2727

28-
export type SetParams = Record<string, string | number | boolean | null | undefined>;
28+
export type SetParams = Record<
29+
string,
30+
string | string[] | number | number[] | boolean | boolean[] | null | undefined
31+
>;
2932

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

232238
/** @deprecated */
233239
export type RouteLoadFunc = RoutePreloadFunc;
234240
/** @deprecated */
235-
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
241+
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;

src/utils.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { createMemo, getOwner, runWithOwner } from "solid-js";
2-
import type { MatchFilter, MatchFilters, Params, PathMatch, RouteDescription, SetParams } from "./types.ts";
2+
import type {
3+
MatchFilter,
4+
MatchFilters,
5+
Params,
6+
PathMatch,
7+
RouteDescription,
8+
SetParams
9+
} from "./types.ts";
310

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

815
export function normalizePath(path: string, omitSlash: boolean = false) {
916
const s = path.replace(trimPathRegex, "$1");
@@ -41,7 +48,13 @@ export function joinPaths(from: string, to: string): string {
4148
export function extractSearchParams(url: URL): Params {
4249
const params: Params = {};
4350
url.searchParams.forEach((value, key) => {
44-
params[key] = value;
51+
if (key in params) {
52+
params[key] = Array.isArray(params[key])
53+
? ([...params[key], value] as string[])
54+
: ([params[key], value] as string[]);
55+
} else {
56+
params[key] = value;
57+
}
4558
});
4659
return params;
4760
}
@@ -153,10 +166,16 @@ export function createMemoObject<T extends Record<string | symbol, unknown>>(fn:
153166
export function mergeSearchString(search: string, params: SetParams) {
154167
const merged = new URLSearchParams(search);
155168
Object.entries(params).forEach(([key, value]) => {
156-
if (value == null || value === "") {
169+
if (value == null || value === "" || (value instanceof Array && !value.length)) {
157170
merged.delete(key);
158171
} else {
159-
merged.set(key, String(value));
172+
if (value instanceof Array) {
173+
value.forEach(v => {
174+
merged.append(key, String(v));
175+
});
176+
} else {
177+
merged.set(key, String(value));
178+
}
160179
}
161180
});
162181
const s = merged.toString();

test/route.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { vi } from 'vitest'
1+
import { vi } from "vitest";
22
import { createBranch, createBranches, createRoutes } from "../src/routing.js";
33
import type { RouteDefinition } from "../src/index.js";
44

@@ -174,7 +174,6 @@ describe("createRoutes should", () => {
174174
expect(match).not.toBeNull();
175175
expect(match.path).toBe("/foo/123/bar/solid.html");
176176
});
177-
178177
});
179178

180179
describe(`expand optional parameters`, () => {

test/utils.spec.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
joinPaths,
44
resolvePath,
55
createMemoObject,
6-
expandOptionals
6+
expandOptionals,
7+
mergeSearchString,
8+
extractSearchParams
79
} from "../src/utils";
810

911
describe("resolvePath should", () => {
@@ -86,6 +88,79 @@ describe("resolvePath should", () => {
8688
});
8789
});
8890

91+
describe("mergeSearchString should", () => {
92+
test("return empty string when current and new params are empty", () => {
93+
const expected = "";
94+
const actual = mergeSearchString("", {});
95+
expect(actual).toBe(expected);
96+
});
97+
98+
test("return new params when current params are empty", () => {
99+
const expected = "?foo=bar";
100+
const actual = mergeSearchString("", { foo: "bar" });
101+
expect(actual).toBe(expected);
102+
});
103+
104+
test("return current params when new params are empty", () => {
105+
const expected = "?foo=bar";
106+
const actual = mergeSearchString("?foo=bar", {});
107+
expect(actual).toBe(expected);
108+
});
109+
110+
test("return merged params when current and new params are not empty", () => {
111+
const expected = "?foo=bar&baz=qux";
112+
const actual = mergeSearchString("?foo=bar", { baz: "qux" });
113+
expect(actual).toBe(expected);
114+
});
115+
116+
test("return ampersand-separated params when new params is an array", () => {
117+
const expected = "?foo=bar&foo=baz";
118+
const actual = mergeSearchString("", { foo: ["bar", "baz"] });
119+
expect(actual).toBe(expected);
120+
});
121+
122+
test("return ampersand-separated params when current params is an array of numbers", () => {
123+
const expected = "?foo=1&foo=2";
124+
const actual = mergeSearchString("", { foo: [1, 2] });
125+
expect(actual).toBe(expected);
126+
});
127+
128+
test("return empty string when new is an empty array", () => {
129+
const expected = "";
130+
const actual = mergeSearchString("", { foo: [] });
131+
expect(actual).toBe(expected);
132+
});
133+
134+
test("return empty string when current is present and new is an empty array", () => {
135+
const expected = "";
136+
const actual = mergeSearchString("?foo=2&foo=3", { foo: [] });
137+
expect(actual).toBe(expected);
138+
});
139+
});
140+
141+
describe("extractSearchParams should", () => {
142+
test("return empty object when URL has no search params", () => {
143+
const url = new URL("http://localhost/");
144+
const expected = {};
145+
const actual = extractSearchParams(url);
146+
expect(actual).toEqual(expected);
147+
});
148+
149+
test("return search params as object", () => {
150+
const url = new URL("http://localhost/?foo=bar&baz=qux");
151+
const expected = { foo: "bar", baz: "qux" };
152+
const actual = extractSearchParams(url);
153+
expect(actual).toEqual(expected);
154+
});
155+
156+
test("return search params as object with array values", () => {
157+
const url = new URL("http://localhost/?foo=bar&foo=baz");
158+
const expected = { foo: ["bar", "baz"] };
159+
const actual = extractSearchParams(url);
160+
expect(actual).toEqual(expected);
161+
});
162+
});
163+
89164
describe("createMatcher should", () => {
90165
test("return empty object when location matches simple path", () => {
91166
const expected = { path: "/foo/bar", params: {} };

0 commit comments

Comments
 (0)