Skip to content

Commit

Permalink
feat(useIDBKeyval): new integration - Idb-keyval wrapper (#2335)
Browse files Browse the repository at this point in the history
Co-authored-by: Jess Sachs <jess.sachs@pathai.com>
closes #230
  • Loading branch information
Harmony222 committed Nov 8, 2022
1 parent 2c4fdcd commit acd1699
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -65,6 +65,7 @@
"eslint": "^8.25.0",
"esno": "^0.16.3",
"export-size": "^0.5.2",
"fake-indexeddb": "^4.0.0",
"fast-glob": "^3.2.12",
"firebase": "^9.12.1",
"fs-extra": "^10.1.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/.test/polyfillIndexedDb.ts
@@ -0,0 +1,4 @@
// Supports polyfilling window, web workers, as well as node's global
import 'fake-indexeddb/auto'

export {}
1 change: 1 addition & 0 deletions packages/.test/setup.ts
@@ -1,6 +1,7 @@
import { Vue2, install, isVue2 } from 'vue-demi'
import './polyfillFetch'
import './polyfillPointerEvents'
import './polyfillIndexedDb'
import { beforeAll, beforeEach } from 'vitest'

const setupVueSwitch = () => {
Expand Down
1 change: 1 addition & 0 deletions packages/add-ons.md
Expand Up @@ -77,6 +77,7 @@ Integration wrappers for utility libraries
- [`useDrauu`](https://vueuse.org/integrations/useDrauu/) — reactive instance for [drauu](https://github.com/antfu/drauu)
- [`useFocusTrap`](https://vueuse.org/integrations/useFocusTrap/) — reactive wrapper for [`focus-trap`](https://github.com/focus-trap/focus-trap)
- [`useFuse`](https://vueuse.org/integrations/useFuse/) — easily implement fuzzy search using a composable with [Fuse.js](https://github.com/krisk/fuse)
- [`useIDBKeyval`](https://vueuse.org/integrations/useIDBKeyval/) — wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval)
- [`useJwt`](https://vueuse.org/integrations/useJwt/) — wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode)
- [`useNProgress`](https://vueuse.org/integrations/useNProgress/) — reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress)
- [`useQRCode`](https://vueuse.org/integrations/useQRCode/) — wrapper for [`qrcode`](https://github.com/soldair/node-qrcode)
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/README.md
Expand Up @@ -21,6 +21,7 @@ npm i <b>@vueuse/integrations</b>
- [`useDrauu`](https://vueuse.org/integrations/useDrauu/) — reactive instance for [drauu](https://github.com/antfu/drauu)
- [`useFocusTrap`](https://vueuse.org/integrations/useFocusTrap/) — reactive wrapper for [`focus-trap`](https://github.com/focus-trap/focus-trap)
- [`useFuse`](https://vueuse.org/integrations/useFuse/) — easily implement fuzzy search using a composable with [Fuse.js](https://github.com/krisk/fuse)
- [`useIDBKeyval`](https://vueuse.org/integrations/useIDBKeyval/) — wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval)
- [`useJwt`](https://vueuse.org/integrations/useJwt/) — wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode)
- [`useNProgress`](https://vueuse.org/integrations/useNProgress/) — reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress)
- [`useQRCode`](https://vueuse.org/integrations/useQRCode/) — wrapper for [`qrcode`](https://github.com/soldair/node-qrcode)
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/index.ts
Expand Up @@ -5,6 +5,7 @@ export * from './useCookies'
export * from './useDrauu'
export * from './useFocusTrap'
export * from './useFuse'
export * from './useIDBKeyval'
export * from './useJwt'
export * from './useNProgress'
export * from './useQRCode'
10 changes: 10 additions & 0 deletions packages/integrations/package.json
Expand Up @@ -86,6 +86,11 @@
"types": "./useAsyncValidator/component.d.ts",
"require": "./useAsyncValidator/component.cjs",
"import": "./useAsyncValidator/component.mjs"
},
"./useIDBKeyval": {
"types": "./useIDBKeyval.d.ts",
"require": "./useIDBKeyval.cjs",
"import": "./useIDBKeyval.mjs"
}
},
"main": "./index.cjs",
Expand All @@ -100,6 +105,7 @@
"drauu": "*",
"focus-trap": "*",
"fuse.js": "*",
"idb-keyval": "*",
"jwt-decode": "*",
"nprogress": "*",
"qrcode": "*",
Expand All @@ -124,6 +130,9 @@
"fuse.js": {
"optional": true
},
"idb-keyval": {
"optional": true
},
"jwt-decode": {
"optional": true
},
Expand Down Expand Up @@ -151,6 +160,7 @@
"drauu": "^0.3.2",
"focus-trap": "^7.0.0",
"fuse.js": "^6.6.2",
"idb-keyval": "^6.2.0",
"jwt-decode": "^3.1.2",
"nprogress": "^0.2.0",
"qrcode": "^1.5.1",
Expand Down
42 changes: 42 additions & 0 deletions packages/integrations/useIDBKeyval/demo.vue
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { stringify } from '@vueuse/docs-utils'
import { useIDBKeyval } from '@vueuse/integrations'
const KEY = 'vue-use-idb-keyval'
const stateObject = useIDBKeyval(`${KEY}-object`, {
name: 'Banana',
color: 'Yellow',
size: 'Medium',
count: 0,
})
const textObject = stringify(stateObject)
const stateString = useIDBKeyval(`${KEY}-string`, 'foobar')
const textString = stateString
const stateArray = useIDBKeyval(`${KEY}-array`, ['foo', 'bar', 'baz'])
const textArray = stringify(stateArray)
</script>

<template>
<h5>Object</h5>
<input v-model="stateObject.name" type="text">
<input v-model="stateObject.color" type="text">
<input v-model="stateObject.size" type="text">
<input v-model.number="stateObject.count" type="range" min="0" step="0.01" max="1000">

<pre lang="json">{{ textObject }}</pre>
<br>

<h5>String</h5>
<input v-model="stateString" type="text">
<pre>{{ textString }}</pre>
<br>

<h5>Array</h5>
<input v-model="stateArray[0]" type="text">
<input v-model="stateArray[1]" type="text">
<input v-model="stateArray[2]" type="text">
<pre lang="json">{{ textArray }}</pre>
</template>
35 changes: 35 additions & 0 deletions packages/integrations/useIDBKeyval/index.md
@@ -0,0 +1,35 @@
---
category: '@Integrations'
---

# useIDBKeyval

Wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval).


## Install idb-keyval as a peer dependency

```bash
npm install idb-keyval
```

## Usage

```ts
import { useIDBKeyval } from '@vueuse/integrations'

// bind object
const storedObject = useIDBKeyval('my-idb-keyval-store', { hello: 'hi', greeting: 'Hello' })

// update object
storedObject.value.hello = 'hola'

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

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

// delete data from idb storage
storedObject.value = null
```
74 changes: 74 additions & 0 deletions packages/integrations/useIDBKeyval/index.test.ts
@@ -0,0 +1,74 @@
import { nextTick } from 'vue-demi'
import { del, get, set, update } from 'idb-keyval'
import { useIDBKeyval } from '.'

const KEY = 'vue-use-idb-keyval'

const defaultState = {
name: 'Banana',
color: 'Yellow',
size: 'Medium',
count: 0,
}

beforeEach(() => {
vi.unmock('idb-keyval')
vi.mock('idb-keyval')
})

describe('useIDBKeyval', () => {
it('set/get', async () => {
const state = useIDBKeyval(KEY, { ...defaultState })
await nextTick()
expect(get).toHaveBeenCalled()
expect(set).toHaveBeenCalled()
expect(state.value).toEqual(defaultState)
})

it('update', async () => {
const state = useIDBKeyval(KEY, { ...defaultState })
state.value.name = 'Apple'
state.value.color = 'Red'
state.value.size = 'Giant'
state.value.count += 1

await nextTick()
expect(update).toHaveBeenCalled()
expect(state.value.name).toBe('Apple')
expect(state.value.color).toBe('Red')
expect(state.value.size).toBe('Giant')
expect(state.value.count).toBe(defaultState.count + 1)
})

it('del', async () => {
const state = useIDBKeyval(KEY, { ...defaultState })
state.value = null
await nextTick()
expect(del).toHaveBeenCalled()
})

it('string', async () => {
const state = useIDBKeyval(KEY, 'foo')
await nextTick()
expect(get).toHaveBeenCalled()
expect(set).toHaveBeenCalled()
expect(state.value).toEqual('foo')
state.value = 'bar'
await nextTick()
expect(state.value).toEqual('bar')
expect(update).toHaveBeenCalled()
})

it('array', async () => {
const defaultArray = ['foo', 'bar', 'baz']
const state = useIDBKeyval(KEY, [...defaultArray])
await nextTick()
expect(get).toHaveBeenCalled()
expect(set).toHaveBeenCalled()
expect(state.value).toEqual(defaultArray)
state.value[1] = 'boop'
await nextTick()
expect(state.value[1]).toEqual('boop')
expect(update).toHaveBeenCalled()
})
})
95 changes: 95 additions & 0 deletions packages/integrations/useIDBKeyval/index.ts
@@ -0,0 +1,95 @@
import type { ConfigurableFlush, MaybeComputedRef, RemovableRef } from '@vueuse/shared'
import { resolveUnref } from '@vueuse/shared'
import type { Ref } from 'vue-demi'
import { ref, shallowRef, watch } from 'vue-demi'
import { del, get, set, update } from 'idb-keyval'

export interface UseIDBOptions extends ConfigurableFlush {
/**
* Watch for deep changes
*
* @default true
*/
deep?: boolean

/**
* On error callback
*
* Default log error to `console.error`
*/
onError?: (error: unknown) => void

/**
* Use shallow ref as reference
*
* @default false
*/
shallow?: boolean
}

/**
*
* @param key
* @param initialValue
* @param options
*/
export function useIDBKeyval<T>(
key: IDBValidKey,
initialValue: MaybeComputedRef<T>,
options: UseIDBOptions = {},
): RemovableRef<T> {
const {
flush = 'pre',
deep = true,
shallow,
onError = (e) => {
console.error(e)
},
} = options

const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>

const rawInit: T = resolveUnref(initialValue)

async function read() {
try {
const rawValue = await get<T>(key)
if (rawValue === undefined) {
if (rawInit)
set(key, rawInit)
}
else {
data.value = rawValue
}
}
catch (e) {
onError(e)
}
}

read()

async function write() {
try {
if (data.value == null) {
await del(key)
}
else {
// IndexedDB does not support saving proxies, convert from proxy before saving
if (Array.isArray(data.value))
await update(key, () => (JSON.parse(JSON.stringify(data.value))))
else if (typeof data.value === 'object')
await update(key, () => ({ ...data.value }))
else
await update(key, () => (data.value))
}
}
catch (e) {
onError(e)
}
}

watch(data, () => write(), { flush, deep })

return data as RemovableRef<T>
}

0 comments on commit acd1699

Please sign in to comment.