diff --git a/README.md b/README.md index 87b424d..2d55e7e 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ export const preferences = persisted('local-storage-key', 'default-value', { syncTabs: true, // choose whether to sync localStorage across tabs, default is true onWriteError: (error) => {/* handle or rethrow */}, // Defaults to console.error with the error object onParseError: (raw, error) => {/* handle or rethrow */}, // Defaults to console.error with the error object + beforeRead: (value) => {/* change value after serialization but before setting store to return value*/}, + beforeWrite: (value) => {/* change value after writing to store, but before writing return value to local storage*/}, }) ``` diff --git a/index.ts b/index.ts index 3e11ca2..426993c 100644 --- a/index.ts +++ b/index.ts @@ -21,13 +21,15 @@ export interface Serializer { export type StorageType = 'local' | 'session' -export interface Options { - serializer?: Serializer +export interface Options { + serializer?: Serializer storage?: StorageType, syncTabs?: boolean, onError?: (e: unknown) => void onWriteError?: (e: unknown) => void onParseError?: (newValue: string | null, e: unknown) => void + beforeRead?: (val: SerializerType) => StoreType + beforeWrite?: (val: StoreType) => SerializerType } function getStorage(type: StorageType) { @@ -35,11 +37,11 @@ function getStorage(type: StorageType) { } /** @deprecated `writable()` has been renamed to `persisted()` */ -export function writable(key: string, initialValue: T, options?: Options): Writable { +export function writable(key: string, initialValue: StoreType, options?: Options): Writable { console.warn("writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'") - return persisted(key, initialValue, options) + return persisted(key, initialValue, options) } -export function persisted(key: string, initialValue: T, options?: Options): Writable { +export function persisted(key: string, initialValue: StoreType, options?: Options): Writable { if (options?.onError) console.warn("onError has been deprecated. Please use onWriteError instead") const serializer = options?.serializer ?? JSON @@ -47,29 +49,39 @@ export function persisted(key: string, initialValue: T, options?: Options) const syncTabs = options?.syncTabs ?? true const onWriteError = options?.onWriteError ?? options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e)) const onParseError = options?.onParseError ?? ((newVal, e) => console.error(`Error when parsing ${newVal ? '"' + newVal + '"' : "value"} from persisted store "${key}"`, e)) + + const beforeRead = options?.beforeRead ?? ((val) => val as unknown as StoreType) + const beforeWrite = options?.beforeWrite ?? ((val) => val as unknown as SerializerType) + const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined' const storage = browser ? getStorage(storageType) : null - function updateStorage(key: string, value: T) { + function updateStorage(key: string, value: StoreType) { + const newVal = beforeWrite(value) + try { - storage?.setItem(key, serializer.stringify(value)) + storage?.setItem(key, serializer.stringify(newVal)) } catch (e) { onWriteError(e) } } - function maybeLoadInitial(): T { - const json = storage?.getItem(key) - - if (json) { + function maybeLoadInitial(): StoreType { + function serialize(json: any) { try { - return serializer.parse(json) + return serializer.parse(json) } catch (e) { onParseError(json, e) } } + const json = storage?.getItem(key) + if (json == null) return initialValue - return initialValue + const serialized = serialize(json) + if (serialized == null) return initialValue + + const newVal = beforeRead(serialized) + return newVal } if (!stores[storageType][key]) { @@ -77,15 +89,17 @@ export function persisted(key: string, initialValue: T, options?: Options) const store = internal(initial, (set) => { if (browser && storageType == 'local' && syncTabs) { const handleStorage = (event: StorageEvent) => { - if (event.key === key) { + if (event.key === key && event.newValue) { let newVal: any try { - newVal = event.newValue ? serializer.parse(event.newValue) : null + newVal = serializer.parse(event.newValue) } catch (e) { onParseError(event.newValue, e) return } - set(newVal) + const processedVal = beforeRead(newVal) + + set(processedVal) } } @@ -98,11 +112,11 @@ export function persisted(key: string, initialValue: T, options?: Options) const { subscribe, set } = store stores[storageType][key] = { - set(value: T) { + set(value: StoreType) { set(value) updateStorage(key, value) }, - update(callback: Updater) { + update(callback: Updater) { return store.update((last) => { const value = callback(last) diff --git a/test/localStorageStore.test.ts b/test/localStorageStore.test.ts index 557e7f3..ec00a38 100644 --- a/test/localStorageStore.test.ts +++ b/test/localStorageStore.test.ts @@ -90,7 +90,7 @@ describe('persisted()', () => { it('publishes updates', () => { const store = persisted('myKey7', 123) const values: number[] = [] - const unsub = store.subscribe((value : number) => { + const unsub = store.subscribe((value: number) => { if (value !== undefined) values.push(value) }) store.set(456) @@ -132,37 +132,67 @@ describe('persisted()', () => { unsub2() }) + describe("beforeRead and beforeWrite", () => { + it("allows modifying initial value before reading", () => { + localStorage.setItem("beforeRead-init-test", JSON.stringify(2)) + const store = persisted("beforeRead-init-test", 0, { beforeRead: (v: number) => v * 2 }) + expect(get(store)).toEqual(4) + }) + it("allows modifying value before reading upon event", () => { + const store = persisted("beforeRead-test", 0, { beforeRead: (v: number) => v * 2 }) + const values: number[] = [] + + const unsub = store.subscribe((val: number) => { + values.push(val) + }) + + const event = new StorageEvent('storage', { key: 'beforeRead-test', newValue: "2" }) + window.dispatchEvent(event) + + expect(values).toEqual([0, 4]) + + unsub() + }) + + it("allows modifying value before writing", () => { + const store = persisted("beforeWrite-test", 0, { beforeWrite: (v) => v * 2 }) + store.set(2) + + expect(JSON.parse(localStorage.getItem("beforeWrite-test") as string)).toEqual(4) + }) + }) + describe('handles window.storage event', () => { - type NumberDict = { [key: string] : number } + type NumberDict = { [key: string]: number } it('sets storage when key matches', () => { - const store = persisted('myKey8', {a: 1}) + const store = persisted('myKey8', { a: 1 }) const values: NumberDict[] = [] const unsub = store.subscribe((value: NumberDict) => { values.push(value) }) - const event = new StorageEvent('storage', {key: 'myKey8', newValue: '{"a": 1, "b": 2}'}) + const event = new StorageEvent('storage', { key: 'myKey8', newValue: '{"a": 1, "b": 2}' }) window.dispatchEvent(event) - expect(values).toEqual([{a: 1}, {a: 1, b: 2}]) + expect(values).toEqual([{ a: 1 }, { a: 1, b: 2 }]) unsub() }) - it('sets store to null when value is null', () => { - const store = persisted('myKey9', {a: 1}) + it('ignores storages events when value is null', () => { + const store = persisted('myKey9', { a: 1 }) const values: NumberDict[] = [] const unsub = store.subscribe((value: NumberDict) => { values.push(value) }) - const event = new StorageEvent('storage', {key: 'myKey9', newValue: null}) + const event = new StorageEvent('storage', { key: 'myKey9', newValue: null }) window.dispatchEvent(event) - expect(values).toEqual([{a: 1}, null]) + expect(values).toEqual([{ a: 1 }]) unsub() }) @@ -175,7 +205,7 @@ describe('persisted()', () => { values.push(value) }) - const event = new StorageEvent('storage', {key: 'unknownKey', newValue: '2'}) + const event = new StorageEvent('storage', { key: 'unknownKey', newValue: '2' }) window.dispatchEvent(event) expect(values).toEqual([1]) @@ -189,7 +219,7 @@ describe('persisted()', () => { const store = persisted('myKeyb', 1) const values: number[] = [] - const event = new StorageEvent('storage', {key: 'myKeyb', newValue: '2'}) + const event = new StorageEvent('storage', { key: 'myKeyb', newValue: '2' }) window.dispatchEvent(event) const unsub = store.subscribe((value: number) => { @@ -209,7 +239,7 @@ describe('persisted()', () => { values.push(value) }) - const event = new StorageEvent('storage', {key: 'myKey10', newValue: '2'}) + const event = new StorageEvent('storage', { key: 'myKey10', newValue: '2' }) window.dispatchEvent(event) expect(values).toEqual([1]) @@ -219,13 +249,13 @@ describe('persisted()', () => { it("doesn't update, when syncTabs option is disabled", () => { const store = persisted('myKey13', 1, { syncTabs: false }) - const values = [] + const values: number[] = [] const unsub = store.subscribe((value) => { values.push(value) }) - const event = new StorageEvent('storage', {key: 'myKey13', newValue: '2'}) + const event = new StorageEvent('storage', { key: 'myKey13', newValue: '2' }) window.dispatchEvent(event) expect(values).toEqual([1]) @@ -248,7 +278,7 @@ describe('persisted()', () => { store.update(d => d.add(4)) expect(value).toEqual(testSet) - expect(localStorage.myKey11).toEqual(serializer.stringify(new Set([1,2,3,4]))) + expect(localStorage.myKey11).toEqual(serializer.stringify(new Set([1, 2, 3, 4]))) }) it('lets you switch storage type', () => {