Skip to content

Commit

Permalink
feat(useStorage): support for primitives
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Feb 21, 2020
1 parent 677817a commit 07c4fd9
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 120 deletions.
1 change: 1 addition & 0 deletions packages/_docs/test.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ let state = {}
const localStorageMock = {
getItem: jest.fn(x => state[x]),
setItem: jest.fn((x, v) => state[x] = v),
removeItem: jest.fn((x, v) => delete state[x]),
clear: jest.fn(() => state = {}),
}

Expand Down
12 changes: 8 additions & 4 deletions packages/core/useLocalStorage/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useStorage } from '../useStorage'
import { Ref } from '../../api'

export function useLocalStorage<T>(
key: string,
defaultValue?: T,
) {
export function useLocalStorage (key: string, defaultValue: string, storage?: Storage): Ref<string>
export function useLocalStorage (key: string, defaultValue: boolean, storage?: Storage): Ref<boolean>
export function useLocalStorage (key: string, defaultValue: number, storage?: Storage): Ref<number>
export function useLocalStorage<T extends object> (key: string, defaultValue: T, storage?: Storage): Ref<T>
export function useLocalStorage<T extends null> (key: string, defaultValue: null, storage?: Storage): Ref<any>
export function useLocalStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T) {
// @ts-ignore
return useStorage(key, defaultValue, localStorage)
}
12 changes: 8 additions & 4 deletions packages/core/useSessionStorage/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useStorage } from '../useStorage'
import { Ref } from '../../api'

export function useSessionStorage<T>(
key: string,
defaultValue?: T,
) {
export function useSessionStorage (key: string, defaultValue: string, storage?: Storage): Ref<string>
export function useSessionStorage (key: string, defaultValue: boolean, storage?: Storage): Ref<boolean>
export function useSessionStorage (key: string, defaultValue: number, storage?: Storage): Ref<number>
export function useSessionStorage<T extends object> (key: string, defaultValue: T, storage?: Storage): Ref<T>
export function useSessionStorage<T extends null> (key: string, defaultValue: null, storage?: Storage): Ref<any>
export function useSessionStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T) {
// @ts-ignore
return useStorage(key, defaultValue, sessionStorage)
}
15 changes: 14 additions & 1 deletion packages/core/useStorage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,18 @@
```jsx
import { useStorage } from '@vueuse/core'

const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' }, sessionStorage)
// bind object
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })

// bind boolean
const flag = useStorage('my-flag', true) // returns Ref<boolean>

// bind number
const count = useStorage('my-count', 0) // returns Ref<number>

// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>

// delete data from storage
state.value = null
```
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { renderHook } from '../../_docs/tests'
import { useStoragePlain } from '.'
import { useStorage } from '.'

const KEY = 'key'
const KEY = 'custom-key'

describe('useStoragePlain', () => {
describe('useStorage', () => {
afterEach(() => {
localStorage.clear()
// @ts-ignore
localStorage.setItem.mockClear()
// @ts-ignore
localStorage.getItem.mockClear()
// @ts-ignore
localStorage.removeItem.mockClear()
})

it('string', () => {
renderHook(() => {
const ref = useStoragePlain(KEY, 'a')
const ref = useStorage(KEY, 'a')

expect(ref.value).toBe('a')

Expand All @@ -26,7 +32,7 @@ describe('useStoragePlain', () => {
localStorage.setItem(KEY, '0')

renderHook(() => {
const ref = useStoragePlain(KEY, 1)
const ref = useStorage(KEY, 1)

expect(ref.value).toBe(0)

Expand All @@ -48,7 +54,7 @@ describe('useStoragePlain', () => {
localStorage.setItem(KEY, 'false')

renderHook(() => {
const ref = useStoragePlain(KEY, true)
const ref = useStorage(KEY, true)

expect(ref.value).toBe(false)

Expand All @@ -66,7 +72,7 @@ describe('useStoragePlain', () => {
localStorage.setItem(KEY, '0')

renderHook(() => {
const ref = useStoragePlain(KEY, null)
const ref = useStorage(KEY, null)

expect(ref.value).toBe('0')
})
Expand All @@ -76,7 +82,7 @@ describe('useStoragePlain', () => {
localStorage.setItem(KEY, '0')

renderHook(() => {
const ref = useStoragePlain(KEY, '1')
const ref = useStorage(KEY, '1')

expect(ref.value).toBe('0')

Expand All @@ -85,4 +91,35 @@ describe('useStoragePlain', () => {
expect(localStorage.setItem).toBeCalledWith(KEY, '2')
})
})

it('object', () => {
expect(localStorage.getItem(KEY)).toEqual(undefined)

renderHook(() => {
const ref = useStorage(KEY, {
name: 'a',
data: 123,
})

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

expect(ref.value).toEqual({
name: 'a',
data: 123,
})

ref.value.name = 'b'

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

ref.value.data = 321

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

// @ts-ignore
ref.value = null

expect(localStorage.removeItem).toBeCalledWith(KEY)
})
})
})
97 changes: 76 additions & 21 deletions packages/core/useStorage/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,85 @@
import { ref, watch } from '../../api'
import { useStoragePlain } from '../useStoragePlain'

export function useStorage<T>(
key: string,
defaultValue?: T,
storage: Storage = localStorage,
) {
function stringify(data?: T) {
return data ? JSON.stringify(data) : ''
}
import { ref, watch, Ref } from '../../api'
import { useEventListener } from '../useEventListener'

function parse(str?: string) {
return str ? JSON.parse(str) : (defaultValue || {})
}
const Serializers = {
boolean: {
read: (v: any) => v === 'true',
write: (v: any) => String(v),
},
object: {
read: (v: any, d: any) => v ? JSON.parse(v) : d,
write: (v: any) => JSON.stringify(v),
},
number: {
read: (v: any, d: any) => v != null ? Number.parseFloat(v) : d,
write: (v: any) => String(v),
},
any: {
read: (v: any, d: any) => v ?? d,
write: (v: any) => String(v),
},
string: {
read: (v: any, d: any) => v ?? d,
write: (v: any) => String(v),
},
}

const plain = useStoragePlain(key, stringify(defaultValue), storage)
export function useStorage (key: string, defaultValue: string, storage?: Storage): Ref<string>
export function useStorage (key: string, defaultValue: boolean, storage?: Storage): Ref<boolean>
export function useStorage (key: string, defaultValue: number, storage?: Storage): Ref<number>
export function useStorage<T extends object> (key: string, defaultValue: T, storage?: Storage): Ref<T>
export function useStorage<T extends null> (key: string, defaultValue: null, storage?: Storage): Ref<any>
export function useStorage<T extends(string|number|boolean|object|null)> (key: string, defaultValue: T, storage: Storage = localStorage) {
const data = ref<T>(defaultValue)

const state = ref<T>(parse(plain.value))
const type = defaultValue == null
? 'any'
: typeof defaultValue === 'boolean'
? 'boolean'
: typeof defaultValue === 'string'
? 'string'
: typeof defaultValue === 'object'
? 'object'
// @ts-ignore
: !Number.isNaN(defaultValue)
? 'number'
: 'any'

function update() {
plain.value = stringify(state.value as any as T)
function read() {
try {
let rawValue = storage.getItem(key)
if (rawValue === undefined && defaultValue) {
rawValue = Serializers[type].write(defaultValue)
storage.setItem(key, rawValue)
}
else {
data.value = Serializers[type].read(rawValue, defaultValue)
}
}
catch (e) {
console.warn(e)
}
}

watch(plain, () => state.value = parse(plain.value), { flush: 'sync', lazy: true })
read()

useEventListener('storage', read)

watch(state, update, { flush: 'sync', lazy: true, deep: true })
watch(
data,
() => {
try {
if (data.value == null)
storage.removeItem(key)
else
storage.setItem(key, Serializers[type].write(data.value))
}
catch (e) {
console.warn(e)
}
},
{ flush: 'sync', lazy: true, deep: true },
)

return state
return data
}
20 changes: 1 addition & 19 deletions packages/core/useStoragePlain/index.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
# useStoragePlain

> Reactive LocalStorage/SessionStorage with primitives values
## Usage

```jsx
import { useStoragePlain } from '@vueuse/core'

export default {
setup() {
const greeting = useStoragePlain('my-store', 'hello')

greeting.value = 'hi'

return {
greeting,
}
},
}
```
> ⚠ DEPRATED, use `useStorage` instead.
65 changes: 2 additions & 63 deletions packages/core/useStoragePlain/index.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,3 @@
import { ref, watch, Ref } from '../../api'
import { useEventListener } from '../useEventListener'
import { useStorage } from '../useStorage'

export function useStoragePlain (key: string, defaultValue: string, storage?: Storage): Ref<string>
export function useStoragePlain (key: string, defaultValue: boolean, storage?: Storage): Ref<boolean>
export function useStoragePlain (key: string, defaultValue: number, storage?: Storage): Ref<number>
export function useStoragePlain (key: string, defaultValue: null, storage?: Storage): Ref<any>
export function useStoragePlain<T extends object> (key: string, defaultValue: T, storage?: Storage): Ref<T>
export function useStoragePlain<T extends(string|number|boolean|null)> (key: string, defaultValue: T, storage: Storage = localStorage) {
const data = ref<T>(defaultValue)

function read() {
try {
let rawValue = storage.getItem(key)
if (rawValue === undefined && defaultValue) {
rawValue = String(defaultValue)
storage.setItem(key, rawValue)
}
else {
if (defaultValue == null)
data.value = rawValue as any
else if (typeof defaultValue === 'boolean')
// @ts-ignore
data.value = defaultValue === 'true'
// @ts-ignore
else if (typeof defaultValue === 'string')
// @ts-ignore
data.value = rawValue != null ? rawValue : defaultValue
// @ts-ignore
else if (!Number.isNaN(defaultValue))
// @ts-ignore
data.value = rawValue != null ? Number.parseFloat(rawValue) : defaultValue
else
// @ts-ignore
data.value = rawValue != null ? rawValue : defaultValue
}
}
catch (e) {
console.warn(e)
}
}

read()

useEventListener('storage', read)

watch(
data,
() => {
try {
if (data.value == null)
storage.removeItem(key)
else
storage.setItem(key, String(data.value))
}
catch (e) {
console.warn(e)
}
},
{ flush: 'sync', lazy: true },
)

return data
}
export const useStoragePlain = useStorage

0 comments on commit 07c4fd9

Please sign in to comment.