Skip to content
This repository was archived by the owner on Feb 14, 2025. It is now read-only.

Commit

Permalink
Added array values (#11)
Browse files Browse the repository at this point in the history
* Added array values

* docs(changeset): Added array values feature
  • Loading branch information
bring-shrubbery authored Nov 12, 2023
1 parent a5bb3d9 commit 2a8964c
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 33 deletions.
7 changes: 7 additions & 0 deletions .changeset/afraid-boxes-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@use-search-params-state/react": patch
"@use-search-params-state/eslint-config": patch
"@use-search-params-state/next": patch
---

Added array values feature
22 changes: 22 additions & 0 deletions apps/playground/src/app/array-values/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { SetKeyValueArrayInputs } from "@/components/set-key-value-inputs";
import { Alert } from "@/components/ui/alert";

import { useSearchParamsState } from "@use-search-params-state/next";

export default function Page() {
const [state, setState] = useSearchParamsState();

return (
<div>
<Alert>
{"This demonstration shows how to use the library with arrays."}
</Alert>

<SetKeyValueArrayInputs setState={(k, v) => setState(k, v)} />

<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
);
}
1 change: 1 addition & 0 deletions apps/playground/src/app/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const Sidebar = () => {
<div className="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
<ul className="space-y-2 font-medium">
<SidebarLink href="/basic">Basic</SidebarLink>
<SidebarLink href="/array-values">Array Values</SidebarLink>
<SidebarLink href="/with-default-values">
With default values
</SidebarLink>
Expand Down
8 changes: 6 additions & 2 deletions apps/playground/src/app/with-weak-typesafety/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { Alert } from "@/components/ui/alert";

import { useSearchParamsState } from "@use-search-params-state/next";

interface SearchParamsType {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type SearchParamsType = {
page: string;
search?: string;
}
testArray?: string[];
};

export default function Page() {
const [state, setState] = useSearchParamsState<SearchParamsType>({
Expand All @@ -17,6 +19,8 @@ export default function Page() {
},
});

setState("page", "2");

return (
<div>
<Alert>
Expand Down
57 changes: 57 additions & 0 deletions apps/playground/src/components/set-key-value-inputs.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState } from "react";
import { PlusIcon } from "lucide-react";

import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
Expand Down Expand Up @@ -40,3 +42,58 @@ export const SetKeyValueInputs = ({
</div>
);
};

export const SetKeyValueArrayInputs = ({
setState,
}: {
setState: (key: string, value: string[]) => void;
}) => {
const [key, setKey] = useState("");
const [v, sv] = useState("");
const [values, setValues] = useState<string[]>([]);

return (
<div className="space-y-2">
<div className="flex gap-2">
<div>
<Label>Key</Label>
<Input
type="text"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
</div>

<div className="space-y-2">
<div className="flex gap-2 items-end">
<div>
<Label>Value</Label>
<Input
type="text"
value={v}
onChange={(e) => sv(e.target.value)}
/>
</div>
<Button
onClick={() => {
setValues([...values, v]);
sv("");
}}
>
<PlusIcon size={18} />
</Button>
</div>
<div className="flex gap-1">
{values.map((node, i) => {
return <Badge key={i}>{node}</Badge>;
})}
</div>
</div>
</div>

<Button className="block" onClick={() => setState(key, values)}>
Set
</Button>
</div>
);
};
20 changes: 20 additions & 0 deletions packages/next/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,35 @@ const Component = () => {
const [state, setState] = useSearchParamsState<{
greeting?: string;
hello: string;
testArray: string[];
}>();

const handleClick = () => {
state.greeting === "hello"
? setState("greeting", "world")
: setState("greeting", "hello");

setState("testArray", ["hello", "world"]);
};

return <button onClick={handleClick}>{state.greeting ?? "hello"}</button>;
};

<Component />;

const Comp2 = () => {
const [state, setState] = useSearchParamsState();

const handleClick = () => {
state.greeting === "hello"
? setState("greeting", "world")
: setState("greeting", "hello");

setState("fdsafdsa", ["fdsafdsa"]);
setState("fdsafdsa", "fdsafdsa");
};

return <button onClick={handleClick}>{state.greeting ?? "hello"}</button>;
};

<Comp2 />;
8 changes: 3 additions & 5 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import { useSearchParamsState as useSearchParamsStateReact } from "@use-search-p
import type { UseSearchParamsStateOptions } from "@use-search-params-state/react";

export const useSearchParamsState = <
State extends
| { [K in keyof State]: string }
| Record<string, string> = Record<string, string>,
S extends Partial<Record<string, string | string[]>>,
>(
opts?: UseSearchParamsStateOptions<State>,
opts?: UseSearchParamsStateOptions<S>,
) => {
const searchParams = useSearchParams();
const router = useRouter();
Expand All @@ -18,7 +16,7 @@ export const useSearchParamsState = <
router.push(pathname + "?" + newSearchParams.toString());
};

return useSearchParamsStateReact<State>({
return useSearchParamsStateReact<S>({
searchParams,
setSearchParams,
...opts,
Expand Down
7 changes: 5 additions & 2 deletions packages/react/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const Component1 = () => {

const [state, setState] = useSearchParamsState<{
greeting?: string;
hello: string;
hello: string[];
}>({
searchParams,
setSearchParams: (newSearchParams) => {
Expand All @@ -19,7 +19,7 @@ const Component1 = () => {
const handleClick = () => {
state.greeting === "hello"
? setState("greeting", "world")
: setState("greeting", "hello");
: setState("hello", ["hello"]);
};

return <button onClick={handleClick}>{state.greeting ?? "hello"}</button>;
Expand All @@ -33,6 +33,7 @@ const Comp2 = () => {
const [state, setState] = useSearchParamsState<{
page: string;
hello: string;
testArray: string[];
}>({
defaultValues: {
hello: "world",
Expand All @@ -46,6 +47,8 @@ const Comp2 = () => {

const handleClick = () => {
state.page === "1" ? setState("page", "2") : setState("page", "1");

setState("testArray", ["hello", "world"]);
};

return <button onClick={handleClick}>{state.page}</button>;
Expand Down
17 changes: 7 additions & 10 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ export * from "./types";
// };

export const useSearchParamsState = <
State extends
| { [K in keyof State]: string }
| Record<string, string> = Record<string, string>,
S extends Partial<Record<string, string | string[]>>,
// ZodSchema extends ZodObject<ZodSchemaRaw>,
// ZodSchemaRaw extends ZodRawShape,
// ZodSchemaType extends ZodInfer<ZodSchema>,
Params extends
UseSearchParamsStateParams<State> = UseSearchParamsStateParams<State>,
Params extends UseSearchParamsStateParams<S> = UseSearchParamsStateParams<S>,
>(
p: Params,
): [State, (key: keyof State, value: string) => void] => {
): [S, <K extends keyof S>(key: K, value: S[K]) => void] => {
// Configure default values.
const opts: Params = {
removeDefaultValues: true,
Expand All @@ -48,20 +45,20 @@ export const useSearchParamsState = <

const [initiallySetKeys] = useState(Array.from(p.searchParams.keys()));

const setState = (key: keyof State, value: string) => {
const setState = <K extends keyof S>(key: K, value: S[K]) => {
const newObject = {
...spObject,
[key]: value,
};

const newSearchParams = getSearchParams({
newObject: newObject as unknown as State,
const newSearchParams = getSearchParams<S>({
newObject: newObject as unknown as S,
options: opts,
initiallySetKeys,
});

p.setSearchParams(newSearchParams);
};

return [spObject as unknown as State, setState];
return [spObject as unknown as S, setState];
};
2 changes: 1 addition & 1 deletion packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface UseSearchParamsStateOptions<State> {
* you're accessing does not contain a value. We recommend always
* providing default values for your search params.
*/
defaultValues?: Record<string, string>;
defaultValues?: Record<string, string | string[]>;

/**
* Zod schema
Expand Down
39 changes: 30 additions & 9 deletions packages/react/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import type { ReadonlyURLSearchParams } from "next/navigation";

import type { UseSearchParamsStateParams } from "./types";

export const getSearchParams = ({
export const getSearchParams = <
S extends Partial<Record<string, string | string[]>>,
>({
newObject,
options,
initiallySetKeys,
}: {
newObject: Record<string, string>;
newObject: S;
options: UseSearchParamsStateParams<Record<string, unknown>>;
initiallySetKeys: string[];
}) => {
Expand All @@ -19,7 +21,14 @@ export const getSearchParams = ({

// If the key was set initially, we want to set save it regardless of value.
if (options.preserveInitialKeys && initiallySetKeys.includes(k)) {
sp.set(k, v);
if (Array.isArray(v)) {
v.forEach((value) => {
sp.append(k, value);
});
} else {
sp.set(k, v);
}

return;
}

Expand All @@ -29,7 +38,13 @@ export const getSearchParams = ({
// If the value is the default value and we want to remove default values, skip it.
if (options.removeDefaultValues && v === options.defaultValues?.[k]) return;

sp.set(k, v);
if (Array.isArray(v)) {
v.forEach((value) => {
sp.append(k, value);
});
} else {
sp.set(k, v);
}
});

if (options.sortKeys) {
Expand All @@ -41,13 +56,19 @@ export const getSearchParams = ({

export const searchParamsToObject = (
sp: URLSearchParams | ReadonlyURLSearchParams,
): Record<string, string> => {
const newObj: Record<string, string> = {};
): Record<string, string | string[]> => {
const newObj: Record<string, string | string[]> = {};

for (const key of Array.from(sp.keys())) {
const value = sp.get(key);
if (value === null) continue;
newObj[key] = value;
const values = sp.getAll(key);

if (values.length > 1) {
newObj[key] = values;
} else {
const value = values.at(0);
if (typeof value === "undefined") continue;
newObj[key] = value;
}
}

return newObj;
Expand Down
4 changes: 0 additions & 4 deletions tooling/eslint/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ const config = {
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/consistent-type-imports": [
"warn",
{ prefer: "type-imports", fixStyle: "separate-type-imports" },
],
"@typescript-eslint/no-misused-promises": [
2,
{ checksVoidReturn: { attributes: false } },
Expand Down

0 comments on commit 2a8964c

Please sign in to comment.