|
1 |
| -import { writable, type Readable } from 'svelte/store'; |
| 1 | +import { writable, type Writable } from 'svelte/store'; |
2 | 2 | import { goto } from '$app/navigation';
|
3 | 3 | import { page } from '$app/stores';
|
4 |
| -import { decodeParam, encodeParam } from './url-helpers'; |
5 | 4 |
|
6 |
| -export interface QueryParamStore<T> extends Readable<T> { |
7 |
| - set: (value: any) => void; |
| 5 | +export interface QueryParamStore<T> extends Writable<T> { |
8 | 6 | remove: () => void;
|
9 | 7 | }
|
10 | 8 |
|
11 | 9 | export interface QueryParamStoreOptions<T> {
|
12 | 10 | key: string;
|
13 |
| - replaceState?: boolean; |
14 | 11 | startWith?: T;
|
15 |
| - log?: boolean; |
| 12 | + replaceState?: boolean; |
16 | 13 | persist?: 'localStorage' | 'sessionStorage';
|
| 14 | + storagePrefix?: string; |
| 15 | + log?: boolean; |
17 | 16 | }
|
18 | 17 |
|
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; |
23 | 44 | }
|
24 |
| -): QueryParamStore<T> { |
25 |
| - const { key, startWith, log, replaceState, persist } = opts; |
26 | 45 |
|
27 |
| - const updateQueryParam = (value: any) => { |
| 46 | + const setQueryParam = (value) => { |
28 | 47 | if (typeof window === 'undefined') return; // safety check in case store value is assigned via $: call server side
|
29 | 48 | 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}`); |
42 | 54 | };
|
43 | 55 |
|
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 | + } |
54 | 62 |
|
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}`); |
56 | 69 | };
|
57 | 70 |
|
58 | 71 | 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}); |
63 | 77 | };
|
64 | 78 |
|
65 | 79 | let firstUrlCheck = true;
|
66 | 80 |
|
67 | 81 | const start = () => {
|
68 |
| - const _teardown = page.subscribe(({ url: { searchParams } }) => { |
| 82 | + const unsubscribe_from_page_store = page.subscribe(({ url: { searchParams } }) => { |
69 | 83 | let value = searchParams.get(key);
|
70 | 84 |
|
71 |
| - // Subsequent URL changes |
| 85 | + // Set store value from url - skipped on first load |
72 | 86 | if (!firstUrlCheck) return setStoreValue(value);
|
73 | 87 | firstUrlCheck = false;
|
74 | 88 |
|
75 |
| - // URL load |
76 |
| - // 1st Priority: query param |
77 |
| - // @ts-ignore |
| 89 | + // 1st Priority: check url query param for value |
78 | 90 | if (value !== undefined && value !== null && value !== '') return setStoreValue(value);
|
79 | 91 |
|
80 |
| - // 2nd Priority: local/sessionStorage |
81 | 92 | 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}); |
89 | 98 | }
|
90 |
| - if (value) return updateQueryParam(value); |
| 99 | + |
| 100 | + if (value) return setQueryParam(value); |
91 | 101 | });
|
92 | 102 |
|
93 |
| - // Unsubscribes from page store when query param store is no longer in use |
94 |
| - return () => _teardown(); |
| 103 | + return () => unsubscribe_from_page_store(); |
95 | 104 | };
|
96 | 105 |
|
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); |
99 | 108 | const { subscribe, set } = store;
|
100 | 109 |
|
101 | 110 | return {
|
102 | 111 | subscribe,
|
103 |
| - set: updateQueryParam, |
| 112 | + set: setQueryParam, |
| 113 | + update: updateQueryParam, |
104 | 114 | remove: removeQueryParam,
|
105 | 115 | };
|
106 | 116 | }
|
107 | 117 |
|
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 |
0 commit comments