Skip to content

Commit

Permalink
feat(useStorage): allow custom serializer (#528)
Browse files Browse the repository at this point in the history
* feat(useStorage): allow custom serializer

* update test
  • Loading branch information
g-plane committed May 24, 2021
1 parent 7b30a8b commit f4d534f
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 34 deletions.
6 changes: 3 additions & 3 deletions packages/core/useDark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { defaultWindow } from '../_configurable'
import { usePreferredDark } from '../usePreferredDark'
import { tryOnMounted } from '@vueuse/shared'

export interface UseDarkOptions extends StorageOptions {
export type ColorSchemes = 'light' | 'dark' | 'auto'

export interface UseDarkOptions extends StorageOptions<ColorSchemes> {
/**
* CSS Selector for the target element applying to
*
Expand Down Expand Up @@ -58,8 +60,6 @@ export interface UseDarkOptions extends StorageOptions {
storage?: StorageLike
}

export type ColorSchemes = 'light' | 'dark' | 'auto'

/**
* Reactive dark mode with auto data persistence.
*
Expand Down
12 changes: 6 additions & 6 deletions packages/core/useLocalStorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { StorageOptions, useStorage } from '../useStorage'
import { Ref } from 'vue-demi'
import { defaultWindow } from '../_configurable'

export function useLocalStorage (key: string, defaultValue: string, options?: StorageOptions): Ref<string>
export function useLocalStorage (key: string, defaultValue: boolean, options?: StorageOptions): Ref<boolean>
export function useLocalStorage(key: string, defaultValue: number, options?: StorageOptions): Ref<number>
export function useLocalStorage<T> (key: string, defaultValue: T, options?: StorageOptions): Ref<T>
export function useLocalStorage<T = unknown> (key: string, defaultValue: null, options?: StorageOptions): Ref<T>
export function useLocalStorage (key: string, defaultValue: string, options?: StorageOptions<string>): Ref<string>
export function useLocalStorage (key: string, defaultValue: boolean, options?: StorageOptions<boolean>): Ref<boolean>
export function useLocalStorage(key: string, defaultValue: number, options?: StorageOptions<number>): Ref<number>
export function useLocalStorage<T> (key: string, defaultValue: T, options?: StorageOptions<T>): Ref<T>
export function useLocalStorage<T = unknown> (key: string, defaultValue: null, options?: StorageOptions<T>): Ref<T>

/**
* Reactive LocalStorage.
Expand All @@ -16,7 +16,7 @@ export function useLocalStorage<T = unknown> (key: string, defaultValue: null, o
* @param defaultValue
* @param options
*/
export function useLocalStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T, options: StorageOptions = {}): Ref<any> {
export function useLocalStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T, options: StorageOptions<T> = {}): Ref<any> {
const { window = defaultWindow } = options
return useStorage(key, defaultValue, window?.localStorage, options)
}
12 changes: 6 additions & 6 deletions packages/core/useSessionStorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { StorageOptions, useStorage } from '../useStorage'
import { Ref } from 'vue-demi'
import { defaultWindow } from '../_configurable'

export function useSessionStorage (key: string, defaultValue: string, options?: StorageOptions): Ref<string>
export function useSessionStorage (key: string, defaultValue: boolean, options?: StorageOptions): Ref<boolean>
export function useSessionStorage(key: string, defaultValue: number, options?: StorageOptions): Ref<number>
export function useSessionStorage<T> (key: string, defaultValue: T, options?: StorageOptions): Ref<T>
export function useSessionStorage<T = unknown> (key: string, defaultValue: null, options?: StorageOptions): Ref<T>
export function useSessionStorage (key: string, defaultValue: string, options?: StorageOptions<string>): Ref<string>
export function useSessionStorage (key: string, defaultValue: boolean, options?: StorageOptions<boolean>): Ref<boolean>
export function useSessionStorage(key: string, defaultValue: number, options?: StorageOptions<number>): Ref<number>
export function useSessionStorage<T> (key: string, defaultValue: T, options?: StorageOptions<T>): Ref<T>
export function useSessionStorage<T = unknown> (key: string, defaultValue: null, options?: StorageOptions<T>): Ref<T>

/**
* Reactive SessionStorage.
Expand All @@ -16,7 +16,7 @@ export function useSessionStorage<T = unknown> (key: string, defaultValue: null,
* @param defaultValue
* @param options
*/
export function useSessionStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T, options: StorageOptions = {}): Ref<any> {
export function useSessionStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T, options: StorageOptions<T> = {}): Ref<any> {
const { window = defaultWindow } = options
return useStorage(key, defaultValue, window?.sessionStorage, options)
}
37 changes: 37 additions & 0 deletions packages/core/useStorage/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,41 @@ describe('useStorage', () => {

expect(localStorage.setItem).toBeCalledWith(KEY, '{"name":"b","data":321}')
})

it('custom serializer', async() => {
expect(localStorage.getItem(KEY)).toEqual(undefined)

const instance = useSetup(() => {
const ref = useStorage(KEY, null, localStorage, { serializer: { read: JSON.parse, write: JSON.stringify } })

expect(localStorage.setItem).toBeCalledWith(KEY, 'null')

expect(ref.value).toBe(null)

return {
ref,
}
})

instance.ref = { name: 'a', data: 123 }
await nextTick()

expect(localStorage.setItem).toBeCalledWith(KEY, '{"name":"a","data":123}')

instance.ref.name = 'b'
await nextTick()

expect(localStorage.setItem).toBeCalledWith(KEY, '{"name":"b","data":123}')

instance.ref.data = 321
await nextTick()

expect(localStorage.setItem).toBeCalledWith(KEY, '{"name":"b","data":321}')

// @ts-ignore
instance.ref = null
await nextTick()

expect(localStorage.removeItem).toBeCalledWith(KEY)
})
})
52 changes: 33 additions & 19 deletions packages/core/useStorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,38 @@ import { ref, Ref } from 'vue-demi'
import { useEventListener } from '../useEventListener'
import { ConfigurableWindow, defaultWindow } from '../_configurable'

const Serializers = {
export type Serializer<T> = {
read(raw: string): T

write(value: T): string
}

const Serializers: Record<string, Serializer<any>> = {
boolean: {
read: (v: any, d: any) => v != null ? v === 'true' : d,
read: (v: any) => v != null ? v === 'true' : null,
write: (v: any) => String(v),
},
object: {
read: (v: any, d: any) => v ? JSON.parse(v) : d,
read: (v: any) => v ? JSON.parse(v) : null,
write: (v: any) => JSON.stringify(v),
},
number: {
read: (v: any, d: any) => v != null ? Number.parseFloat(v) : d,
read: (v: any) => v != null ? Number.parseFloat(v) : null,
write: (v: any) => String(v),
},
any: {
read: (v: any, d: any) => v != null ? v : d,
read: (v: any) => v != null ? v : null,
write: (v: any) => String(v),
},
string: {
read: (v: any, d: any) => v != null ? v : d,
read: (v: any) => v != null ? v : null,
write: (v: any) => String(v),
},
}

export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>

export interface StorageOptions extends ConfigurableEventFilter, ConfigurableWindow, ConfigurableFlush {
export interface StorageOptions<T> extends ConfigurableEventFilter, ConfigurableWindow, ConfigurableFlush {
/**
* Watch for deep changes
*
Expand All @@ -42,13 +48,18 @@ export interface StorageOptions extends ConfigurableEventFilter, ConfigurableWin
* @default true
*/
listenToStorageChanges?: boolean

/**
* Custom data serialization
*/
serializer?: Serializer<T>
}

export function useStorage(key: string, defaultValue: string, storage?: StorageLike, options?: StorageOptions): Ref<string>
export function useStorage(key: string, defaultValue: boolean, storage?: StorageLike, options?: StorageOptions): Ref<boolean>
export function useStorage(key: string, defaultValue: number, storage?: StorageLike, options?: StorageOptions): Ref<number>
export function useStorage<T> (key: string, defaultValue: T, storage?: StorageLike, options?: StorageOptions): Ref<T>
export function useStorage<T = unknown> (key: string, defaultValue: null, storage?: StorageLike, options?: StorageOptions): Ref<T>
export function useStorage(key: string, defaultValue: string, storage?: StorageLike, options?: StorageOptions<string>): Ref<string>
export function useStorage(key: string, defaultValue: boolean, storage?: StorageLike, options?: StorageOptions<boolean>): Ref<boolean>
export function useStorage(key: string, defaultValue: number, storage?: StorageLike, options?: StorageOptions<number>): Ref<number>
export function useStorage<T> (key: string, defaultValue: T, storage?: StorageLike, options?: StorageOptions<T>): Ref<T>
export function useStorage<T = unknown> (key: string, defaultValue: null, storage?: StorageLike, options?: StorageOptions<T>): Ref<T>

/**
* Reactive LocalStorage/SessionStorage.
Expand All @@ -63,7 +74,7 @@ export function useStorage<T extends(string|number|boolean|object|null)> (
key: string,
defaultValue: T,
storage: StorageLike | undefined = defaultWindow?.localStorage,
options: StorageOptions = {},
options: StorageOptions<T> = {},
) {
const {
flush = 'pre',
Expand All @@ -88,6 +99,7 @@ export function useStorage<T extends(string|number|boolean|object|null)> (
: !Number.isNaN(defaultValue)
? 'number'
: 'any'
const serializer = options.serializer ?? Serializers[type]

function read(event?: StorageEvent) {
if (!storage)
Expand All @@ -97,12 +109,14 @@ export function useStorage<T extends(string|number|boolean|object|null)> (
return

try {
let rawValue = event ? event.newValue : storage.getItem(key)
if (rawValue == null && defaultValue) {
rawValue = Serializers[type].write(defaultValue)
storage.setItem(key, rawValue)
const rawValue = event ? event.newValue : storage.getItem(key)
if (rawValue == null) {
(data as Ref<T>).value = defaultValue
storage.setItem(key, serializer.write(defaultValue))
}
else {
data.value = serializer.read(rawValue)
}
data.value = Serializers[type].read(rawValue, defaultValue)
}
catch (e) {
console.warn(e)
Expand All @@ -124,7 +138,7 @@ export function useStorage<T extends(string|number|boolean|object|null)> (
if (data.value == null)
storage.removeItem(key)
else
storage.setItem(key, Serializers[type].write(data.value))
storage.setItem(key, serializer.write(data.value))
}
catch (e) {
console.warn(e)
Expand Down

0 comments on commit f4d534f

Please sign in to comment.