├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── integrate.yml ├── .gitignore ├── .huskyrc ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── .remarkrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── build └── rollup.config.ts ├── commitlint.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── ReactiveMap.ts ├── ReactiveSet.ts ├── index.ts ├── types.ts ├── useReactiveMap.ts └── useReactiveSet.ts ├── test └── index.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{Makefile,**.mk}] 13 | indent_style = tab 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Describe the PR 2 | 3 | A clear and concise description of what the pull request does. 4 | 5 | ## PR checklist 6 | 7 | 8 | 9 | **What kind of change does this PR introduce?** (check at least one) 10 | 11 | - [ ] Bugfix (fixes a boo-boo in the code) - `fix(...)`, requires a patch version update 12 | - [ ] Feature (adds a new feature to `vue-reactive-collection`) - `feat(...)`, requires a minor version update 13 | - [ ] Enhancement (augments an existing feature) - `feat(...)`, requires a minor version update 14 | - [ ] Documentation update (improves documentation or typo fixes) - `docs(...)`, requires a patch version update 15 | - [ ] Other (please describe) 16 | 17 | **Does this PR introduce a breaking change?** (check one) 18 | 19 | - [ ] No 20 | - [ ] Yes (please describe since breaking changes require a major version update) 21 | 22 | **The PR fulfills these requirements:** 23 | 24 | - [ ] It's submitted to the `dev` branch, **not** the `main` branch 25 | - [ ] When resolving a specific issue, it's referenced in the PR's title (i.e. `[...] (fixes #xxx[,#xxx])`, where "xxx" is the issue number) 26 | - [ ] It should address only one issue or feature. If adding multiple features or fixing a bug and adding a new feature, break them into separate PRs if at all possible. 27 | - [ ] The title should follow the [**Conventional Commits**](https://www.conventionalcommits.org/) naming convention (i.e. `fix: message`, `docs: message`, `chore: message`, etc.). **This is very important, as the `CHANGELOG` is generated from these messages, and determines the next version type (patch or minor).** 28 | 29 | **If new features/enhancement/fixes are added or changed:** 30 | 31 | - [ ] Includes documentation updates 32 | - [ ] New/updated tests are included and passing (required for new features and enhancements) 33 | - [ ] Existing test suites are passing 34 | 35 | **If adding a new feature, or changing the functionality of an existing feature, the PR's 36 | description above includes:** 37 | 38 | - [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it) 39 | -------------------------------------------------------------------------------- /.github/workflows/integrate.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main, dev] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')" 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | - uses: actions/cache@v3 18 | with: 19 | path: ~/.npm 20 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 21 | restore-keys: | 22 | ${{ runner.os }}-node- 23 | - name: Install dependencies 24 | run: npm install 25 | - name: Lint 26 | run: npm run lint 27 | - name: Test 28 | run: npm run test 29 | - name: Build 30 | run: npm run build 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 5 | "pre-push": "npm run lint" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*": [ 3 | "editorconfig-checker" 4 | ], 5 | "*.{js,ts,md}": [ 6 | "prettier" 7 | ], 8 | "package.json": [ 9 | "sort-package-json --check" 10 | ], 11 | "*.md": [ 12 | "remark" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "endOfLine": "lf" 5 | } 6 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "remark-preset-lint-recommended", 4 | [ 5 | "remark-lint-list-item-indent", 6 | "space" 7 | ], 8 | [ 9 | "remark-lint-no-undefined-references", 10 | { 11 | "allow": [ 12 | " " 13 | ] 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 26 | - Trolling, insulting/derogatory comments, and personal or political attacks 27 | - Public or private harassment 28 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 29 | - Other conduct which could reasonably be considered inappropriate in a 30 | professional setting 31 | 32 | ## Our Responsibilities 33 | 34 | Project maintainers are responsible for clarifying the standards of acceptable 35 | behavior and are expected to take appropriate and fair corrective action in 36 | response to any instances of unacceptable behavior. 37 | 38 | Project maintainers have the right and responsibility to remove, edit, or 39 | reject comments, commits, code, wiki edits, issues, and other contributions 40 | that are not aligned to this Code of Conduct, or to ban temporarily or 41 | permanently any contributor for other behaviors that they deem inappropriate, 42 | threatening, offensive, or harmful. 43 | 44 | ## Scope 45 | 46 | This Code of Conduct applies both within project spaces and in public spaces 47 | when an individual is representing the project or its community. Examples of 48 | representing a project or community include using an official project e-mail 49 | address, posting via an official social media account, or acting as an appointed 50 | representative at an online or offline event. Representation of a project may be 51 | further defined and clarified by project maintainers. 52 | 53 | ## Enforcement 54 | 55 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 56 | reported by contacting the project team at team@bluecanvas.io. All 57 | complaints will be reviewed and investigated and will result in a response that 58 | is deemed necessary and appropriate to the circumstances. The project team is 59 | obligated to maintain confidentiality with regard to the reporter of an incident. 60 | Further details of specific enforcement policies may be posted separately. 61 | 62 | Project maintainers who do not follow or enforce the Code of Conduct in good 63 | faith may face temporary or permanent repercussions as determined by other 64 | members of the project's leadership. 65 | 66 | ## Attribution 67 | 68 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 69 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 70 | 71 | [homepage]: https://www.contributor-covenant.org 72 | 73 | For answers to common questions about this code of conduct, see 74 | https://www.contributor-covenant.org/faq 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Blue Canvas 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | npm run build 4 | 5 | .PHONY: test 6 | test: 7 | npm test 8 | 9 | lint: 10 | npm run lint 11 | 12 | fmt: 13 | npm run fmt 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-reactive-collection 2 | 3 | ## Installation 4 | 5 | Reactive `Map` and `Set` for Vue 2 using [vue-demi](https://github.com/vueuse/vue-demi) plugin. 6 | 7 | ### NPM 8 | 9 | ```bash 10 | $ npm i vue-reactive-collection 11 | ``` 12 | 13 | ### Yarn 14 | 15 | ```bash 16 | $ yarn add vue-reactive-collection 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```ts 22 | import { defineComponent } from 'vue' 23 | import { useReactiveSet, useReactiveMap } from 'vue-reactive-collection' 24 | 25 | export default defineComponent({ 26 | // ... 27 | setup() { 28 | const set = useReactiveSet() 29 | const map = useReactiveMap() 30 | 31 | return { 32 | set, 33 | map, 34 | } 35 | }, 36 | }) 37 | ``` 38 | 39 | `set` and `map` will have the same methods as native `Set` and `Map`. They can be accessed via `.value` as you would do it with `ref`. The beauty of it is that they are completely reactive, so calling `set.value.add/delete/clear` or `map.value.set/delete/clear` will cause a template rerender. 40 | 41 | ## Motivation 42 | 43 | One of the [Vue 2 drawbacks](https://github.com/vuejs/vue/issues/2410) is the lack of a first class support for `Map` and `Set`. Though it has been recently implemented in Vue 3. The purpose of this library is to allow the usage of `Map` and `Set` in Vue 2 as you would use them in Vue 3. 44 | 45 | During Vue 3 migration `useReactiveSet` and `useReactiveMap` will be replaced with Vue `ref`. 46 | 47 | ```ts 48 | const set = useReactiveSet() 49 | ``` 50 | 51 | will become 52 | 53 | ```ts 54 | const set = ref(new Set()) 55 | ``` 56 | 57 | That's it, that simple. 58 | 59 | The idea was adopted from @inca [comment](https://github.com/vuejs/vue/issues/2410#issuecomment-318487855) in this [issue](https://github.com/vuejs/vue/issues/2410). 60 | 61 | ## Q&A 62 | 63 | ### How to pass an initial value 64 | 65 | You can pass an initial value as you would it with native `Map` and `Set`. 66 | 67 | ```ts 68 | const fruits = useReactiveSet(new Set(['apple', 'pear'])) 69 | const vegetables = useReactiveSet(['cabbage', 'onion']) 70 | ``` 71 | 72 | ```ts 73 | const fruits = useReactiveMap( 74 | new Map([ 75 | ['apple', true], 76 | ['pear', true], 77 | ]), 78 | ) 79 | const vegetables = useReactiveMap([ 80 | ['cabbage', true], 81 | ['onion', true], 82 | ]) 83 | ``` 84 | 85 | ## Tests 86 | 87 | ```bash 88 | npm test 89 | ``` 90 | 91 | ## Build 92 | 93 | ```bash 94 | npm run build 95 | ``` 96 | 97 | ## License 98 | 99 | [MIT](http://opensource.org/licenses/MIT) 100 | -------------------------------------------------------------------------------- /build/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { URL } from 'url' 3 | import { defineConfig } from 'rollup' 4 | import common from '@rollup/plugin-commonjs' 5 | import resolve from '@rollup/plugin-node-resolve' 6 | import typescript from '@rollup/plugin-typescript' 7 | import replace from '@rollup/plugin-replace' 8 | import terser from '@rollup/plugin-terser' 9 | 10 | const __dirname = new URL('.', import.meta.url).pathname 11 | 12 | const FILE = { 13 | name: 'vue-reactive-collection', 14 | path: path.join(__dirname, '../src/index.ts'), 15 | } 16 | 17 | const DIST_DIR = 'dist' 18 | const name = 'VueReactiveCollection' 19 | const external = ['vue-demi'] 20 | const globals = { 21 | 'vue-demi': 'VueDemi', 22 | } 23 | const plugins = [ 24 | common(), 25 | resolve(), 26 | replace({ 27 | 'process.env.NODE_ENV': 'production', 28 | preventAssignment: true, 29 | }), 30 | typescript({ 31 | tsconfig: './tsconfig.json', 32 | }), 33 | ] 34 | 35 | export default [ 36 | defineConfig({ 37 | input: FILE.path, 38 | external, 39 | output: [ 40 | { 41 | file: `${DIST_DIR}/${FILE.name}.js`, 42 | format: 'umd', 43 | globals, 44 | name, 45 | }, 46 | { 47 | file: `${DIST_DIR}/${FILE.name}.common.js`, 48 | format: 'cjs', 49 | }, 50 | { 51 | file: `${DIST_DIR}/${FILE.name}.esm.js`, 52 | format: 'esm', 53 | }, 54 | ], 55 | plugins, 56 | }), 57 | defineConfig({ 58 | input: FILE.path, 59 | external, 60 | output: { 61 | file: `${DIST_DIR}/${FILE.name}.min.js`, 62 | format: 'umd', 63 | globals, 64 | name, 65 | }, 66 | plugins: [terser({ format: { comments: false } })].concat(plugins), 67 | }), 68 | ] 69 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | collectCoverage: true, 6 | collectCoverageFrom: ['./src/**/*.ts'], 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-reactive-collection", 3 | "version": "0.2.0", 4 | "private": false, 5 | "description": "Reactive Map and Set for Vue 2", 6 | "keywords": [ 7 | "vue", 8 | "reactivity", 9 | "map", 10 | "set" 11 | ], 12 | "homepage": "https://github.com/bluecanvas/vue-reactive-collection#readme", 13 | "bugs": { 14 | "url": "https://github.com/bluecanvas/vue-reactive-collection/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/bluecanvas/vue-reactive-collection.git" 19 | }, 20 | "license": "MIT", 21 | "author": "Blue Canvas ", 22 | "contributors": [ 23 | { 24 | "name": "Reen Lokum" 25 | }, 26 | { 27 | "name": "Andrew Vasylchuk", 28 | "email": "andrew.d.vasilchuk@gmail.com" 29 | } 30 | ], 31 | "sideEffects": false, 32 | "main": "dist/vue-reactive-collection.js", 33 | "jsdelivr": "dist/vue-reactive-collection.min.js", 34 | "unpkg": "dist/vue-reactive-collection.min.js", 35 | "module": "dist/vue-reactive-collection.esm.js", 36 | "types": "dist/types/index.d.ts", 37 | "files": [ 38 | "src", 39 | "dist" 40 | ], 41 | "scripts": { 42 | "build": "rimraf dist/* && rollup --config build/rollup.config.ts --configPlugin typescript", 43 | "fmt": "npm run fmt:prettier && npm run fmt:package-json", 44 | "fmt:package-json": "sort-package-json", 45 | "fmt:prettier": "prettier --write ./**/*.{js,ts,md}", 46 | "lint": "npm run lint:editorconfig && npm run lint:package-json && npm run lint:prettier && npm run lint:remark", 47 | "lint:editorconfig": "editorconfig-checker", 48 | "lint:package-json": "sort-package-json --check", 49 | "lint:prettier": "prettier --check ./**/*.{js,ts,md}", 50 | "lint:remark": "remark {.,.github}", 51 | "test": "jest" 52 | }, 53 | "dependencies": { 54 | "vue-demi": "latest" 55 | }, 56 | "devDependencies": { 57 | "@rollup/plugin-commonjs": "^25.0.4", 58 | "@rollup/plugin-node-resolve": "^15.1.0", 59 | "@rollup/plugin-replace": "^5.0.2", 60 | "@rollup/plugin-terser": "^0.4.3", 61 | "@rollup/plugin-typescript": "^11.1.2", 62 | "@types/jest": "^29.5.3", 63 | "@vue/test-utils": "^1.3.6", 64 | "editorconfig-checker": "^5.1.1", 65 | "husky": "^8.0.3", 66 | "jest": "^29.6.2", 67 | "jest-environment-jsdom": "^29.6.2", 68 | "lint-staged": "^14.0.0", 69 | "prettier": "^3.0.2", 70 | "remark-cli": "^11.0.0", 71 | "remark-lint": "^9.1.2", 72 | "remark-preset-lint-markdown-style-guide": "^5.1.3", 73 | "remark-preset-lint-recommended": "^6.1.3", 74 | "rimraf": "^5.0.1", 75 | "rollup": "^3.28.0", 76 | "sort-package-json": "^2.5.1", 77 | "ts-jest": "^29.1.1", 78 | "tslib": "^2.6.1", 79 | "typescript": "^5.1.6", 80 | "vue": "^2.7.14", 81 | "vue-template-compiler": "^2.7.14" 82 | }, 83 | "peerDependencies": { 84 | "@vue/composition-api": "^1.0.0-rc.1", 85 | "vue": "^2.0.0 || >=3.0.0" 86 | }, 87 | "peerDependenciesMeta": { 88 | "@vue/composition-api": { 89 | "optional": true 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ReactiveMap.ts: -------------------------------------------------------------------------------- 1 | import type { MapConstructorArgument } from './types' 2 | 3 | export default class ReactiveMap extends Map { 4 | constructor( 5 | private readonly onMutate: () => void, 6 | entries?: MapConstructorArgument, 7 | ) { 8 | super(entries) 9 | } 10 | 11 | set(key: K, value: V) { 12 | super.set(key, value) 13 | if (this.onMutate !== undefined) { 14 | this.onMutate() 15 | } 16 | return this 17 | } 18 | 19 | delete(key: K): boolean { 20 | const res = super.delete(key) 21 | this.onMutate() 22 | return res 23 | } 24 | 25 | clear() { 26 | super.clear() 27 | this.onMutate() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ReactiveSet.ts: -------------------------------------------------------------------------------- 1 | import type { SetConstructorArgument } from './types' 2 | 3 | export default class ReactiveSet extends Set { 4 | constructor( 5 | private readonly onMutate: () => void, 6 | values?: SetConstructorArgument, 7 | ) { 8 | super(values) 9 | } 10 | 11 | add(value: T) { 12 | super.add(value) 13 | if (this.onMutate !== undefined) { 14 | this.onMutate() 15 | } 16 | return this 17 | } 18 | 19 | delete(value: T): boolean { 20 | const res = super.delete(value) 21 | this.onMutate() 22 | return res 23 | } 24 | 25 | clear() { 26 | super.clear() 27 | this.onMutate() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useReactiveMap } from './useReactiveMap' 2 | export { default as useReactiveSet } from './useReactiveSet' 3 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type MapConstructorArgument = 2 | | readonly (readonly [K, V])[] 3 | | null 4 | | Map 5 | 6 | export type SetConstructorArgument = readonly T[] | null | Set 7 | -------------------------------------------------------------------------------- /src/useReactiveMap.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue-demi' 2 | 3 | import type { MapConstructorArgument } from './types' 4 | import ReactiveMap from './ReactiveMap' 5 | 6 | export default function useReactiveMap( 7 | entires?: MapConstructorArgument, 8 | ) { 9 | function onMutate() { 10 | map.value = map.value 11 | } 12 | 13 | const inner = ref(new ReactiveMap(onMutate, entires)) 14 | 15 | const map = computed>({ 16 | get: () => { 17 | return inner.value 18 | }, 19 | set: (map: Map) => { 20 | inner.value = new ReactiveMap(onMutate, map) 21 | }, 22 | }) 23 | 24 | return map 25 | } 26 | -------------------------------------------------------------------------------- /src/useReactiveSet.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue-demi' 2 | 3 | import type { SetConstructorArgument } from './types' 4 | import ReactiveSet from './ReactiveSet' 5 | 6 | export default function useReactiveSet(values?: SetConstructorArgument) { 7 | function onMutate() { 8 | set.value = set.value 9 | } 10 | 11 | const inner = ref(new ReactiveSet(onMutate, values)) 12 | 13 | const set = computed>({ 14 | get: () => { 15 | return inner.value 16 | }, 17 | set: (set: Set) => { 18 | inner.value = new ReactiveSet(onMutate, set) 19 | }, 20 | }) 21 | 22 | return set 23 | } 24 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue-demi' 2 | import { shallowMount, mount } from '@vue/test-utils' 3 | 4 | import { useReactiveMap, useReactiveSet } from '../src' 5 | 6 | describe('vue-reactive-collection', () => { 7 | describe('useReactiveMap', () => { 8 | describe('should accept the same arguments as original Map', () => { 9 | it('when instance of Map is passed', () => { 10 | const m = new Map() 11 | m.set('foo', 'bar') 12 | 13 | const map = useReactiveMap(m) 14 | 15 | expect(map.value.get('foo')).toBe('bar') 16 | }) 17 | 18 | it('when entries are passed', () => { 19 | const map = useReactiveMap([['foo', 'bar']]) 20 | 21 | expect(map.value.get('foo')).toBe('bar') 22 | }) 23 | }) 24 | 25 | it('exposed ref should be editable', () => { 26 | const map = useReactiveMap() 27 | 28 | map.value = new Map([['foo', 'bar']]) 29 | 30 | expect(map.value.get('foo')).toBe('bar') 31 | }) 32 | 33 | it('should trigger an update when Map#set method is called', async () => { 34 | const Component = defineComponent({ 35 | setup() { 36 | const map = useReactiveMap() 37 | 38 | function addItem() { 39 | map.value.set('foo', 'bar') 40 | } 41 | 42 | return { 43 | addItem, 44 | map, 45 | } 46 | }, 47 | template: /* HTML */ `
48 | 49 |
    50 |
  • {{ value }}
  • 51 |
52 |
`, 53 | }) 54 | const wrapper = shallowMount(Component) 55 | 56 | wrapper.find('button').trigger('click') 57 | await wrapper.vm.$nextTick() 58 | 59 | expect(wrapper.find('li').text()).toBe('bar') 60 | }) 61 | 62 | it('should trigger an update when Map#delete method is called', async () => { 63 | const Component = defineComponent({ 64 | setup() { 65 | const m = new Map() 66 | m.set('foo', 'bar') 67 | const map = useReactiveMap(m) 68 | 69 | function deleteItem() { 70 | map.value.delete('foo') 71 | } 72 | 73 | return { 74 | deleteItem, 75 | map, 76 | } 77 | }, 78 | template: /* HTML */ `
79 | 80 |
    81 |
  • {{ value }}
  • 82 |
83 |
`, 84 | }) 85 | const wrapper = shallowMount(Component) 86 | 87 | wrapper.find('button').trigger('click') 88 | await wrapper.vm.$nextTick() 89 | 90 | expect(wrapper.find('li').exists()).toBe(false) 91 | }) 92 | 93 | it('should trigger an update when Map#clear method is called', async () => { 94 | const Component = defineComponent({ 95 | setup() { 96 | const m = new Map() 97 | m.set('foo', 'bar') 98 | const map = useReactiveMap(m) 99 | 100 | function clear() { 101 | map.value.clear() 102 | } 103 | 104 | return { 105 | clear, 106 | map, 107 | } 108 | }, 109 | template: /* HTML */ `
110 | 111 |
    112 |
  • {{ value }}
  • 113 |
114 |
`, 115 | }) 116 | const wrapper = shallowMount(Component) 117 | 118 | wrapper.find('button').trigger('click') 119 | await wrapper.vm.$nextTick() 120 | 121 | expect(wrapper.find('li').exists()).toBe(false) 122 | }) 123 | 124 | it('should work when passed as a prop', async () => { 125 | const Child = defineComponent({ 126 | props: { 127 | map: { 128 | type: Map, 129 | required: true, 130 | }, 131 | }, 132 | template: /* HTML */ `
    133 |
  • {{ value }}
  • 134 |
`, 135 | }) 136 | 137 | const Component = defineComponent({ 138 | components: { Child }, 139 | setup() { 140 | const map = useReactiveMap() 141 | 142 | function add() { 143 | map.value.set('foo', 'bar') 144 | } 145 | 146 | return { 147 | map, 148 | add, 149 | } 150 | }, 151 | template: /* HTML */ `
152 | 154 |
`, 155 | }) 156 | const wrapper = mount(Component) 157 | 158 | wrapper.find('button').trigger('click') 159 | await wrapper.vm.$nextTick() 160 | 161 | expect(wrapper.find('li').text()).toBe('bar') 162 | }) 163 | }) 164 | 165 | describe('useReactiveSet', () => { 166 | describe('should accept the same arguments as original Set', () => { 167 | it('when instance of Set is passed', () => { 168 | const s = new Set() 169 | s.add('foo') 170 | 171 | const set = useReactiveSet(s) 172 | 173 | expect(set.value.has('foo')).toBe(true) 174 | }) 175 | 176 | it('when values are passed', () => { 177 | const set = useReactiveSet(['foo']) 178 | 179 | expect(set.value.has('foo')).toBe(true) 180 | }) 181 | }) 182 | 183 | it('exposed ref should be editable', () => { 184 | const set = useReactiveSet() 185 | 186 | set.value = new Set(['foo']) 187 | 188 | expect(set.value.has('foo')).toBe(true) 189 | }) 190 | 191 | it('should trigger an update when Set#add method is called', async () => { 192 | const Component = defineComponent({ 193 | setup() { 194 | const set = useReactiveSet() 195 | 196 | function addItem() { 197 | set.value.add('foo') 198 | } 199 | 200 | return { 201 | addItem, 202 | set, 203 | } 204 | }, 205 | template: /* HTML */ `
206 | 207 |
    208 |
  • {{ item }}
  • 209 |
210 |
`, 211 | }) 212 | const wrapper = shallowMount(Component) 213 | 214 | wrapper.find('button').trigger('click') 215 | await wrapper.vm.$nextTick() 216 | 217 | expect(wrapper.find('li').text()).toBe('foo') 218 | }) 219 | 220 | it('should trigger an update when Set#delete method is called', async () => { 221 | const Component = defineComponent({ 222 | setup() { 223 | const s = new Set() 224 | s.add('foo') 225 | const set = useReactiveSet(s) 226 | 227 | function deleteItem() { 228 | set.value.delete('foo') 229 | } 230 | 231 | return { 232 | deleteItem, 233 | set, 234 | } 235 | }, 236 | template: /* HTML */ `
237 | 238 |
    239 |
  • {{ item }}
  • 240 |
241 |
`, 242 | }) 243 | const wrapper = shallowMount(Component) 244 | 245 | wrapper.find('button').trigger('click') 246 | await wrapper.vm.$nextTick() 247 | 248 | expect(wrapper.find('li').exists()).toBe(false) 249 | }) 250 | 251 | it('should trigger an update when Set#clear method is called', async () => { 252 | const Component = defineComponent({ 253 | setup() { 254 | const s = new Set() 255 | s.add('foo') 256 | const set = useReactiveSet(s) 257 | 258 | function clear() { 259 | set.value.clear() 260 | } 261 | 262 | return { 263 | clear, 264 | set, 265 | } 266 | }, 267 | template: /* HTML */ `
268 | 269 |
    270 |
  • {{ item }}
  • 271 |
272 |
`, 273 | }) 274 | const wrapper = shallowMount(Component) 275 | 276 | wrapper.find('button').trigger('click') 277 | await wrapper.vm.$nextTick() 278 | 279 | expect(wrapper.find('li').exists()).toBe(false) 280 | }) 281 | 282 | it('should work when passed as a prop', async () => { 283 | const Child = defineComponent({ 284 | props: { 285 | set: { 286 | type: Set, 287 | required: true, 288 | }, 289 | }, 290 | template: /* HTML */ `
    291 |
  • {{ item }}
  • 292 |
`, 293 | }) 294 | 295 | const Component = defineComponent({ 296 | components: { Child }, 297 | setup() { 298 | const set = useReactiveSet() 299 | 300 | function add() { 301 | set.value.add('foo') 302 | } 303 | 304 | return { 305 | set, 306 | add, 307 | } 308 | }, 309 | template: /* HTML */ `
310 | 311 | 312 |
`, 313 | }) 314 | const wrapper = mount(Component) 315 | 316 | wrapper.find('button').trigger('click') 317 | await wrapper.vm.$nextTick() 318 | 319 | expect(wrapper.find('li').text()).toBe('foo') 320 | }) 321 | }) 322 | }) 323 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "Node", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "declarationDir": "./types" 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["node_modules"] 12 | } 13 | --------------------------------------------------------------------------------