diff --git a/package.json b/package.json
index 55d2f46aa5f..b8f18eab6a7 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/packages/.test/polyfillIndexedDb.ts b/packages/.test/polyfillIndexedDb.ts
new file mode 100644
index 00000000000..424b3ceed4e
--- /dev/null
+++ b/packages/.test/polyfillIndexedDb.ts
@@ -0,0 +1,4 @@
+// Supports polyfilling window, web workers, as well as node's global
+import 'fake-indexeddb/auto'
+
+export {}
diff --git a/packages/.test/setup.ts b/packages/.test/setup.ts
index 649688151c7..c2be2dc593c 100644
--- a/packages/.test/setup.ts
+++ b/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 = () => {
diff --git a/packages/add-ons.md b/packages/add-ons.md
index e2a2b01ef03..be5f3349e52 100644
--- a/packages/add-ons.md
+++ b/packages/add-ons.md
@@ -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)
diff --git a/packages/integrations/README.md b/packages/integrations/README.md
index be16c0bd924..3aa79f27eab 100644
--- a/packages/integrations/README.md
+++ b/packages/integrations/README.md
@@ -21,6 +21,7 @@ npm i @vueuse/integrations
- [`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)
diff --git a/packages/integrations/index.ts b/packages/integrations/index.ts
index 9e0711e2fb6..8b2cca5830f 100644
--- a/packages/integrations/index.ts
+++ b/packages/integrations/index.ts
@@ -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'
diff --git a/packages/integrations/package.json b/packages/integrations/package.json
index 203c688df30..21b485fbe18 100644
--- a/packages/integrations/package.json
+++ b/packages/integrations/package.json
@@ -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",
@@ -100,6 +105,7 @@
"drauu": "*",
"focus-trap": "*",
"fuse.js": "*",
+ "idb-keyval": "*",
"jwt-decode": "*",
"nprogress": "*",
"qrcode": "*",
@@ -124,6 +130,9 @@
"fuse.js": {
"optional": true
},
+ "idb-keyval": {
+ "optional": true
+ },
"jwt-decode": {
"optional": true
},
@@ -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",
diff --git a/packages/integrations/useIDBKeyval/demo.vue b/packages/integrations/useIDBKeyval/demo.vue
new file mode 100644
index 00000000000..f5aa7455083
--- /dev/null
+++ b/packages/integrations/useIDBKeyval/demo.vue
@@ -0,0 +1,42 @@
+
+
+
+ Object
+
+
+
+
+
+ {{ textObject }}
+
+
+ String
+
+ {{ textString }}
+
+
+ Array
+
+
+
+ {{ textArray }}
+
diff --git a/packages/integrations/useIDBKeyval/index.md b/packages/integrations/useIDBKeyval/index.md
new file mode 100644
index 00000000000..4434eafea31
--- /dev/null
+++ b/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
+
+// bind number
+const count = useIDBKeyval('my-count', 0) // returns Ref
+
+// delete data from idb storage
+storedObject.value = null
+```
diff --git a/packages/integrations/useIDBKeyval/index.test.ts b/packages/integrations/useIDBKeyval/index.test.ts
new file mode 100644
index 00000000000..b803cfc5fb1
--- /dev/null
+++ b/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()
+ })
+})
diff --git a/packages/integrations/useIDBKeyval/index.ts b/packages/integrations/useIDBKeyval/index.ts
new file mode 100644
index 00000000000..2fec92bb148
--- /dev/null
+++ b/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(
+ key: IDBValidKey,
+ initialValue: MaybeComputedRef,
+ options: UseIDBOptions = {},
+): RemovableRef {
+ const {
+ flush = 'pre',
+ deep = true,
+ shallow,
+ onError = (e) => {
+ console.error(e)
+ },
+ } = options
+
+ const data = (shallow ? shallowRef : ref)(initialValue) as Ref
+
+ const rawInit: T = resolveUnref(initialValue)
+
+ async function read() {
+ try {
+ const rawValue = await get(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
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bc5352271b0..42042ae32a4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,7 @@ importers:
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
@@ -110,6 +111,7 @@ importers:
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
@@ -202,6 +204,7 @@ importers:
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
@@ -220,6 +223,7 @@ importers:
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
@@ -3552,6 +3556,11 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
+ /base64-arraybuffer-es6/0.7.0:
+ resolution: {integrity: sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
/base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
@@ -4306,6 +4315,12 @@ packages:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: true
+ /domexception/1.0.1:
+ resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==}
+ dependencies:
+ webidl-conversions: 4.0.2
+ dev: true
+
/domexception/4.0.0:
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
engines: {node: '>=12'}
@@ -5576,6 +5591,12 @@ packages:
- supports-color
dev: true
+ /fake-indexeddb/4.0.0:
+ resolution: {integrity: sha512-oCfWSJ/qvQn1XPZ8SHX6kY3zr1t+bN7faZ/lltGY0SBGhFOPXnWf0+pbO/MOAgfMx6khC2gK3S/bvAgQpuQHDQ==}
+ dependencies:
+ realistic-structured-clone: 3.0.0
+ dev: true
+
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
@@ -6221,6 +6242,12 @@ packages:
safer-buffer: 2.1.2
dev: true
+ /idb-keyval/6.2.0:
+ resolution: {integrity: sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==}
+ dependencies:
+ safari-14-idb-fix: 3.0.0
+ dev: true
+
/idb/7.0.1:
resolution: {integrity: sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==}
dev: true
@@ -7949,6 +7976,14 @@ packages:
dependencies:
picomatch: 2.3.1
+ /realistic-structured-clone/3.0.0:
+ resolution: {integrity: sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q==}
+ dependencies:
+ domexception: 1.0.1
+ typeson: 6.1.0
+ typeson-registry: 1.0.0-alpha.39
+ dev: true
+
/regenerate-unicode-properties/10.0.1:
resolution: {integrity: sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==}
engines: {node: '>=4'}
@@ -8187,6 +8222,10 @@ packages:
tslib: 2.4.0
dev: true
+ /safari-14-idb-fix/3.0.0:
+ resolution: {integrity: sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==}
+ dev: true
+
/safe-buffer/5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -8835,6 +8874,13 @@ packages:
punycode: 2.1.1
dev: true
+ /tr46/2.1.0:
+ resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==}
+ engines: {node: '>=8'}
+ dependencies:
+ punycode: 2.1.1
+ dev: true
+
/tr46/3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
@@ -8961,6 +9007,20 @@ packages:
hasBin: true
dev: true
+ /typeson-registry/1.0.0-alpha.39:
+ resolution: {integrity: sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ base64-arraybuffer-es6: 0.7.0
+ typeson: 6.1.0
+ whatwg-url: 8.7.0
+ dev: true
+
+ /typeson/6.1.0:
+ resolution: {integrity: sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==}
+ engines: {node: '>=0.1.14'}
+ dev: true
+
/ufo/0.8.5:
resolution: {integrity: sha512-e4+UtA5IRO+ha6hYklwj6r7BjiGMxS0O+UaSg9HbaTefg4kMkzj4tXzEBajRR+wkxf+golgAWKzLbytCUDMJAA==}
@@ -9528,6 +9588,11 @@ packages:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
dev: true
+ /webidl-conversions/6.1.0:
+ resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==}
+ engines: {node: '>=10.4'}
+ dev: true
+
/webidl-conversions/7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -9593,6 +9658,15 @@ packages:
webidl-conversions: 4.0.2
dev: true
+ /whatwg-url/8.7.0:
+ resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==}
+ engines: {node: '>=10'}
+ dependencies:
+ lodash: 4.17.21
+ tr46: 2.1.0
+ webidl-conversions: 6.1.0
+ dev: true
+
/which-boxed-primitive/1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies: