Skip to content

Commit 4595cec

Browse files
committed
fix: createQueryParamStore always uses goto with keepFocus in addition to noScroll so it works again in SvelteKit 2, also update function to make it match writable and adds optional storagePrefix option
1 parent b7b2e89 commit 4595cec

File tree

2 files changed

+70
-169
lines changed

2 files changed

+70
-169
lines changed
Lines changed: 70 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,118 @@
1-
import { writable, type Readable } from 'svelte/store';
1+
import { writable, type Writable } from 'svelte/store';
22
import { goto } from '$app/navigation';
33
import { page } from '$app/stores';
4-
import { decodeParam, encodeParam } from './url-helpers';
54

6-
export interface QueryParamStore<T> extends Readable<T> {
7-
set: (value: any) => void;
5+
export interface QueryParamStore<T> extends Writable<T> {
86
remove: () => void;
97
}
108

119
export interface QueryParamStoreOptions<T> {
1210
key: string;
13-
replaceState?: boolean;
1411
startWith?: T;
15-
log?: boolean;
12+
replaceState?: boolean;
1613
persist?: 'localStorage' | 'sessionStorage';
14+
storagePrefix?: string;
15+
log?: boolean;
1716
}
1817

19-
export function createQueryParamStore<T>(
20-
opts: QueryParamStoreOptions<T> = {
21-
key: 'queryParam',
22-
replaceState: true,
18+
const stringify = (value) => {
19+
if (typeof value === 'undefined' || value === null) return undefined;
20+
if (typeof value === 'string') return value;
21+
return JSON.stringify(value);
22+
};
23+
24+
const parse = (value: string) => {
25+
if (typeof value === 'undefined') return undefined;
26+
try {
27+
return JSON.parse(value);
28+
} catch {
29+
return value; // if the original input was just a string (and never JSON stringified), it will throw an error so just return the string
30+
}
31+
};
32+
33+
export function createQueryParamStore<T>(opts: QueryParamStoreOptions<T>) {
34+
const { key, log, persist } = opts;
35+
const replaceState = typeof opts.replaceState === 'undefined' ? true : opts.replaceState;
36+
const storageKey = `${opts.storagePrefix || ''}${key}`
37+
38+
let storage: Storage = undefined
39+
if (typeof window !== 'undefined') {
40+
if (persist === 'localStorage')
41+
storage = localStorage;
42+
if (persist === 'sessionStorage')
43+
storage = sessionStorage;
2344
}
24-
): QueryParamStore<T> {
25-
const { key, startWith, log, replaceState, persist } = opts;
2645

27-
const updateQueryParam = (value: any) => {
46+
const setQueryParam = (value) => {
2847
if (typeof window === 'undefined') return; // safety check in case store value is assigned via $: call server side
2948
if (value === undefined || value === null) return removeQueryParam();
30-
// from https://github.com/sveltejs/kit/issues/969
31-
const url = new URL(window.location.href);
32-
url.searchParams.set(key, encodeParam(value));
33-
34-
if (replaceState) {
35-
history.replaceState({}, '', url);
36-
setStoreValue(value);
37-
} else {
38-
goto(url.toString(), { noScroll: true }); // breaks input focus
39-
}
40-
41-
log && console.log(`user action changed: ${key} to ${value}`);
49+
const {hash} = window.location
50+
const searchParams = new URLSearchParams(window.location.search)
51+
searchParams.set(key, stringify(value));
52+
goto(`?${searchParams}${hash}`, { keepFocus: true, noScroll: true, replaceState });
53+
if (log) console.info(`user action changed: ${key} to ${value}`);
4254
};
4355

44-
const removeQueryParam = () => {
45-
const url = new URL(window.location.href);
46-
url.searchParams.delete(key);
47-
48-
if (replaceState) {
49-
history.replaceState({}, '', url);
50-
setStoreValue(null);
51-
} else {
52-
goto(url.toString(), { noScroll: true }); // breaks input focus
53-
}
56+
const updateQueryParam = (fn: (value: T) => T) => {
57+
const searchParams = new URLSearchParams(window.location.search)
58+
const value = searchParams.get(key);
59+
const parsed_value = parse(value) as T;
60+
setQueryParam(fn(parsed_value));
61+
}
5462

55-
log && console.log(`user action removed: ${key}`);
63+
const removeQueryParam = () => {
64+
const {hash} = window.location
65+
const searchParams = new URLSearchParams(window.location.search)
66+
searchParams.delete(key);
67+
goto(`?${searchParams}${hash}`, { keepFocus: true, noScroll: true, replaceState });
68+
if (log) console.info(`user action removed: ${key}`);
5669
};
5770

5871
const setStoreValue = (value: string) => {
59-
const properlyTypedValue = decodeParam(value) as T;
60-
typeof window !== 'undefined' && localStorage.setItem(key, JSON.stringify(properlyTypedValue));
61-
log && console.log(`URL set ${key} to ${properlyTypedValue}`);
62-
set(properlyTypedValue);
72+
const parsed_value = parse(value) as T;
73+
set(parsed_value);
74+
if (log) console.info(`URL set ${key} to ${parsed_value}`);
75+
storage?.setItem(storageKey, JSON.stringify(parsed_value));
76+
if (log && storage) console.info({[storageKey + '_to_cache']: parsed_value});
6377
};
6478

6579
let firstUrlCheck = true;
6680

6781
const start = () => {
68-
const _teardown = page.subscribe(({ url: { searchParams } }) => {
82+
const unsubscribe_from_page_store = page.subscribe(({ url: { searchParams } }) => {
6983
let value = searchParams.get(key);
7084

71-
// Subsequent URL changes
85+
// Set store value from url - skipped on first load
7286
if (!firstUrlCheck) return setStoreValue(value);
7387
firstUrlCheck = false;
7488

75-
// URL load
76-
// 1st Priority: query param
77-
// @ts-ignore
89+
// 1st Priority: check url query param for value
7890
if (value !== undefined && value !== null && value !== '') return setStoreValue(value);
7991

80-
// 2nd Priority: local/sessionStorage
8192
if (typeof window === 'undefined') return;
82-
if (persist === 'localStorage') {
83-
value = JSON.parse(localStorage.getItem(key));
84-
log && console.log(`cached: ${key} is ${value}`);
85-
}
86-
if (persist === 'sessionStorage') {
87-
value = JSON.parse(sessionStorage.getItem(key));
88-
log && console.log(`cached: ${key} is ${value}`);
93+
94+
// 2nd Priority: check localStorage/sessionStorage for value
95+
if (persist) {
96+
value = JSON.parse(storage.getItem(storageKey));
97+
if (log) console.info({[storageKey + '_from_cache']: value});
8998
}
90-
if (value) return updateQueryParam(value);
99+
100+
if (value) return setQueryParam(value);
91101
});
92102

93-
// Unsubscribes from page store when query param store is no longer in use
94-
return () => _teardown();
103+
return () => unsubscribe_from_page_store();
95104
};
96105

97-
// 3rd Priority: startWith won't be overridden if query param nor local/sessionStorage key is set
98-
const store = writable<T>(startWith, start);
106+
// 3rd Priority: use startWith if no query param in url nor storage value found
107+
const store = writable<T>(opts.startWith, start);
99108
const { subscribe, set } = store;
100109

101110
return {
102111
subscribe,
103-
set: updateQueryParam,
112+
set: setQueryParam,
113+
update: updateQueryParam,
104114
remove: removeQueryParam,
105115
};
106116
}
107117

108-
// const newValues = {}
109-
// for (const key of page.url.searchParams.keys()) {
110-
// console.log(page.url.searchParams.get(key));
111-
// newValues[key] = page.url.searchParams.get(key);
112-
// set(newValues)
113-
// }
114-
115-
// window.addEventListener('popstate', (e) => {
116-
// console.log(e);
117-
// const { searchParams } = new URL(window.location.href);
118-
// console.log(`${searchParams.get(key)}, ${e.state}`);
119-
// });
118+
// SvelteKit Goto dicussion https://github.com/sveltejs/kit/issues/969

src/lib/stores/url-helpers.ts

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)