├── .codecov.yml ├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── demo.gif └── workflows │ └── tests.yaml ├── .gitignore ├── .mocharc.json ├── .prettierrc.json ├── LICENSE ├── README.md ├── examples ├── basic │ ├── .browserslistrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.vue │ │ ├── assets │ │ └── logo.png │ │ ├── main.js │ │ └── store │ │ └── index.js └── nuxt │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── assets │ └── README.md │ ├── components │ ├── Logo.vue │ └── README.md │ ├── layouts │ ├── README.md │ └── default.vue │ ├── nuxt.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ ├── README.md │ └── index.vue │ ├── plugins │ └── multiTabState.client.js │ ├── static │ ├── README.md │ └── favicon.ico │ └── store │ ├── animals.js │ ├── fruits.js │ └── index.js ├── package-lock.json ├── package.json ├── src ├── index.ts └── tab.ts ├── test └── test.spec.ts └── tsconfig.json /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | status: 9 | project: 10 | default: 11 | target: 90% 12 | threshold: 5% 13 | base: auto 14 | flags: null 15 | paths: null 16 | branches: null 17 | if_no_uploads: error 18 | if_not_found: success 19 | if_ci_failed: error 20 | patch: no 21 | changes: no 22 | 23 | parsers: 24 | gcov: 25 | branch_detection: 26 | conditional: yes 27 | loop: yes 28 | method: no 29 | macro: no 30 | 31 | comment: 32 | layout: "reach,diff,flags,tree" 33 | behavior: default 34 | require_changes: no 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "mocha": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "plugins": ["@typescript-eslint", "prettier"], 17 | "rules": { 18 | "prettier/prettier": ["error"], 19 | "import/extensions": [ 20 | "error", 21 | "ignorePackages", 22 | { 23 | "js": "never", 24 | "ts": "never" 25 | } 26 | ], 27 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 28 | "no-param-reassign": [ 29 | "error", 30 | { 31 | "ignorePropertyModificationsFor": [ 32 | "state" 33 | ] 34 | } 35 | ] 36 | }, 37 | "settings": { 38 | "import/resolver": { 39 | "node": { 40 | "extensions": [".js", ".ts"] 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 7 | 8 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to a positive environment for our community include: 13 | 14 | * Demonstrating empathy and kindness toward other people 15 | * Being respectful of differing opinions, viewpoints, and experiences 16 | * Giving and gracefully accepting constructive feedback 17 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 18 | * Focusing on what is best not just for us as individuals, but for the overall community 19 | 20 | Examples of unacceptable behavior include: 21 | 22 | * The use of sexualized language or imagery, and sexual attention or 23 | advances of any kind 24 | * Trolling, insulting or derogatory comments, and personal or political attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or email 27 | address, without their explicit permission 28 | * Other conduct which could reasonably be considered inappropriate in a 29 | professional setting 30 | 31 | ## Enforcement Responsibilities 32 | 33 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 34 | 35 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 40 | 41 | ## Enforcement 42 | 43 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 44 | 45 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 46 | 47 | ## Enforcement Guidelines 48 | 49 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 50 | 51 | ### 1. Correction 52 | 53 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 54 | 55 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 56 | 57 | ### 2. Warning 58 | 59 | **Community Impact**: A violation through a single incident or series of actions. 60 | 61 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 62 | 63 | ### 3. Temporary Ban 64 | 65 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 66 | 67 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 68 | 69 | ### 4. Permanent Ban 70 | 71 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 72 | 73 | **Consequence**: A permanent ban from any sort of public interaction within the community. 74 | 75 | ## Attribution 76 | 77 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 78 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 79 | 80 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 81 | 82 | [homepage]: https://www.contributor-covenant.org 83 | 84 | For answers to common questions about this code of conduct, see the FAQ at 85 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 86 | 87 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gabrielmbmb 2 | custom: ["https://www.buymeacoffee.com/gabrielmbmb"] 3 | -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielmbmb/vuex-multi-tab-state/76a061834334dc969d856ea4d9939c0a9ac0d52e/.github/demo.gif -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | run-tests: 5 | runs-on: ubuntu-latest 6 | 7 | strategy: 8 | matrix: 9 | node-version: [10.x, 12.x, 14.x, 15.x] 10 | 11 | steps: 12 | # Checkout the repo 13 | - uses: actions/checkout@v2 14 | 15 | # Setup Node 16 | - name: Setup Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Run tests 25 | run: npm test 26 | 27 | - name: Run coverage 28 | run: npm run coverage 29 | if: matrix.os == 'ubuntu-latest' && matrix.node-version == '10.x' 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v1 33 | if: matrix.os == 'ubuntu-latest' && matrix.node-version == '10.x' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Build 107 | lib/ 108 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "test/**/*.spec.ts", 4 | "require": ["ts-node/register", "jsdom-global/register", "mock-local-storage"] 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "useTabs": false, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gabriel Martín Blázquez 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuex-multi-tab-state 2 | 3 | ![Tests](https://github.com/gabrielmbmb/vuex-multi-tab-state/actions/workflows/tests.yaml/badge.svg) 4 | [![npm](https://img.shields.io/npm/v/vuex-multi-tab-state)](https://www.npmjs.com/package/vuex-multi-tab-state) 5 | [![codecov](https://codecov.io/gh/gabrielmbmb/vuex-multi-tab-state/branch/master/graph/badge.svg)](https://codecov.io/gh/gabrielmbmb/vuex-multi-tab-state) 6 | [![codebeat badge](https://codebeat.co/badges/9c5328f7-a70e-412a-a68a-ce67668bfc0d)](https://codebeat.co/projects/github-com-gabrielmbmb-vuex-multi-tab-state-master) 7 | [![npm](https://img.shields.io/npm/dm/vuex-multi-tab-state)](https://www.npmjs.com/package/vuex-multi-tab-state) 8 | [![npm bundle size](https://img.shields.io/bundlephobia/min/vuex-multi-tab-state)](https://www.npmjs.com/package/vuex-multi-tab-state) 9 | [![npm type definitions](https://img.shields.io/npm/types/vuex-multi-tab-state)](https://www.npmjs.com/package/vuex-multi-tab-state) 10 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 11 | ![demo](https://raw.githubusercontent.com/gabrielmbmb/vuex-multi-tab-state/master/.github/demo.gif?token=AHBT6NTORAZEFGKQRZ3IC4C6KHJA6) 12 | 13 | This Vuex plugin allows you to sync and share the status of your Vue application 14 | across multiple tabs or windows using the local storage. 15 | 16 | **This repository has a gitter chat where you can ask questions and propose new features:** 17 | 18 | [![Gitter](https://badges.gitter.im/vuex-multi-tab-state/community.svg)](https://gitter.im/vuex-multi-tab-state/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 19 | 20 | ### Vue 3 and Vuex 4 compatibility :warning: 21 | 22 | The plugin has been tested with Vue 3 and Vuex 4 and no problems have been found. 23 | There is an [issue](https://github.com/gabrielmbmb/vuex-multi-tab-state/issues/36) 24 | where you can find how the plugin has been used using the new Vue 3 Composition 25 | API. If you encounter a problem using the plugin with Vue 3 and Vuex 4, please 26 | post it there. 27 | 28 | ## Installation 29 | 30 | vuex-multi-tab-state is available in npm and can be installed with the following command: 31 | 32 | npm i vuex-multi-tab-state 33 | 34 | ## Usage 35 | 36 | Just import vuex-multi-tab-state and add it in the plugins list of your Vuex Store object. 37 | 38 | ```javascript 39 | import Vue from 'vue'; 40 | import Vuex from 'vuex'; 41 | import createMultiTabState from 'vuex-multi-tab-state'; 42 | 43 | Vue.use(Vuex); 44 | 45 | export default new Vuex.Store({ 46 | state: { ... }, 47 | mutations: { ... }, 48 | actions: { ... }, 49 | getters: { ... }, 50 | plugins: [ 51 | createMultiTabState(), 52 | ], 53 | }); 54 | ``` 55 | 56 | You can check the example provided [here](https://github.com/gabrielmbmb/vuex-multi-tab-state/tree/master/examples/basic) 57 | 58 | ## NuxtJS 59 | 60 | Integrating the plugin in NuxtJS requires a little more effort than in Vue. First 61 | of all, we have to create a file inside the `plugins` directory. 62 | 63 | ```javascript 64 | // ~/plugins/multiTabState.client.js 65 | import createMultiTabState from 'vuex-multi-tab-state'; 66 | 67 | export default ({ store }) => { 68 | createMultiTabState()(store); 69 | }; 70 | ``` 71 | 72 | Note that the filename must have the following format `*.client.js`. The next 73 | step is to add this plugin to NuxtJS in `nuxt.config.js`: 74 | 75 | ```javascript 76 | // nuxt.config.js 77 | export default { 78 | ... 79 | plugins: [{ src: '~/plugins/multiTabState.client.js' }], 80 | ... 81 | } 82 | ``` 83 | 84 | If you didn't name the file according to the specified format, you can add the 85 | plugin this way: 86 | 87 | ```javascript 88 | // nuxt.config.js 89 | export default { 90 | ... 91 | plugins: [{ src: '~/plugins/multiTabState.client.js', mode: 'client' }], 92 | ... 93 | } 94 | ``` 95 | 96 | Both ways tell NuxtJS that the plugin should only be run client-side 97 | (because the plugin uses `window`, not available server-side). 98 | 99 | ## API 100 | 101 | ### `createMultiTabState({options})` 102 | 103 | Creates a new instance of the plugin with the given options. The possible options 104 | are as follows: 105 | 106 | - `statesPaths Array`: contains the name of the states to be synchronized 107 | with dot notation. If the param is not provided, the whole state of your app will 108 | be sync. Defaults to `[]`. 109 | 110 | > Example: Only the oranges will be synchronized. 111 | 112 | ```javascript 113 | export default new Vuex.Store({ 114 | state: { 115 | fruits: { 116 | oranges: 0, 117 | apples: 0, 118 | }, 119 | }, 120 | plugins: [createMultiTabState({ 121 | statesPaths: ['fruits.oranges'], 122 | })], 123 | }); 124 | ``` 125 | 126 | - `key? `: key of the local storage in which the state will be stored. 127 | Defaults to `'vuex-multi-tab'`. 128 | - `onBeforeReplace? `: hook function that receives the state and allows to modify it before replacing it. The function can return either a modified state or a `falsy` value (which means that no modifications has been done inside the hook). 129 | - `onBeforeSave? `: hook function that receives the state and allows to modify it before saving it in local storage. The function can return either a modified state or a `falsy` value (which means that no modifications has been done inside the hook). 130 | 131 | ```javascript 132 | export default new Vuex.Store({ 133 | state: { 134 | fruits: { 135 | oranges: 0, 136 | apples: 0, 137 | }, 138 | }, 139 | plugins: [createMultiTabState({ 140 | statesPaths: ['fruits.oranges'], 141 | onBeforeSave: (state) => { 142 | // Modify state here 143 | return state; 144 | }, 145 | onBeforeReplace: (state) => { 146 | // Modify state here 147 | return state; 148 | } 149 | })], 150 | }); 151 | ``` 152 | 153 | ## Test 154 | 155 | The tests have been written with [mocha](https://github.com/mochajs/mocha) and [chai](https://github.com/chaijs/chai). 156 | 157 | npm install 158 | npm run test 159 | 160 | ## Collaborate 161 | 162 | ![npm collaborators](https://img.shields.io/npm/collaborators/vuex-multi-tab-state) 163 | 164 | If you feel that something can be improved, go on, create a pull request! Please 165 | follow the programming style and document your changes correctly. 166 | 167 | ## License 168 | 169 | [![NPM](https://img.shields.io/npm/l/vuex-multi-tab-state)](https://github.com/gabrielmbmb/vuex-multi-tab-state/blob/master/LICENSE) 170 | 171 | This project is under the MIT license. More information [here](https://github.com/gabrielmbmb/vuex-multi-tab-state/blob/master/LICENSE). 172 | -------------------------------------------------------------------------------- /examples/basic/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /examples/basic/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /examples/basic/.eslintignore: -------------------------------------------------------------------------------- 1 | ../../lib/* -------------------------------------------------------------------------------- /examples/basic/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb', 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint', 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic example of vuex-multi-tab-state 2 | 3 | Just a very simple example of vuex-multi-tab-state. -------------------------------------------------------------------------------- /examples/basic/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.4", 12 | "vue": "^2.6.12", 13 | "vuex": "^3.5.1", 14 | "vuex-multi-tab-state": "^1.0.15" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "^4.2.0", 18 | "@vue/cli-plugin-eslint": "^4.2.0", 19 | "@vue/cli-plugin-vuex": "^4.2.0", 20 | "@vue/cli-service": "^4.2.0", 21 | "@vue/eslint-config-airbnb": "^5.0.2", 22 | "babel-eslint": "^10.0.3", 23 | "eslint": "^6.7.2", 24 | "eslint-plugin-import": "^2.20.1", 25 | "eslint-plugin-vue": "^6.1.2", 26 | "vue-template-compiler": "^2.6.12" 27 | }, 28 | "files": [ 29 | "dist/**/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielmbmb/vuex-multi-tab-state/76a061834334dc969d856ea4d9939c0a9ac0d52e/examples/basic/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/basic/src/App.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 82 | -------------------------------------------------------------------------------- /examples/basic/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielmbmb/vuex-multi-tab-state/76a061834334dc969d856ea4d9939c0a9ac0d52e/examples/basic/src/assets/logo.png -------------------------------------------------------------------------------- /examples/basic/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import store from './store'; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | store, 9 | render: (h) => h(App), 10 | }).$mount('#app'); 11 | -------------------------------------------------------------------------------- /examples/basic/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import createMultiTabState from 'vuex-multi-tab-state'; 4 | 5 | Vue.use(Vuex); 6 | 7 | // Modules 8 | const fruits = { 9 | state: { 10 | oranges: 0, 11 | apples: 0, 12 | }, 13 | mutations: { 14 | addOrange(state) { 15 | state.oranges += 1; 16 | }, 17 | addApple(state) { 18 | state.apples += 1; 19 | }, 20 | }, 21 | actions: { 22 | commitAddOrange({ commit }) { 23 | commit('addOrange'); 24 | }, 25 | commitAddApple({ commit }) { 26 | commit('addApple'); 27 | }, 28 | }, 29 | getters: { 30 | getOranges(state) { 31 | return state.oranges; 32 | }, 33 | getApples(state) { 34 | return state.apples; 35 | }, 36 | }, 37 | }; 38 | 39 | const animals = { 40 | state: { 41 | penguins: 0, 42 | wolfs: 0, 43 | }, 44 | mutations: { 45 | addPenguin(state) { 46 | state.penguins += 1; 47 | }, 48 | addWolf(state) { 49 | state.wolfs += 1; 50 | }, 51 | }, 52 | actions: { 53 | commitAddPenguin({ commit }) { 54 | commit('addPenguin'); 55 | }, 56 | commitAddWolf({ commit }) { 57 | commit('addWolf'); 58 | }, 59 | }, 60 | getters: { 61 | getPenguins(state) { 62 | return state.penguins; 63 | }, 64 | getWolfs(state) { 65 | return state.wolfs; 66 | }, 67 | }, 68 | }; 69 | 70 | export default new Vuex.Store({ 71 | state: { 72 | counter: 0, 73 | dictionary: {}, 74 | }, 75 | mutations: { 76 | increment(state) { 77 | state.counter += 1; 78 | }, 79 | addKeyValue(state, { key, value }) { 80 | console.log(state); 81 | state.dictionary[key] = value; 82 | }, 83 | removeKey(state, key) { 84 | const { [key]: removedKey, ...rest } = state.dictionary; 85 | state.dictionary = rest; 86 | }, 87 | nullify(state) { 88 | state.dictionary = null; 89 | }, 90 | newDictionary(state) { 91 | state.dictionary = {}; 92 | }, 93 | }, 94 | modules: { fruits, animals }, 95 | plugins: [ 96 | createMultiTabState({ 97 | statesPaths: ['fruits.oranges', 'animals.penguins', 'counter', 'dictionary'], 98 | }), 99 | ], 100 | }); 101 | -------------------------------------------------------------------------------- /examples/nuxt/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /examples/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /examples/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # nuxt 2 | 3 | > Just a basic Nuxt app to show how to integrate vuex-multi-tab-state on its store 4 | 5 | ## Build Setup 6 | 7 | ```bash 8 | # install dependencies 9 | $ npm install 10 | 11 | # serve with hot reload at localhost:3000 12 | $ npm run dev 13 | 14 | # build for production and launch server 15 | $ npm run build 16 | $ npm run start 17 | 18 | # generate static project 19 | $ npm run generate 20 | ``` 21 | 22 | For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 23 | -------------------------------------------------------------------------------- /examples/nuxt/assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /examples/nuxt/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 20 | 35 | -------------------------------------------------------------------------------- /examples/nuxt/components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /examples/nuxt/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /examples/nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | -------------------------------------------------------------------------------- /examples/nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'universal', 3 | /* 4 | ** Headers of the page 5 | */ 6 | head: { 7 | title: process.env.npm_package_name || '', 8 | meta: [ 9 | { charset: 'utf-8' }, 10 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 11 | { 12 | hid: 'description', 13 | name: 'description', 14 | content: process.env.npm_package_description || '', 15 | }, 16 | ], 17 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 18 | }, 19 | /* 20 | ** Customize the progress-bar color 21 | */ 22 | loading: { color: '#fff' }, 23 | /* 24 | ** Global CSS 25 | */ 26 | css: [], 27 | /* 28 | ** Plugins to load before mounting the App 29 | */ 30 | plugins: [{ src: '~/plugins/multiTabState.client.js' }], 31 | /* 32 | ** Nuxt.js dev-modules 33 | */ 34 | buildModules: [], 35 | /* 36 | ** Nuxt.js modules 37 | */ 38 | modules: [], 39 | /* 40 | ** Build configuration 41 | */ 42 | build: { 43 | /* 44 | ** You can extend webpack config here 45 | */ 46 | extend(config, ctx) {}, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /examples/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt", 3 | "version": "1.0.0", 4 | "description": "Just a basic Nuxt app to show how to integrate vuex-multi-tab-state on its store", 5 | "author": "Gabriel Martín Blázquez", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate" 12 | }, 13 | "dependencies": { 14 | "nuxt": "^2.0.0", 15 | "vuex-multi-tab-state": "^1.0.13" 16 | }, 17 | "devDependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /examples/nuxt/pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /examples/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 66 | 67 | 99 | -------------------------------------------------------------------------------- /examples/nuxt/plugins/multiTabState.client.js: -------------------------------------------------------------------------------- 1 | import createMultiTabState from 'vuex-multi-tab-state'; 2 | 3 | export default ({ store }) => { 4 | createMultiTabState({ 5 | statesPaths: ['counter', 'fruits', 'animals.wolfs'], 6 | })(store); 7 | }; 8 | -------------------------------------------------------------------------------- /examples/nuxt/static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /examples/nuxt/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielmbmb/vuex-multi-tab-state/76a061834334dc969d856ea4d9939c0a9ac0d52e/examples/nuxt/static/favicon.ico -------------------------------------------------------------------------------- /examples/nuxt/store/animals.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | wolfs: 0, 3 | penguins: 0, 4 | }); 5 | 6 | export const mutations = { 7 | addAnimal(state, animal) { 8 | state[animal]++; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /examples/nuxt/store/fruits.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | oranges: 0, 3 | apples: 0, 4 | }); 5 | 6 | export const mutations = { 7 | addFruit(state, fruit) { 8 | state[fruit]++; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /examples/nuxt/store/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | counter: 0, 3 | }); 4 | 5 | export const mutations = { 6 | increment(state) { 7 | state.counter++; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-multi-tab-state", 3 | "version": "1.0.17", 4 | "description": "Share and synchronize status between multiple tabs with this plugin for Vuex.", 5 | "main": "./lib/index.js", 6 | "module": "./lib/index.esm.js", 7 | "unpkg": "./lib/index.umd.js", 8 | "umd:main": "./lib/index.umd.js", 9 | "exports": "./lib/index.modern.js", 10 | "types": "./lib/index.d.ts", 11 | "source": "./src/index.ts", 12 | "files": [ 13 | "/lib/**/*" 14 | ], 15 | "scripts": { 16 | "build": "microbundle -o lib/index.js", 17 | "prepare": "npm run build", 18 | "prepublishOnly": "npm test && npm run lint", 19 | "preversion": "npm run lint", 20 | "test": "nyc ./node_modules/mocha/bin/mocha", 21 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov", 22 | "lint": "tsc --noEmit && eslint 'src/**/*' --quiet --fix", 23 | "format": "prettier --write \"src/**/*\"" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/gabrielmbmb/vuex-multi-tab-state.git" 28 | }, 29 | "keywords": [ 30 | "vue", 31 | "vuex", 32 | "plugin" 33 | ], 34 | "author": "Gabriel Martin Blazquez ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/gabrielmbmb/vuex-multi-tab-state/issues" 38 | }, 39 | "homepage": "https://github.com/gabrielmbmb/vuex-multi-tab-state#readme", 40 | "devDependencies": { 41 | "@types/chai": "^4.2.9", 42 | "@types/chai-spies": "^1.0.1", 43 | "@types/dot-object": "^2.1.2", 44 | "@types/mocha": "^7.0.1", 45 | "@typescript-eslint/eslint-plugin": "^2.19.2", 46 | "@typescript-eslint/parser": "^2.19.2", 47 | "chai": "^4.2.0", 48 | "chai-spies": "^1.0.0", 49 | "codecov": "^3.7.2", 50 | "eslint": "^6.8.0", 51 | "eslint-config-airbnb-base": "^14.0.0", 52 | "eslint-config-prettier": "^6.10.0", 53 | "eslint-plugin-import": "^2.20.1", 54 | "eslint-plugin-prettier": "^3.1.2", 55 | "jsdom": "^16.1.0", 56 | "jsdom-global": "^3.0.2", 57 | "microbundle": "^0.12.4", 58 | "mocha": "^7.0.1", 59 | "mock-local-storage": "^1.1.11", 60 | "nyc": "^15.0.0", 61 | "prettier": "^1.19.1", 62 | "ts-node": "^8.6.2", 63 | "typescript": "^3.7.5", 64 | "vue": "^2.6.11", 65 | "vuex": "^3.1.2" 66 | }, 67 | "dependencies": { 68 | "dot-object": "^2.1.4" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { pick, set, remove } from 'dot-object'; 2 | import Tab from './tab'; 3 | 4 | export interface Options { 5 | statesPaths?: string[]; 6 | key?: string; 7 | onBeforeReplace?(state: any): any; 8 | onBeforeSave?(state: any): any; 9 | } 10 | 11 | export default function(options?: Options) { 12 | const tab = new Tab(window); 13 | let key: string = 'vuex-multi-tab'; 14 | let statesPaths: string[] = []; 15 | let onBeforeReplace = (state: any) => state; 16 | let onBeforeSave = (state: any) => state; 17 | 18 | if (options) { 19 | key = options.key ? options.key : key; 20 | statesPaths = options.statesPaths ? options.statesPaths : statesPaths; 21 | onBeforeReplace = options.onBeforeReplace || onBeforeReplace; 22 | onBeforeSave = options.onBeforeSave || onBeforeSave; 23 | } 24 | 25 | function filterStates(state: { [key: string]: any }): { [key: string]: any } { 26 | const result = {}; 27 | statesPaths.forEach(statePath => { 28 | set(statePath, pick(statePath, state), result); 29 | }); 30 | return result; 31 | } 32 | 33 | /** 34 | * simple object deep clone method 35 | * @param obj 36 | */ 37 | function cloneObj(obj: any): any { 38 | if (Array.isArray(obj)) { 39 | return obj.map(val => cloneObj(val)); 40 | } 41 | if (typeof obj === 'object' && obj !== null) { 42 | return Object.keys(obj).reduce((r: any, objKey) => { 43 | r[objKey] = cloneObj(obj[objKey]); 44 | return r; 45 | }, {}); 46 | } 47 | return obj; 48 | } 49 | 50 | function mergeState(oldState: any, newState: object) { 51 | // if whole state is to be replaced then do just that 52 | if (statesPaths.length === 0) return { ...newState }; 53 | 54 | // else clone old state 55 | const merged: any = cloneObj(oldState); 56 | 57 | // and replace only specified paths 58 | statesPaths.forEach(statePath => { 59 | const newValue = pick(statePath, newState); 60 | // remove value if it doesn't exist, overwrite otherwise 61 | if (typeof newValue === 'undefined') remove(statePath, merged); 62 | else set(statePath, newValue, merged); 63 | }); 64 | return merged; 65 | } 66 | 67 | if (!tab.storageAvailable()) { 68 | throw new Error('Local storage is not available!'); 69 | } 70 | 71 | function replaceState(store: any, state: object) { 72 | const adjustedState = onBeforeReplace(state); 73 | 74 | if (adjustedState) { 75 | store.replaceState(mergeState(store.state, adjustedState)); 76 | } 77 | } 78 | 79 | return (store: any) => { 80 | // First time, fetch state from local storage 81 | tab.fetchState(key, (state: object) => { 82 | replaceState(store, state); 83 | }); 84 | 85 | // Add event listener to the state saved in local storage 86 | tab.addEventListener(key, (state: object) => { 87 | replaceState(store, state); 88 | }); 89 | 90 | store.subscribe((mutation: MutationEvent, state: object) => { 91 | let toSave = state; 92 | 93 | // Filter state 94 | if (statesPaths.length > 0) { 95 | toSave = filterStates(state); 96 | } 97 | 98 | toSave = onBeforeSave(toSave); 99 | 100 | // Save state in local storage 101 | if (toSave) { 102 | tab.saveState(key, toSave); 103 | } 104 | }); 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /src/tab.ts: -------------------------------------------------------------------------------- 1 | export default class Tab { 2 | private tabId!: string; 3 | 4 | private window!: Window; 5 | 6 | constructor(window: Window) { 7 | // Thanks to: https://gist.github.com/6174/6062387 8 | this.tabId = 9 | Math.random() 10 | .toString(36) 11 | .substring(2, 15) + 12 | Math.random() 13 | .toString(36) 14 | .substring(2, 15); 15 | this.window = window; 16 | } 17 | 18 | storageAvailable(): Boolean { 19 | const test = 'vuex-multi-tab-state-test'; 20 | try { 21 | this.window.localStorage.setItem(test, test); 22 | this.window.localStorage.removeItem(test); 23 | return true; 24 | } catch (e) { 25 | return false; 26 | } 27 | } 28 | 29 | saveState(key: string, state: object) { 30 | const toSave = JSON.stringify({ 31 | id: this.tabId, 32 | state, 33 | }); 34 | 35 | // Save the state in local storage 36 | this.window.localStorage.setItem(key, toSave); 37 | } 38 | 39 | fetchState(key: string, cb: Function) { 40 | const value = this.window.localStorage.getItem(key); 41 | 42 | if (value) { 43 | try { 44 | const parsed = JSON.parse(value); 45 | cb(parsed.state); 46 | } catch (e) { 47 | console.warn(`State saved in localStorage with key ${key} is invalid!`); 48 | } 49 | } 50 | } 51 | 52 | addEventListener(key: string, cb: Function) { 53 | return this.window.addEventListener('storage', (event: StorageEvent) => { 54 | if (!event.newValue || event.key !== key) { 55 | return; 56 | } 57 | 58 | try { 59 | const newState = JSON.parse(event.newValue); 60 | 61 | // Check if the new state is from another tab 62 | if (newState.id !== this.tabId) { 63 | cb(newState.state); 64 | } 65 | } catch (e) { 66 | console.warn( 67 | `New state saved in localStorage with key ${key} is invalid` 68 | ); 69 | } 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/test.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | import chai, { expect } from 'chai'; 5 | import spies from 'chai-spies'; 6 | import createMultiTabState from '../src/index'; 7 | 8 | chai.use(spies); 9 | 10 | Vue.use(Vuex); 11 | 12 | // Do not show tips 13 | Vue.config.productionTip = false; 14 | 15 | describe('vuex-multi-tab-state basic tests', () => { 16 | const warnSpy = chai.spy.on(console, 'warn'); 17 | 18 | afterEach(() => { 19 | if (window.localStorage !== null) { 20 | window.localStorage.clear(); 21 | } 22 | }); 23 | 24 | it('should fetch state from local storage', () => { 25 | const testState = { id: 'randomIdHere', state: { random: 6 } }; 26 | window.localStorage.setItem('vuex-multi-tab', JSON.stringify(testState)); 27 | 28 | const store = new Vuex.Store({ state: { random: 0 } }); 29 | const plugin = createMultiTabState(); 30 | const spy = chai.spy.on(store, 'replaceState'); 31 | 32 | plugin(store); 33 | expect(spy).to.have.been.called.with(testState.state); 34 | }); 35 | 36 | it('should fetch filtered nested modules state from local storage without no-state-mutation errors', () => { 37 | const testState = { 38 | id: 'randomIdHere', 39 | state: { 40 | random: 1, 41 | modA: { 42 | rainbow: [1, 2, 3], 43 | some: { 44 | value1: 3, 45 | value2: 4, 46 | }, 47 | }, 48 | }, 49 | }; 50 | window.localStorage.setItem('vuex-multi-tab', JSON.stringify(testState)); 51 | 52 | const store = new Vuex.Store({ 53 | strict: true, 54 | state: { random: 0 }, 55 | modules: { 56 | modA: { 57 | //namespaced: true, 58 | state: { 59 | rainbow: [], 60 | some: { 61 | value1: 0, 62 | value2: 0, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }); 68 | 69 | const plugin = createMultiTabState({ 70 | statesPaths: ['random', 'modA.rainbow', 'modA.some.value1'], 71 | }); 72 | 73 | plugin(store); 74 | expect(store.state).to.be.eql({ 75 | random: 1, 76 | modA: { 77 | rainbow: [1, 2, 3], 78 | some: { 79 | value1: 3, 80 | value2: 0, // not set during read-from-storage 81 | }, 82 | }, 83 | }); 84 | }); 85 | 86 | it('should save only the specified states in local storage', () => { 87 | const store = new Vuex.Store({ 88 | state: { bar: { random: 0, rainbow: 0 }, foo: 0 }, 89 | mutations: { 90 | incrementBarRandom(state) { 91 | state.bar.random += 1; 92 | }, 93 | incrementFoo(state) { 94 | state.foo += 1; 95 | }, 96 | }, 97 | plugins: [createMultiTabState({ statesPaths: ['bar.random', 'foo'] })], 98 | }); 99 | 100 | store.commit('incrementBarRandom'); 101 | store.commit('incrementFoo'); 102 | 103 | const stateInLs: string | null = window.localStorage.getItem( 104 | 'vuex-multi-tab' 105 | ); 106 | 107 | if (typeof stateInLs === 'string') { 108 | const parsedStateInLs = JSON.parse(stateInLs); 109 | expect(parsedStateInLs.state.bar.random).to.be.eq(1); 110 | expect(parsedStateInLs.state.foo).to.be.eq(1); 111 | expect(parsedStateInLs.state.bar.rainbow).to.be.undefined; 112 | } 113 | }); 114 | 115 | it('should properly merge specified states from same parent when saving to local storage', () => { 116 | const store = new Vuex.Store({ 117 | state: { bar: { random: 0, rainbow: 0 } }, 118 | mutations: { 119 | incrementBarRandom(state) { 120 | state.bar.random += 1; 121 | }, 122 | }, 123 | plugins: [ 124 | createMultiTabState({ statesPaths: ['bar.random', 'bar.rainbow'] }), 125 | ], 126 | }); 127 | 128 | store.commit('incrementBarRandom'); 129 | 130 | const stateInLs: string | null = window.localStorage.getItem( 131 | 'vuex-multi-tab' 132 | ); 133 | 134 | if (typeof stateInLs === 'string') { 135 | const parsedStateInLs = JSON.parse(stateInLs); 136 | expect(parsedStateInLs.state.bar.random).to.be.eq(1); 137 | expect(parsedStateInLs.state.bar.rainbow).to.be.eq(0); 138 | } 139 | }); 140 | 141 | it('should merge arrays correctly', () => { 142 | const store = new Vuex.Store({ 143 | state: { random: ['bar', 'foo'] }, 144 | }); 145 | const plugin = createMultiTabState(); 146 | 147 | window.localStorage.setItem( 148 | 'vuex-multi-tab', 149 | JSON.stringify({ 150 | id: 'randomIdHere', 151 | state: { 152 | random: ['bar'], 153 | }, 154 | }) 155 | ); 156 | 157 | plugin(store); 158 | expect(store.state.random).to.be.eql(['bar']); 159 | }); 160 | 161 | it('should merge objects correctly', () => { 162 | const store = new Vuex.Store({ 163 | state: { random: { bar1: 'foo1', bar2: 'foo1' } }, 164 | }); 165 | const plugin = createMultiTabState(); 166 | 167 | window.localStorage.setItem( 168 | 'vuex-multi-tab', 169 | JSON.stringify({ 170 | id: 'randomIdHere', 171 | state: { 172 | random: { bar2: 'foo2' }, 173 | }, 174 | }) 175 | ); 176 | 177 | plugin(store); 178 | expect(store.state.random).to.be.eql({ bar2: 'foo2' }); 179 | }); 180 | 181 | it('should merge objects only from specified paths', () => { 182 | const store = new Vuex.Store({ 183 | state: { random: { bar1: 'foo1', bar2: 'foo1', bar3: 'foo1' } }, 184 | }); 185 | const plugin = createMultiTabState({ 186 | statesPaths: ['random.bar2', 'random.bar3'], 187 | }); 188 | 189 | window.localStorage.setItem( 190 | 'vuex-multi-tab', 191 | JSON.stringify({ 192 | id: 'randomIdHere', 193 | state: { 194 | random: { bar2: 'foo2' }, 195 | }, 196 | }) 197 | ); 198 | 199 | plugin(store); 200 | expect(store.state.random).to.be.eql({ bar1: 'foo1', bar2: 'foo2' }); 201 | }); 202 | 203 | it('should properly merge falsy values', () => { 204 | const store = new Vuex.Store({ 205 | state: { 206 | random: { bar1: 0, bar2: true }, 207 | }, 208 | }); 209 | const plugin = createMultiTabState({ 210 | statesPaths: ['random.bar1', 'random.bar2'], 211 | }); 212 | 213 | window.localStorage.setItem( 214 | 'vuex-multi-tab', 215 | JSON.stringify({ 216 | id: 'randomIdHere', 217 | state: { 218 | random: { bar1: 0, bar2: false }, 219 | }, 220 | }) 221 | ); 222 | 223 | plugin(store); 224 | expect(store.state.random).to.be.eql({ bar1: 0, bar2: false }); 225 | }); 226 | 227 | it('should warn the user if the state in local storage is invalid', () => { 228 | window.localStorage.setItem('vuex-multi-tab', ''); 229 | // eslint-disable-next-line no-unused-vars 230 | const store = new Vuex.Store({ 231 | state: { random: 0 }, 232 | plugins: [createMultiTabState()], 233 | }); 234 | expect(warnSpy).to.have.been.called; 235 | }); 236 | 237 | it('should work with onBeforeSave option set', () => { 238 | const store = new Vuex.Store({ 239 | strict: true, 240 | state: { 241 | counter: 1, 242 | }, 243 | mutations: { 244 | count(state) { 245 | state.counter += 1; 246 | }, 247 | }, 248 | plugins: [ 249 | createMultiTabState({ 250 | onBeforeSave(state) { 251 | if (state.counter > 2) return; 252 | 253 | return { 254 | ...state, 255 | __time: Date.now(), 256 | }; 257 | }, 258 | }), 259 | ], 260 | }); 261 | 262 | store.commit('count'); 263 | store.commit('count'); 264 | store.commit('count'); 265 | 266 | const stateInLs: string | null = 267 | window.localStorage.getItem('vuex-multi-tab'); 268 | 269 | expect(typeof stateInLs).to.be.eq('string'); 270 | if (typeof stateInLs === 'string') { 271 | const parsedStateInLs = JSON.parse(stateInLs); 272 | 273 | expect(parsedStateInLs.state.__time).to.be.lte(Date.now()); 274 | expect(parsedStateInLs.state.counter).to.be.eq(2); 275 | } 276 | }); 277 | 278 | it('should work with onBeforeReplace option set', () => { 279 | const testState = { id: 'randomIdHere', state: { random: 6 } }; 280 | window.localStorage.setItem('vuex-multi-tab', JSON.stringify(testState)); 281 | 282 | const store = new Vuex.Store({ 283 | strict: true, 284 | state: { random: 0 }, 285 | }); 286 | const plugin = createMultiTabState({ 287 | onBeforeReplace(state) { 288 | return { random: 12 }; 289 | }, 290 | }); 291 | const spy = chai.spy.on(store, 'replaceState'); 292 | 293 | plugin(store); 294 | expect(spy).to.have.been.called.with({ random: 12 }); 295 | expect(store.state.random).to.be.eq(12); 296 | }); 297 | 298 | it('should work with onBeforeReplace option returning falsy value', () => { 299 | const testState = { id: 'randomIdHere', state: { random: 6 } }; 300 | window.localStorage.setItem('vuex-multi-tab', JSON.stringify(testState)); 301 | 302 | const store = new Vuex.Store({ 303 | strict: true, 304 | state: { random: 0 }, 305 | }); 306 | const plugin = createMultiTabState({ 307 | onBeforeReplace(state) { 308 | return; 309 | }, 310 | }); 311 | const spy = chai.spy.on(store, 'replaceState'); 312 | 313 | plugin(store); 314 | expect(spy).to.not.have.been.called; 315 | expect(store.state.random).to.be.eq(0); 316 | }); 317 | 318 | it('should not fetch state from local storage if event newValue property is undefined', () => { 319 | const testState = { id: 'randomIdHere', state: { random: 6 } }; 320 | window.localStorage.setItem('vuex-multi-tab', JSON.stringify(testState)); 321 | 322 | const store = new Vuex.Store({ 323 | state: { random: 0 }, 324 | plugins: [createMultiTabState()], 325 | }); 326 | const spy = chai.spy.on(store, 'replaceState'); 327 | 328 | const event = new CustomEvent('storage'); 329 | window.dispatchEvent(event); 330 | 331 | expect(spy).to.not.have.been.called.twice; 332 | }); 333 | 334 | it('should save state in local storage when new state is set', () => { 335 | const store = new Vuex.Store({ 336 | state: { random: 0 }, 337 | mutations: { 338 | increment(state) { 339 | state.random += 1; 340 | }, 341 | }, 342 | plugins: [createMultiTabState()], 343 | }); 344 | 345 | store.commit('increment'); 346 | 347 | const stateInLs: string | null = window.localStorage.getItem( 348 | 'vuex-multi-tab' 349 | ); 350 | 351 | if (typeof stateInLs === 'string') { 352 | const parsedStateInLs = JSON.parse(stateInLs); 353 | expect(parsedStateInLs.state.random).to.be.eq(1); 354 | } 355 | }); 356 | 357 | it('should fetch state when it has been changed', () => { 358 | Object.defineProperty(window, 'addEventListener', { 359 | value: (type: string, fn: Function) => { 360 | fn({ 361 | key: 'vuex-multi-tab', 362 | newValue: JSON.stringify({ 363 | id: 'randomIdHere', 364 | state: { 365 | random: 8, 366 | }, 367 | }), 368 | }); 369 | }, 370 | }); 371 | 372 | const testState = { id: 'randomIdHere', state: { random: 6 } }; 373 | window.localStorage.setItem('vuex-multi-tab', JSON.stringify(testState)); 374 | 375 | const store = new Vuex.Store({ 376 | state: { random: 0 }, 377 | plugins: [createMultiTabState()], 378 | }); 379 | 380 | expect(store.state.random).to.be.eq(8); 381 | }); 382 | 383 | it('should warn the user if the new state in local storage is invalid', () => { 384 | Object.defineProperty(window, 'addEventListener', { 385 | value: (type: string, fn: Function) => { 386 | fn({ 387 | key: 'vuex-multi-tab', 388 | newValue: '', 389 | }); 390 | }, 391 | }); 392 | 393 | // eslint-disable-next-line no-unused-vars 394 | const store = new Vuex.Store({ 395 | state: { random: 0 }, 396 | plugins: [createMultiTabState()], 397 | }); 398 | 399 | expect(warnSpy).to.have.been.called; 400 | }); 401 | 402 | it('should accept custom local storage key', () => { 403 | const store = new Vuex.Store({ 404 | state: { bar: 'foo1' }, 405 | }); 406 | const plugin = createMultiTabState({ key: 'custom-key' }); 407 | 408 | window.localStorage.setItem( 409 | 'custom-key', 410 | JSON.stringify({ 411 | id: 'randomIdHere', 412 | state: { bar: 'foo2' }, 413 | }) 414 | ); 415 | 416 | plugin(store); 417 | expect(store.state.bar).to.be.eql('foo2'); 418 | }); 419 | 420 | it('should throw if local storage is not available', () => { 421 | Object.defineProperty(window, 'localStorage', { 422 | value: null, 423 | }); 424 | expect(() => createMultiTabState()).to.throw(); 425 | }); 426 | }); 427 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./lib" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": ["src"], 67 | "exclude": ["node_modules", "test", "examples"] 68 | } 69 | --------------------------------------------------------------------------------