├── .eslintignore ├── .npmignore ├── .gitattributes ├── .prettierrc ├── tsconfig.json ├── .editorconfig ├── bili.config.js ├── types └── vuejs-storage.d.ts ├── run-test.js ├── .github └── workflows │ └── test.yml ├── test ├── index.ts ├── drivers.ts ├── objpath.ts ├── merge.ts ├── install.ts └── vuexplugin.ts ├── src ├── index.ts ├── merge.ts ├── drivers.ts ├── interfaces.ts ├── objpath.ts ├── vuexplugin.ts └── install.ts ├── LICENSE ├── .gitignore ├── karma.conf.js ├── package.json ├── example.html └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | example.html linguist-vendored=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "useTabs": true, 5 | "tabWidth": 4 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "types": ["mocha", "chai"], 5 | "module": "UMD", 6 | "target": "es5", 7 | "lib": ["ES2015", "DOM"], 8 | "moduleResolution": "Node" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | 9 | [md] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [yaml] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /bili.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | format: ['umd', 'umd-min', 'es', 'cjs'], 3 | input: 'src/index.ts', 4 | output: { 5 | dir: 'dist', 6 | moduleName: 'vuejsStorage', 7 | fileName: 'vuejs-storage.[format][min][ext]', 8 | format: ['umd', 'cjs', 'es', 'umd-min', 'cjs-min', 'es-min'], 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /types/vuejs-storage.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/types/index' 2 | import { StorageOptionWithFactory } from '../src/interfaces' 3 | import vjs from '../src/index' 4 | 5 | export = vjs 6 | 7 | declare module 'vue/types/options' { 8 | interface ComponentOptions { 9 | storage?: StorageOptionWithFactory 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /run-test.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('chromium').path 2 | const cp = require('child_process') 3 | const child = cp.spawn(__dirname + '/node_modules/.bin/karma', [ 4 | 'start', 5 | '--single-run', 6 | ]) 7 | process.stdin.pipe(child.stdin) 8 | child.stdout.pipe(process.stdout) 9 | child.stderr.pipe(process.stderr) 10 | child.on('exit', (code) => process.exit(code)) 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 12 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: "12" 14 | - run: yarn 15 | - run: yarn test 16 | - uses: codecov/codecov-action@v1 17 | with: 18 | file: coverage/coverage-final.json 19 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import vuejsStorage from '../src/index' 2 | import { install } from '../src/install' 3 | import { createVuexPlugin } from '../src/vuexplugin' 4 | 5 | const vjs = vuejsStorage 6 | describe('plugin entry', () => { 7 | it('install() should be same', () => [vjs.install.should.equal(install)]) 8 | it('vuex plugin should be same', () => { 9 | const opt = { namespace: 'asd', keys: [] } 10 | vjs(opt) 11 | .toString() 12 | .should.equal(createVuexPlugin(opt).toString()) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/drivers.ts: -------------------------------------------------------------------------------- 1 | import { localStorage, sessionStorage } from '../src/drivers' 2 | 3 | describe('drivers', () => { 4 | it('localStorage', () => { 5 | localStorage.set('a', { a: 1 }) 6 | localStorage.has('a').should.equal(true) 7 | localStorage.get('a').should.deep.equal({ a: 1 }) 8 | localStorage.has('b').should.equal(false) 9 | }) 10 | it('sessionStorage', () => { 11 | sessionStorage.set('a', { a: 1 }) 12 | sessionStorage.has('a').should.equal(true) 13 | sessionStorage.get('a').should.deep.equal({ a: 1 }) 14 | sessionStorage.has('b').should.equal(false) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Vue, VueConstructor, VuexPlugin, StorageDriver } from './interfaces' 2 | 3 | import { install } from './install' 4 | import { createVuexPlugin } from './vuexplugin' 5 | import * as drivers from './drivers' 6 | 7 | export interface vjs { 8 | (): VuexPlugin 9 | install: (Vue: VueConstructor) => void 10 | drivers: { localStorage: StorageDriver; sessionStorage: StorageDriver } 11 | } 12 | const vuejsStorage = function(option) { 13 | return createVuexPlugin(option) 14 | } 15 | vuejsStorage.install = install 16 | vuejsStorage.drivers = drivers 17 | 18 | export default vuejsStorage 19 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | // a simple object merge function implementation 2 | export const isobj = x => typeof x === 'object' && !Array.isArray(x) && x !== null 3 | const merge = (target, source) => { 4 | for (const key of Object.keys(source)) { 5 | if (!isobj(source[key])) { 6 | target[key] = source[key] 7 | continue 8 | } 9 | 10 | if (!(key in target)) { 11 | target[key] = source[key] 12 | continue 13 | } 14 | 15 | const targetValue = (typeof target[key] === 'undefined' || target[key] === null) ? {} : target[key] 16 | merge(targetValue, source[key]) 17 | } 18 | return target 19 | } 20 | export default merge 21 | -------------------------------------------------------------------------------- /src/drivers.ts: -------------------------------------------------------------------------------- 1 | import { StorageDriver } from './interfaces' 2 | export default class StroageDriverImpl implements StorageDriver { 3 | private storage 4 | constructor(storage: Storage) { 5 | this.storage = storage 6 | } 7 | set(key: string, value: any) { 8 | this.storage.setItem(key, JSON.stringify(value)) 9 | } 10 | get(key: string) { 11 | return JSON.parse(this.storage.getItem(key)) 12 | } 13 | has(key: string) { 14 | return !!this.storage.getItem(key) 15 | } 16 | } 17 | export const localStorage = new StroageDriverImpl(window.localStorage) 18 | export const sessionStorage = new StroageDriverImpl(window.sessionStorage) 19 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export { Vue, VueConstructor } from 'vue/types/vue' 2 | export { Plugin as VuexPlugin, Store } from 'vuex/types/index' 3 | export interface StorageDriver { 4 | set: (key: string, value: any) => void 5 | get: (key: string) => any 6 | has: (key: string) => boolean 7 | } 8 | export interface Option { 9 | keys: string[] 10 | namespace: string 11 | merge?: (obj1: object, ...object) => object //default=internal merge function 12 | driver?: StorageDriver //default=localStorageDriver 13 | } 14 | export type StorageOption = Option | Option[] 15 | export type StorageOptionWithFactory = StorageOption | (() => StorageOption) 16 | -------------------------------------------------------------------------------- /src/objpath.ts: -------------------------------------------------------------------------------- 1 | export function parsePath(path: string): string[] { 2 | return path 3 | .replace(/\[([^[\]]*)\]/g, '.$1.') 4 | .split('.') 5 | .filter(t => t !== '') 6 | } 7 | export function get(obj: object, path: string): any { 8 | return parsePath(path).reduce((prev, cur) => prev && prev[cur], obj) 9 | } 10 | export function set(obj: object, path: string, value: any): void { 11 | const paths = parsePath(path) 12 | let cur = obj 13 | for (let i = 0; i < paths.length - 1; i++) { 14 | const pname = paths[i] 15 | if (!cur.hasOwnProperty(pname)) cur[pname] = {} 16 | cur = cur[pname] 17 | } 18 | cur[paths[paths.length - 1]] = value 19 | } 20 | export function copy(dest: object, source: object, path: string): void { 21 | set(dest, path, get(source, path)) 22 | } 23 | -------------------------------------------------------------------------------- /src/vuexplugin.ts: -------------------------------------------------------------------------------- 1 | import { Store, VuexPlugin, Option } from './interfaces' 2 | import { copy } from './objpath' 3 | import { localStorage } from './drivers' 4 | import defaultMerge from './merge' 5 | 6 | /** 7 | * Create Vuex plugin 8 | */ 9 | export function createVuexPlugin(option: Option): VuexPlugin { 10 | const { keys, merge = defaultMerge, namespace: ns, driver = localStorage } = option 11 | return (store: Store) => { 12 | if (driver.has(ns)) { 13 | const data = driver.get(ns) 14 | store.replaceState(merge(store.state, data)) 15 | } else { 16 | const data = {} 17 | for (const k of keys) { 18 | copy(data, store.state, k) 19 | } 20 | driver.set(ns, data) 21 | } 22 | store.subscribe((mutation, state) => { 23 | const data = {} 24 | for (const k of keys) { 25 | copy(data, state, k) 26 | } 27 | driver.set(ns, data) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/objpath.ts: -------------------------------------------------------------------------------- 1 | import { get, set, copy, parsePath } from '../src/objpath' 2 | 3 | describe('objpath', () => { 4 | const obj = { a: 1, b: { e: { f: 87 } }, c: [{ d: 1 }] } 5 | it('parsePath', () => { 6 | parsePath('a.b[123].casd.test').should.deep.equal(['a', 'b', '123', 'casd', 'test']) 7 | }) 8 | it('get', () => { 9 | get(obj, 'b.e.f').should.equal(87) 10 | }) 11 | it('set', () => { 12 | set(obj, 'q.r.s.t', 5) 13 | get(obj, 'q.r.s.t').should.equal(5) 14 | }) 15 | it('get_array', () => { 16 | get(obj, 'c[0].d').should.equal(1) 17 | }) 18 | it('set_array', () => { 19 | set(obj, 'c[0].d', 63) 20 | get(obj, 'c[0].d').should.equal(63) 21 | }) 22 | it('final', () => { 23 | obj.should.deep.equal({ a: 1, b: { e: { f: 87 } }, c: [{ d: 63 }], q: { r: { s: { t: 5 } } } }) 24 | }) 25 | it('copy', () => { 26 | const o = { a: { b: { c: 5 } } } 27 | const p: any = {} 28 | copy(p, o, 'a.b.c') 29 | p.a.b.c.should.deep.equal(5) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 maple3142 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Editor directories and files 61 | .idea 62 | 63 | dist 64 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | const cfgs = { 3 | basePath: '', 4 | frameworks: ['mocha', 'chai', 'karma-typescript'], 5 | files: ['test/**/*.ts', 'src/**/*.ts'], 6 | exclude: [], 7 | preprocessors: { 8 | '**/*.ts': ['karma-typescript'] 9 | }, 10 | karmaTypescriptConfig: { 11 | bundlerOptions: { 12 | entrypoints: /test.*\.ts$/, 13 | exclude: ['vue/types/vue', 'vuex/types/index'] 14 | }, 15 | compilerOptions: { 16 | module: 'commonjs' 17 | }, 18 | tsconfig: './tsconfig.json', 19 | coverageOptions: { 20 | exclude: [/interfaces\.ts$/, /test/] 21 | } 22 | }, 23 | coverageReporter: { 24 | reporters: [{ type: 'lcovonly', subdir: '.' }, { type: 'json', subdir: '.' }] 25 | }, 26 | reporters: ['progress', 'coverage', 'karma-typescript'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ['ChromeHeadless'], 32 | singleRun: false, 33 | concurrency: Infinity, 34 | customLaunchers: { 35 | Chrome_travis_ci: { 36 | base: 'Chrome', 37 | flags: ['--no-sandbox'] 38 | } 39 | } 40 | } 41 | if (process.env.TRAVIS) { 42 | cfgs.browsers = ['Chrome_travis_ci'] 43 | } 44 | config.set(cfgs) 45 | } 46 | -------------------------------------------------------------------------------- /test/merge.ts: -------------------------------------------------------------------------------- 1 | import merge, { isobj } from '../src/merge' 2 | 3 | class Test {} 4 | ;(Test.prototype).value = 123 5 | const obj1 = { a: 5, c: 7 } 6 | const obj2 = new Test() 7 | Object.defineProperty(obj1, '__proto__', { 8 | enumerable: false, 9 | value: { x: 456 } 10 | }) 11 | const obj4 = { a: { b: 7 } } 12 | const obj5 = { a: { c: 6 }, x: { test: 8 } } 13 | 14 | const objass = (Object).assign 15 | const clone = obj => objass({}, obj) 16 | 17 | describe('merge', () => { 18 | it('isobj', () => { 19 | isobj({}).should.be.true 20 | isobj('').should.be.false 21 | isobj([]).should.be.false 22 | isobj(null).should.be.false 23 | isobj(undefined).should.be.false 24 | }) 25 | it('2 value', () => { 26 | const expected = objass({}, obj1, obj2) 27 | const result = merge(clone(obj1), obj2) 28 | result.should.deep.equal(expected) 29 | }) 30 | it('deep', () => { 31 | const expected = { a: { b: 7, c: 6 }, x: { test: 8 } } 32 | const result = merge(clone(obj4), obj5) 33 | result.should.deep.equal(expected) 34 | }) 35 | it('do not merge array', () => { 36 | merge({ arr: [{ a: 1, b: 2 }, { a: 3, b: 4 }], b: 4 }, { c: 5 }).should.deep.equal({ 37 | arr: [{ a: 1, b: 2 }, { a: 3, b: 4 }], 38 | b: 4, 39 | c: 5 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-storage", 3 | "description": "Vue.js and Vuex plugin to persistence data with localStorage/sessionStorage", 4 | "version": "3.1.1", 5 | "main": "./dist/vuejs-storage.cjs.js", 6 | "module": "./dist/vuejs-storage.es.js", 7 | "browser": "./dist/vuejs-storage.umd.js", 8 | "types": "./types/vuejs-storage.d.ts", 9 | "license": "MIT", 10 | "keywords": [ 11 | "vue", 12 | "vuex", 13 | "localStorage", 14 | "sessionStorage", 15 | "persistence" 16 | ], 17 | "devDependencies": { 18 | "@types/chai": "^4.2.12", 19 | "@types/mocha": "^8.0.3", 20 | "@types/object-assign": "^4.0.30", 21 | "bili": "^5.0.5", 22 | "chai": "^4.1.2", 23 | "chromium": "^3.0.1", 24 | "karma": "^5.1.1", 25 | "karma-chai": "^0.1.0", 26 | "karma-chrome-launcher": "^3.1.0", 27 | "karma-coverage": "^2.0.3", 28 | "karma-mocha": "^2.0.1", 29 | "karma-typescript": "^5.5.3", 30 | "lodash.assignin": "^4.2.0", 31 | "mocha": "^8.1.1", 32 | "rollup-plugin-typescript2": "^0.27.2", 33 | "tslib": "^2.0.1", 34 | "typescript": "^4.0.2", 35 | "vue": "^2.5.21", 36 | "vuex": "^3.0.1" 37 | }, 38 | "scripts": { 39 | "build": "bili", 40 | "test": "node run-test.js", 41 | "prepublishOnly": "bili" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/maple3142/vuejs-storage.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/maple3142/vuejs-storage/issues" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Vue, 3 | VueConstructor, 4 | Option, 5 | StorageOptionWithFactory, 6 | StorageOption, 7 | } from './interfaces' 8 | import { set, copy } from './objpath' 9 | import { localStorage } from './drivers' 10 | import defaultMerge from './merge' 11 | 12 | function applyPersistence(vm, option: Option) { 13 | const { 14 | keys, 15 | merge = defaultMerge, 16 | namespace: ns, 17 | driver = localStorage, 18 | } = option 19 | 20 | let originaldata = {} 21 | for (const k of keys) { 22 | copy(originaldata, vm, k) 23 | } 24 | 25 | let data = null 26 | if (driver.has(ns)) { 27 | data = driver.get(ns) 28 | } else { 29 | const tmp = {} 30 | for (const k of keys) { 31 | copy(tmp, originaldata, k) 32 | } 33 | data = tmp 34 | driver.set(ns, data) 35 | } 36 | data = merge(originaldata, data) 37 | for (const k of keys) { 38 | copy(vm, data, k) 39 | vm.$watch(k, { 40 | handler: (value) => { 41 | set(data, k, value) 42 | driver.set(ns, data) 43 | }, 44 | deep: true, 45 | }) 46 | } 47 | } 48 | 49 | export function install(Vue: VueConstructor) { 50 | Vue.mixin({ 51 | created() { 52 | if ('storage' in this.$options) { 53 | let option: StorageOptionWithFactory = this.$options.storage 54 | if (typeof option === 'function') { 55 | option = option.apply(this) as StorageOption 56 | } 57 | if (Array.isArray(option)) 58 | option.forEach((opt) => applyPersistence(this, opt)) 59 | else applyPersistence(this, option) 60 | } 61 | }, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | example 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | Vue counter: {{count}} 18 | 19 |
20 |
21 | Vuex counter: {{vuexcount}} 22 | 23 |
24 |
25 |
26 |
27 | sessionStorage counter: {{count}} {{message}} 28 | 29 |
30 |
31 |
32 | Try open this page in another tab. 33 |
34 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /test/install.ts: -------------------------------------------------------------------------------- 1 | import * as plugin from '../src/install' 2 | import { sessionStorage } from '../src/drivers' 3 | import Vue from 'vue' 4 | 5 | Vue.config.productionTip = false 6 | Vue.config.devtools = false 7 | Vue.use(plugin) 8 | 9 | let vm: any 10 | describe('plugin', () => { 11 | before(() => { 12 | window.localStorage.clear() 13 | }) 14 | it('data should be store as json in localStorage', () => { 15 | vm = new Vue({ 16 | data: { 17 | a: 1, 18 | b: 2, 19 | }, 20 | storage: { 21 | namespace: 'vue1', 22 | keys: ['a'], 23 | }, 24 | }) 25 | vm.a.should.equal(1) 26 | JSON.parse(window.localStorage.getItem('vue1')).should.deep.equal({ 27 | a: 1, 28 | }) 29 | }) 30 | it('data can be change', (done) => { 31 | vm.a = 2 32 | vm.a.should.equal(2) 33 | vm.$nextTick(() => { 34 | JSON.parse(window.localStorage.getItem('vue1')).should.deep.equal({ 35 | a: 2, 36 | }) 37 | done() 38 | }) 39 | }) 40 | it('data will be load from localStorage', () => { 41 | vm.$destroy() 42 | vm = new Vue({ 43 | data: { 44 | a: 1, 45 | b: 2, 46 | }, 47 | storage: { 48 | namespace: 'vue1', 49 | keys: ['a'], 50 | }, 51 | }) 52 | vm.a.should.equal(2) 53 | }) 54 | it('can handle nested key', () => { 55 | vm.$destroy() 56 | vm = new Vue({ 57 | data: { 58 | a: { b: { c: 5 } }, 59 | d: 123, 60 | }, 61 | storage: { 62 | namespace: 'vue2', 63 | keys: ['a.b.c'], 64 | }, 65 | }) 66 | JSON.parse(window.localStorage.getItem('vue2')).should.deep.equal({ 67 | a: { b: { c: 5 } }, 68 | }) 69 | }) 70 | it('nested key can be change', (done) => { 71 | vm.a.b.c = 8 72 | vm.$nextTick(() => { 73 | JSON.parse(window.localStorage.getItem('vue2')).should.deep.equal({ 74 | a: { b: { c: 8 } }, 75 | }) 76 | done() 77 | }) 78 | }) 79 | it('can handle object', (done) => { 80 | vm.$destroy() 81 | window.localStorage.setItem( 82 | 'vue3', 83 | JSON.stringify({ a: { b: { c: 4 } } }) 84 | ) 85 | vm = new Vue({ 86 | data: { 87 | a: { b: { c: 5 } }, 88 | }, 89 | storage: { 90 | namespace: 'vue3', 91 | keys: ['a'], 92 | }, 93 | }) 94 | vm.$nextTick(() => { 95 | JSON.parse(window.localStorage.getItem('vue3')).should.deep.equal({ 96 | a: { b: { c: 4 } }, 97 | }) 98 | done() 99 | }) 100 | }) 101 | it('merge fn works', () => { 102 | vm.$destroy() 103 | vm = new Vue({ 104 | data: { 105 | a: { b: { c: 5 } }, 106 | }, 107 | storage: { 108 | namespace: 'vue3', //merge fn only called if key exists 109 | keys: ['a'], 110 | merge: () => ({ 111 | a: 123, 112 | }), 113 | }, 114 | }) 115 | vm.a.should.equal(123) 116 | }) 117 | it('multiple storage', () => { 118 | vm.$destroy() 119 | vm = new Vue({ 120 | data: { 121 | a: 1, 122 | b: 2, 123 | }, 124 | storage: [ 125 | { 126 | namespace: 'vue4', 127 | keys: ['a'], 128 | }, 129 | { 130 | namespace: 'vue4', 131 | keys: ['b'], 132 | driver: sessionStorage, 133 | }, 134 | ], 135 | }) 136 | window.localStorage 137 | .getItem('vue4') 138 | .should.equal(JSON.stringify({ a: 1 })) 139 | window.sessionStorage 140 | .getItem('vue4') 141 | .should.equal(JSON.stringify({ b: 2 })) 142 | }) 143 | it("other state shouldn't be change", () => { 144 | vm.$destroy() 145 | vm = new Vue({ 146 | data: { 147 | a: 1, 148 | b: 2, 149 | }, 150 | storage: { 151 | namespace: 'vue5', 152 | keys: ['a'], 153 | }, 154 | }) 155 | window.localStorage 156 | .getItem('vue5') 157 | .should.equal(JSON.stringify({ a: 1 })) 158 | vm.a.should.equal(1) 159 | vm.b.should.equal(2) 160 | }) 161 | it('can use factory function as storage and this is accessible', () => { 162 | const rand = Math.random().toString() 163 | vm = new Vue({ 164 | data: { 165 | a: 1, 166 | b: 2, 167 | rand, 168 | }, 169 | storage() { 170 | return { namespace: this.rand, keys: ['a'] } 171 | }, 172 | }) 173 | JSON.parse(window.localStorage.getItem(rand)).should.deep.equal({ 174 | a: 1, 175 | }) 176 | }) 177 | }) 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuejs-storage 2 | 3 | > Vue.js and Vuex plugin to persistence data with localStorage/sessionStorage 4 | 5 | [![npm](https://img.shields.io/npm/v/vuejs-storage.svg?style=flat-square)](https://www.npmjs.com/package/vuejs-storage) 6 | [![npm](https://img.shields.io/npm/dm/vuejs-storage?style=flat-square)](https://www.npmjs.com/package/vuejs-storage) 7 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/maple3142/vuejs-storage/Node.js%20CI?style=flat-square)](https://github.com/maple3142/vuejs-storage/actions?query=workflow%3A%22Node.js+CI%22) 8 | [![codecov](https://img.shields.io/codecov/c/github/maple3142/vuejs-storage.svg?style=flat-square)](https://codecov.io/gh/maple3142/vuejs-storage) 9 | 10 | 11 | ## Purpose 12 | 13 | This plugin provide a simple binding with `localStorage` and `sessionStorage` (or something similar) to **Vue** and **Vuex**. 14 | 15 | It has **[no dependencies](https://www.npmjs.com/package/vuejs-storage?activeTab=dependencies)**, so it is really small. 16 | 17 | * `.js` size: 5.75KB (1.7KB gzipped) 18 | * `.min.js` size: 2.21KB (1.1KB gzipped) 19 | 20 | ## Usage 21 | 22 | ```js 23 | //in webpack environment: 24 | import vuejsStorage from 'vuejs-storage' 25 | //in browser script tag: 26 | const vuejsStorage = window.vuejsStorage 27 | 28 | Vue.use(vuejsStorage) 29 | 30 | //vue example 31 | new Vue({ 32 | //... 33 | data: { 34 | count: 0, 35 | text: '' 36 | }, 37 | storage: { 38 | keys: ['count'], 39 | //keep data.count in localStorage 40 | namespace: 'my-namespace' 41 | } 42 | }) 43 | 44 | //vuex example 45 | const store = new Vuex.Store({ 46 | //state... 47 | state: { 48 | count: 0 49 | }, 50 | mutations: { 51 | increment(state) { 52 | state.count++ 53 | } 54 | }, 55 | plugins: [ 56 | vuejsStorage({ 57 | keys: ['count'], 58 | //keep state.count in localStorage 59 | namespace: 'my-namespace', 60 | driver: vuejsStorage.drivers.sessionStorage 61 | //if you want to use sessionStorage instead of localStorage 62 | }) 63 | ] 64 | }) 65 | ``` 66 | 67 | ## Nested key 68 | 69 | ```javascript 70 | data: { 71 | a: { 72 | b: 1, 73 | c: 2 74 | } 75 | }, 76 | storage: { 77 | namespace: 'test', 78 | keys: ['a.b'] 79 | //only keep a.b in localStorage 80 | } 81 | ``` 82 | 83 | ## Vuex modules 84 | 85 | ```javascript 86 | state: { 87 | a: 1 88 | }, 89 | modules: { 90 | moduleA: { 91 | state: { 92 | a: 2 93 | } 94 | } 95 | }, 96 | plugins: [ 97 | vuejsStorage({ 98 | namespace: 'test', 99 | keys: ['moduleA','a'] 100 | // keep both root's state.a & moduleA's state 101 | }) 102 | ] 103 | ``` 104 | 105 | ## Multiple storage 106 | 107 | ```javascript 108 | data: { 109 | a: 1, 110 | b: 2 111 | }, 112 | storage: [ 113 | { 114 | namespace: 'test', 115 | keys: ['a'] 116 | }, 117 | { 118 | namespace: 'test', 119 | keys: ['b'], 120 | driver: vuejsStorage.drivers.sessionStorage 121 | } 122 | ] 123 | ``` 124 | 125 | ## API 126 | 127 | ### `vuejsStorage` 128 | 129 | **Vue** plugin 130 | 131 | ```javascript 132 | Vue.use(vuejsStorage) 133 | ``` 134 | 135 | ### `vuejsStorage(option)` 136 | 137 | Create a **Vuex** plugin 138 | 139 | ```javascript 140 | const vuexplugin = vuejsStorage(/* option object*/) 141 | ``` 142 | 143 | ### `option` 144 | 145 | Option object, can be used when create **Vuex** plugin or in **Vue** option `storage` field 146 | 147 | ```javascript 148 | { 149 | keys: [], //array of string 150 | /* 151 | this option is different when use in vue and vuex 152 | when used in Vue constructor option, keys means which data should be keep in localStorage 153 | when used in Vuex plugin, keys mean which state should be keep in localStorage 154 | */ 155 | driver: vuejsStorage.drivers.sessionStorage, //any object has 'set','get','has' api, default: vuejsStorage.drivers.localStorage 156 | namespace: 'ns', //a string, REQUIRED 157 | merge: _assign //a function to merge object like Object.assign, default: internal implementation(src/assign.ts) 158 | } 159 | ``` 160 | 161 | ## Examples 162 | 163 | * [Counter](https://rawgit.com/maple3142/vuejs-storage/master/example.html) 164 | * [maple3142/TodoList](https://github.com/maple3142/TodoList) 165 | -------------------------------------------------------------------------------- /test/vuexplugin.ts: -------------------------------------------------------------------------------- 1 | import { createVuexPlugin } from '../src/vuexplugin' 2 | import { sessionStorage } from '../src/drivers' 3 | import Vue from 'vue/dist/vue.runtime.min.js' 4 | import Vuex from 'vuex' 5 | 6 | Vue.use(Vuex) 7 | 8 | describe('vuexplugin', () => { 9 | it('first', () => { 10 | const store = new Vuex.Store({ 11 | state: { 12 | count: 1 13 | }, 14 | mutations: { 15 | inc: (state: any) => state.count++ 16 | }, 17 | plugins: [ 18 | createVuexPlugin({ 19 | namespace: 'vuextest1', 20 | keys: ['count'] 21 | }) 22 | ] 23 | }) 24 | store.state.count.should.equal(1) 25 | store.commit('inc') 26 | JSON.parse(window.localStorage.getItem('vuextest1')).should.deep.equal({ 27 | count: 2 28 | }) 29 | }) 30 | it('second', () => { 31 | const store = new Vuex.Store({ 32 | state: { 33 | count: 1 34 | }, 35 | mutations: { 36 | inc: (state: any) => state.count++ 37 | }, 38 | plugins: [ 39 | createVuexPlugin({ 40 | namespace: 'vuextest1', 41 | keys: ['count'] 42 | }) 43 | ] 44 | }) 45 | store.state.count.should.equal(2) 46 | }) 47 | it('can handle nested key', () => { 48 | const store = new Vuex.Store({ 49 | state: { 50 | a: { b: { c: 5 } }, 51 | d: 123 52 | }, 53 | plugins: [ 54 | createVuexPlugin({ 55 | namespace: 'vuextest2', 56 | keys: ['a.b.c'] 57 | }) 58 | ] 59 | }) 60 | JSON.parse(window.localStorage.getItem('vuextest2')).should.deep.equal({ 61 | a: { b: { c: 5 } } 62 | }) 63 | }) 64 | it('merge fn works', () => { 65 | const store = new Vuex.Store({ 66 | state: { 67 | a: { b: { c: 5 } }, 68 | d: 123 69 | }, 70 | plugins: [ 71 | createVuexPlugin({ 72 | namespace: 'vuextest2', 73 | keys: ['a'], 74 | merge: () => ({ a: 3 }) 75 | }) 76 | ] 77 | }) 78 | store.state.should.deep.equal({ a: 3 }) 79 | }) 80 | it('modules', () => { 81 | const store = new Vuex.Store({ 82 | state: { 83 | a: 1 84 | }, 85 | modules: { 86 | moduleA: { 87 | state: { 88 | a: 7 89 | } 90 | } 91 | }, 92 | plugins: [ 93 | createVuexPlugin({ 94 | namespace: 'vuextest3', 95 | keys: ['a', 'moduleA'] 96 | }) 97 | ] 98 | }) 99 | JSON.parse(window.localStorage.getItem('vuextest3')).should.deep.equal({ 100 | a: 1, 101 | moduleA: { a: 7 } 102 | }) 103 | }) 104 | it("other state shouldn't be change", () => { 105 | const store = new Vuex.Store({ 106 | state: { 107 | a: 1, 108 | b: 2 109 | }, 110 | modules: { 111 | moduleA: { 112 | state: { 113 | a: 1, 114 | b: 2 115 | } 116 | } 117 | }, 118 | plugins: [ 119 | createVuexPlugin({ 120 | namespace: 'vuextest4', 121 | keys: ['a', 'moduleA.a'] 122 | }) 123 | ] 124 | }) 125 | JSON.parse(window.localStorage.getItem('vuextest4')).should.deep.equal({ 126 | a: 1, 127 | moduleA: { a: 1 } 128 | }) 129 | store.state.should.deep.equal({ a: 1, b: 2, moduleA: { a: 1, b: 2 } }) 130 | }) 131 | it('null or undefined state', () => { 132 | const store = new Vuex.Store({ 133 | state: { 134 | nullVar: null, 135 | undefinedVar: undefined 136 | }, 137 | mutations: { 138 | inc: (state: any) => { 139 | state.nullVar = 'not null' 140 | state.undefinedVar = 'not undefined' 141 | } 142 | }, 143 | plugins: [ 144 | createVuexPlugin({ 145 | namespace: 'vuextest1', 146 | keys: ['nullVar', 'undefinedVar'] 147 | }) 148 | ] 149 | }) 150 | 151 | const nullVarIsNull = (store.state.nullVar === null) 152 | const undefinedVarIsUndefined = (store.state.undefinedVar === undefined) 153 | nullVarIsNull.should.equal(true) 154 | undefinedVarIsUndefined.should.equal(true) 155 | store.commit('inc') 156 | JSON.parse(window.localStorage.getItem('vuextest1')).should.deep.equal({ 157 | nullVar: 'not null', 158 | undefinedVar: 'not undefined' 159 | }) 160 | }) 161 | it('complex #1', () => { 162 | const store = new Vuex.Store({ 163 | state: { 164 | ar: [{ value: 1 }, { value: 2 }], 165 | data: { 166 | data: { 167 | id: 123, 168 | value: 456 169 | }, 170 | savedData: { 171 | id: 789, 172 | value: 101112, 173 | ar: [4, 5, 6, 7] 174 | } 175 | } 176 | }, 177 | modules: { 178 | test: { 179 | state: { 180 | ar: [{ value: 1 }, { value: 2 }], 181 | wontBeSave: 1 182 | } 183 | } 184 | }, 185 | plugins: [ 186 | createVuexPlugin({ 187 | namespace: 'vuextest5', 188 | keys: ['ar', 'test.ar'] 189 | }), 190 | createVuexPlugin({ 191 | namespace: 'vuextest5', 192 | driver: sessionStorage, 193 | keys: ['data.savedData'] 194 | }) 195 | ] 196 | }) 197 | JSON.parse(window.localStorage.getItem('vuextest5')).should.deep.equal({ 198 | ar: [{ value: 1 }, { value: 2 }], 199 | test: { 200 | ar: [{ value: 1 }, { value: 2 }] 201 | } 202 | }) 203 | JSON.parse(window.sessionStorage.getItem('vuextest5')).should.deep.equal({ 204 | data: { 205 | savedData: { 206 | id: 789, 207 | value: 101112, 208 | ar: [4, 5, 6, 7] 209 | } 210 | } 211 | }) 212 | }) 213 | }) 214 | --------------------------------------------------------------------------------