diff --git a/packages/example-nextjs/src/app/Form.tsx b/packages/example-nextjs/src/app/Form.tsx index 52f5e17..693b6de 100644 --- a/packages/example-nextjs/src/app/Form.tsx +++ b/packages/example-nextjs/src/app/Form.tsx @@ -1,36 +1,32 @@ 'use client'; import React from 'react'; -import { useSearchParams, useRouter, usePathname } from 'next/navigation'; +import { useRouter, usePathname } from 'next/navigation'; import { Button } from './Button'; import { Field } from './Field'; -import { encode, decode } from 'encoder'; -const form = { - name: '', - age: 0, - 'agree to terms': false, -}; +import { useUrlState } from './useUrlState'; +import { form } from './form'; // TODO: need some api to make it user friendly -// UrlState class that extends URLSearchParams? -// toObj and fromObj ? to.array() to.object()? -function getObj (params: URLSearchParams) { - return Object.fromEntries( - [...params.entries()].map(([key, value]) => [ - key, - decode(value) - ]), - ) as T; + +function usePrevious(val: T) { + const value = React.useRef() + + React.useEffect(() => { + value.current = val + }, [val]) + + return value.current } export const Form = ({ className }: { className?: string }) => { const router = useRouter(); const pathname = usePathname(); - const searchParams= useSearchParams(); - const parsedParams = getObj(searchParams); - const [state, setState] = React.useState(parsedParams); + const { parse, stringify } = useUrlState(form) + + const [state, setState] = React.useState(() => parse()); const handleChange = React.useCallback( @@ -45,11 +41,7 @@ export const Form = ({ className }: { className?: string }) => { ); const handleSave = () => { - const params = new URLSearchParams(searchParams.toString()); - Object.entries(state).forEach(([key, value]) => { - params.set(String(key), encode(value)); - }); - router.push(`${pathname}?${params.toString()}`); + router.push(`${pathname}?${stringify(state)}`); }; // // set URI when state change @@ -107,7 +99,7 @@ export const Form = ({ className }: { className?: string }) => { }; -interface InputProps extends React.InputHTMLAttributes {} +interface InputProps extends React.InputHTMLAttributes { } const Input = ({ className, ...props }: InputProps) => { return ; }; diff --git a/packages/example-nextjs/src/app/Status.tsx b/packages/example-nextjs/src/app/Status.tsx index 678483f..984415f 100644 --- a/packages/example-nextjs/src/app/Status.tsx +++ b/packages/example-nextjs/src/app/Status.tsx @@ -1,25 +1,19 @@ 'use client'; import React from 'react'; -import { useSearchParams } from 'next/navigation'; import { Field } from './Field'; -import { decode } from 'encoder'; +import { useUrlState } from './useUrlState'; +import { form } from './form' export const Status = ({ className }: { className?: string }) => { - const searchParams = useSearchParams(); - const state = getObj(searchParams); + const { parse } = useUrlState(form) return (

Types of data are presered

-        {JSON.stringify(state, null, 2)}
+        {JSON.stringify(parse(), null, 2)}
       
); }; - -const getObj = (params: URLSearchParams) => - Object.fromEntries( - [...params.entries()].map(([key, value]) => [key, decode(value)]), - ); diff --git a/packages/example-nextjs/src/app/form.ts b/packages/example-nextjs/src/app/form.ts new file mode 100644 index 0000000..41ce907 --- /dev/null +++ b/packages/example-nextjs/src/app/form.ts @@ -0,0 +1,5 @@ +export const form: {name: string, age: number | string, 'agree to terms': boolean} = { + name: '', + age: "", + 'agree to terms': false, +}; diff --git a/packages/example-nextjs/src/app/useUrlState.ts b/packages/example-nextjs/src/app/useUrlState.ts new file mode 100644 index 0000000..a5dd5bc --- /dev/null +++ b/packages/example-nextjs/src/app/useUrlState.ts @@ -0,0 +1,41 @@ +import { useSearchParams } from 'next/navigation'; +import React from 'react' +import { encode, decode } from 'encoder'; + + +// TODO: tests +export function useUrlState (obj?: T) { + const searchParams = useSearchParams(); + + const stringify = React.useCallback(function(state: T): string { + const params = new URLSearchParams(); + if (Array.isArray(state)) { + state.forEach(([key, value]) => params.set(String(key), encode(value)) ) + } else if (!!state && typeof state === 'object' ) { + Object.entries(state).forEach(([key, value]) => { + // TODO: fix ts + const initialVal = (obj as typeof state)?.[key as keyof state] + if (value !== initialVal ) { + params.set(String(key), encode(value)); + } else { + params.delete(key) + } + }) + } else { + params.set('value', encode(state)) + } + + return params.toString() + }, []) + + const parse = React.useCallback(function() { + // TODO: array, obj, primitive + return {...obj, ...(Object.fromEntries( + [...searchParams.entries()].map(([key, value]) => [ + key, + decode(value) || obj?.[key] + ])) as T)} + }, [searchParams]) + + return { parse, stringify } +}