├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs ├── .vitepress │ ├── components │ │ ├── index.js │ │ ├── useBoolean.vue │ │ ├── useDebounce.vue │ │ ├── useDebounceFn.vue │ │ ├── useEvent.vue │ │ ├── useEventRef.vue │ │ ├── useHash.vue │ │ ├── useHistory.vue │ │ ├── useInterval.vue │ │ ├── useLifecycles.vue │ │ ├── useLocalStorage.vue │ │ ├── useResize.vue │ │ ├── useScroll.vue │ │ ├── useScrollRef.vue │ │ ├── useSessionStorage.vue │ │ ├── useStorage.vue │ │ ├── useTimeout.vue │ │ ├── useTimeoutFn.vue │ │ ├── useTitle.vue │ │ ├── useToggle.vue │ │ └── useWindowScroll.vue │ ├── config.js │ └── theme │ │ └── index.js ├── dom │ ├── useEvent.md │ └── useEventRef.md ├── effects │ ├── useDebounce.md │ ├── useDebounceFn.md │ ├── useInterval.md │ ├── useTimeout.md │ └── useTimeoutFn.md ├── index.md ├── lifecycles │ └── useLifecycles.md ├── sensors │ ├── useResize.md │ ├── useScroll.md │ ├── useScrollRef.md │ └── useWindowScroll.md ├── state │ ├── useBoolean.md │ ├── useHash.md │ ├── useHistory.md │ ├── useTitle.md │ └── useToggle.md └── storage │ ├── useLocalStorage.md │ ├── useSessionStorage.md │ └── useStorage.md ├── gulpfile.js ├── jest.config.js ├── package-lock.json ├── package.json ├── release.config.js ├── scripts └── registerComponent.js ├── src ├── index.ts ├── useBoolean.ts ├── useDebounce.ts ├── useDebounceFn.ts ├── useEvent.ts ├── useEventRef.ts ├── useHash.ts ├── useHistory.ts ├── useInterval.ts ├── useLifecycles.ts ├── useLocalStorage.ts ├── useResize.ts ├── useScroll.ts ├── useScrollRef.ts ├── useSessionStorage.ts ├── useStorage.ts ├── useTimeout.ts ├── useTimeoutFn.ts ├── useTitle.ts ├── useToggle.ts ├── useWindowScroll.ts └── util.ts ├── tests ├── useBoolean.test.ts ├── useDebounce.test.ts ├── useDebounceFn.test.ts ├── useEvent.test.ts ├── useEventRef.test.ts ├── useHash.test.ts ├── useInterval.test.ts ├── useLifecycles.test.ts ├── useLocalStorage.test.ts ├── useResize.test.ts ├── useSessionStorage.test.ts ├── useStorage.test.ts ├── useTimeout.test.ts ├── useTimeoutFn.test.ts ├── useTitle.test.ts ├── useToggle.test.ts ├── useWindowScroll.test.ts └── util │ ├── index.ts │ ├── invokeHook.ts │ └── patchEventTarget.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # http://editorconfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | max_line_length = 120 13 | 14 | [*.{ts, tsx}] 15 | ij_typescript_enforce_trailing_comma = keep 16 | ij_typescript_use_double_quotes = false 17 | ij_typescript_force_quote_style = true 18 | ij_typescript_align_imports = false 19 | ij_typescript_align_multiline_ternary_operation = false 20 | ij_typescript_align_multiline_parameters_in_calls = false 21 | ij_typescript_align_multiline_parameters = false 22 | ij_typescript_align_multiline_chained_methods = false 23 | ij_typescript_else_on_new_line = false 24 | ij_typescript_catch_on_new_line = false 25 | ij_typescript_spaces_within_interpolation_expressions = false 26 | 27 | [*.md] 28 | max_line_length = 0 29 | trim_trailing_whitespace = false 30 | 31 | [COMMIT_EDITMSG] 32 | max_line_length = 0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | esm/* 2 | lib/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true 6 | }, 7 | extends: [ 8 | 'plugin:vue/essential', 9 | 'standard' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 12, 13 | parser: '@typescript-eslint/parser', 14 | sourceType: 'module' 15 | }, 16 | plugins: [ 17 | 'vue', 18 | '@typescript-eslint' 19 | ], 20 | rules: { 21 | 'no-undef': 'off', 22 | 'space-before-function-paren': 'off' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | esm 4 | lib 5 | coverage 6 | docs/.vitepress/dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "tabWidth": 2, 5 | "semi": false, 6 | "trailingComma": "none" 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | cache: 5 | yarn: true 6 | directories: 7 | - ~/.npm 8 | notifications: 9 | email: false 10 | node_js: 11 | - node 12 | stages: 13 | - name: lint 14 | - test 15 | - name: deploy 16 | 17 | jobs: 18 | include: 19 | - stage: lint 20 | script: 21 | - yarn lint:types 22 | - yarn lint 23 | - stage: test 24 | script: 25 | - yarn test 26 | - stage: deploy 27 | script: 28 | - yarn build 29 | deploy: 30 | provider: script 31 | email: '$npm_email' 32 | api_key: '$npm_key' 33 | skip_cleanup: true 34 | script: 35 | - yarn release 36 | on: 37 | tags: false 38 | 39 | branches: 40 | only: 41 | - master 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2020-10-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * fix eslint error ([41bae4a](https://github.com/lmhcoding/vhook/commit/41bae4ac27721c1686567dce1946a49d50a3ac87)) 7 | * **useEvent:** 修复 ref 变化时没有解绑事件的bug ([a5aa835](https://github.com/lmhcoding/vhook/commit/a5aa835b14e60a8dfd459cf3d6b2a923bc8c6f10)) 8 | * **useHistory:** patch when is client ([230290d](https://github.com/lmhcoding/vhook/commit/230290d9be044c33f08dc947eedccd42d0ac19c8)) 9 | * **useSessionStorage:** 修复没有导出 useSessionStorage 的bug ([644060a](https://github.com/lmhcoding/vhook/commit/644060ade92fce43499bb2ffee36db21baf35ae6)) 10 | * **useTimeout:** clear setTimeout when component is unMounted ([6a00858](https://github.com/lmhcoding/vhook/commit/6a008585f49ef0b5f9c4024e2aafc43190dec28b)) 11 | * **useToogle:** fix wrong type and ban changing state with ref ([963aca2](https://github.com/lmhcoding/vhook/commit/963aca220b609f5a780fe0dcac355d45c2c911cf)) 12 | 13 | 14 | ### Features 15 | 16 | * **useDebounceFn:** support passing params tothe returned function ([17c97f0](https://github.com/lmhcoding/vhook/commit/17c97f0b086c203bb1d71a5d60fac206383c8754)) 17 | * **useHistory:** export clear function ([cbfd197](https://github.com/lmhcoding/vhook/commit/cbfd197c35ae04096901a418d4868414a5f5397a)) 18 | * **useInterval:** support restart interval ([5376ed8](https://github.com/lmhcoding/vhook/commit/5376ed8eb76f31c4147aa8d7351b674ea1db8c92)) 19 | * **useResize:** support debounce ([3ccabbf](https://github.com/lmhcoding/vhook/commit/3ccabbf54ce05736ee422b81229203b8cd73b036)) 20 | * **useScroll:** support throttle ([cc8d636](https://github.com/lmhcoding/vhook/commit/cc8d6368ad0b656cee1fc067e54f75f4ae81703c)) 21 | * **useTimeoutFn:** 将 watch 设置为同步执行 ([87a5635](https://github.com/lmhcoding/vhook/commit/87a5635001eef0ee25cba6abfed89d9d8617fe63)) 22 | * **useWindowScroll:** support throttle ([c600d1b](https://github.com/lmhcoding/vhook/commit/c600d1b7592b95abc68f439df9c7a8312c57846e)) 23 | * add useDebounce ([6a23363](https://github.com/lmhcoding/vhook/commit/6a233638c4c7756299fd8084dc1ce6ce3bb5a24f)) 24 | * add useDebounceFn ([5f01102](https://github.com/lmhcoding/vhook/commit/5f01102d4311586f648c1a37856587d475b2a9d5)) 25 | * add useHash ([8c8d3f7](https://github.com/lmhcoding/vhook/commit/8c8d3f7e8468093fdb8b60f70f02d06285b3c047)) 26 | * add useHistory ([af1bd7f](https://github.com/lmhcoding/vhook/commit/af1bd7fa1361927d4f5462f891bbbd354831a48c)) 27 | * add useInterval ([a52fa1b](https://github.com/lmhcoding/vhook/commit/a52fa1bc1402bb90d3bd35dcf8426d7d4093686c)) 28 | * add useLifecycles ([b19dd8d](https://github.com/lmhcoding/vhook/commit/b19dd8ddd4bef7e6543ec14119a517378df5353e)) 29 | * add useLifecycles ([99286f8](https://github.com/lmhcoding/vhook/commit/99286f855c0273de065b6a1974fd400579443755)) 30 | * add useStorage & useLocalStorage & useSessionStorage ([b6d87c9](https://github.com/lmhcoding/vhook/commit/b6d87c9d79a282b20fecf706896e4138ebf5f27b)) 31 | * add useTimeoutFn ([7324143](https://github.com/lmhcoding/vhook/commit/73241434f3b9a60c97c4846a61d39437bac35588)) 32 | * add useToogle & useBoolean ([addfd95](https://github.com/lmhcoding/vhook/commit/addfd95bf2a34f976b013ecb26657874fcaa6846)) 33 | * config vitepress ([cc1a3a5](https://github.com/lmhcoding/vhook/commit/cc1a3a5d728c45c365c687e4bb5a2134aa7658fa)) 34 | * config vitepress ([31ba7fc](https://github.com/lmhcoding/vhook/commit/31ba7fc3beb62831d9e3647e4acf7224cb4bcc95)) 35 | * useTimeout ([65b364a](https://github.com/lmhcoding/vhook/commit/65b364a9ccdd5d9bcc1645f8655272755c2bd01f)) 36 | * **project:** init project ([689d0c6](https://github.com/lmhcoding/vhook/commit/689d0c6b172595bae67acd645fad00d423bad61c)) 37 | * **useEvent:** add useEvent ([8db2731](https://github.com/lmhcoding/vhook/commit/8db273151287b7a80f5c3e7c4a269276a9571037)) 38 | * **useEventRef:** add useEventRef ([7f20514](https://github.com/lmhcoding/vhook/commit/7f205140cb3d426fb8ee52767dd6e2d690a96be4)) 39 | * **useResize:** add useResize ([478f889](https://github.com/lmhcoding/vhook/commit/478f88979d827633cfbdbc5b59c785306c831347)) 40 | * **useScroll:** add useScroll ([70b2a3a](https://github.com/lmhcoding/vhook/commit/70b2a3a1b2debc0e489ee9dca7b2f950326ba64b)) 41 | * **useScrollRef:** add useScrollRef ([d963675](https://github.com/lmhcoding/vhook/commit/d963675572148f6529224aab8a056dd6b4daf99b)) 42 | * **useTitle:** add useTitle ([527846c](https://github.com/lmhcoding/vhook/commit/527846c813304ceeee11f7095997f46026bbe724)) 43 | * **useWindowScroll:** add useWindowScroll ([d1d29fc](https://github.com/lmhcoding/vhook/commit/d1d29fc0b4785b581990d117f6dab36b1c202c4c)) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 lmhcoding 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vhook 2 | 3 | Collection of Vue Composition Functions 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i vhook 9 | ``` 10 | 11 | 12 | 13 | 14 | 15 | - State 16 | - [``useTitle``](https://lmhcoding.github.io/vhook/state/useTitle.html) —— 用于设置页面的标签页标题 17 | - [``useToggle``](https://lmhcoding.github.io/vhook/state/useToggle.html) —— 用于在两个状态之间切换 18 | - [``useBoolean``](https://lmhcoding.github.io/vhook/state/useBoolean.html) —— 用于管理 ``Boolean`` 状态的 ``Hook`` 19 | - [``useHash``](https://lmhcoding.github.io/vhook/state/useHash.html) —— 追踪 ``location.hash`` 的变化 20 | - [``useHistory``](https://lmhcoding.github.io/vhook/state/useHistory.html) —— 追踪 ``history`` 的变化 21 | - DOM 22 | - [``useEvent``](https://lmhcoding.github.io/vhook/dom/useEvent.html) / [``useEventRef``](https://lmhcoding.github.io/vhook/dom/useEventRef.html) —— 用于监听事件的 ``Hook`` 23 | - Lifecycles 24 | - [``useLifecycles``](https://lmhcoding.github.io/vhook/lifecycles/useLifecycles.html) —— 同时使用 ``onMounted`` 和 ``onUnmounted`` 的 ``Hook`` 25 | - SideEffects 26 | - [``useDebounce``](https://lmhcoding.github.io/vhook/effects/useDebounce.html) —— 带防抖功能的状态 27 | - [``useDebounceFn``](https://lmhcoding.github.io/vhook/effects/useDebounceFn.html) —— 生成带防抖功能的函数 28 | - [``useInterval``](https://lmhcoding.github.io/vhook/effects/useInterval.html) —— 对 ``setInterval`` 的简单封装 29 | - [``useTimeout``](https://lmhcoding.github.io/vhook/effects/useTimeout.html) —— 用于在一段时间后更新值 30 | - [``useTimeoutFn``](https://lmhcoding.github.io/vhook/effects/useTimeoutFn.html) —— 用于在一段时间后执行回调 31 | - Storage 32 | - [``useLocalStorage``](https://lmhcoding.github.io/vhook/storage/useLocalStorage.html) —— 具备响应式功能的 ``localStorage`` 状态 33 | - [``useSessionStorage``](https://lmhcoding.github.io/vhook/storage/useSessionStorage.html) —— 具备响应式功能的 ``sessionStorage`` 状态 34 | - [``useStorage``](https://lmhcoding.github.io/vhook/storage/useStorage.html) —— 提供具备响应式的 ``localStorage`` 或 ``sessionStorage`` 状态 35 | - Sensors 36 | - [``useResize``](https://lmhcoding.github.io/vhook/sensors/useResize.html) —— 追踪 ``window`` 的大小 37 | - [``useScroll``](https://lmhcoding.github.io/vhook/sensors/useScroll.html) / [``useScrollRef``](https://lmhcoding.github.io/vhook/sensors/useScrollRef.html) —— 追踪特定 ``DOM`` 节点的滚动位置 38 | - [``useWindowScroll``](https://lmhcoding.github.io/vhook/sensors/useWindowScroll.html) —— 追踪 ``window`` 滚动的位置 -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@commitlint/config-conventional' 4 | ], 5 | rules: { 6 | 'body-max-line-length': [0], 7 | 'scope-case': [0] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/.vitepress/components/index.js: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent } from 'vue' 2 | 3 | const UseBoolean = defineAsyncComponent(() => import('./useBoolean.vue')) 4 | const UseDebounce = defineAsyncComponent(() => import('./useDebounce.vue')) 5 | const UseDebounceFn = defineAsyncComponent(() => import('./useDebounceFn.vue')) 6 | const UseEvent = defineAsyncComponent(() => import('./useEvent.vue')) 7 | const UseEventRef = defineAsyncComponent(() => import('./useEventRef.vue')) 8 | const UseHash = defineAsyncComponent(() => import('./useHash.vue')) 9 | const UseHistory = defineAsyncComponent(() => import('./useHistory.vue')) 10 | const UseInterval = defineAsyncComponent(() => import('./useInterval.vue')) 11 | const UseLifecycles = defineAsyncComponent(() => import('./useLifecycles.vue')) 12 | const UseLocalStorage = defineAsyncComponent(() => import('./useLocalStorage.vue')) 13 | const UseResize = defineAsyncComponent(() => import('./useResize.vue')) 14 | const UseScroll = defineAsyncComponent(() => import('./useScroll.vue')) 15 | const UseScrollRef = defineAsyncComponent(() => import('./useScrollRef.vue')) 16 | const UseSessionStorage = defineAsyncComponent(() => import('./useSessionStorage.vue')) 17 | const UseStorage = defineAsyncComponent(() => import('./useStorage.vue')) 18 | const UseTimeout = defineAsyncComponent(() => import('./useTimeout.vue')) 19 | const UseTimeoutFn = defineAsyncComponent(() => import('./useTimeoutFn.vue')) 20 | const UseTitle = defineAsyncComponent(() => import('./useTitle.vue')) 21 | const UseToggle = defineAsyncComponent(() => import('./useToggle.vue')) 22 | const UseWindowScroll = defineAsyncComponent(() => import('./useWindowScroll.vue')) 23 | 24 | export default function registerComponent(app) { 25 | app.component('UseBoolean', UseBoolean) 26 | app.component('UseDebounce', UseDebounce) 27 | app.component('UseDebounceFn', UseDebounceFn) 28 | app.component('UseEvent', UseEvent) 29 | app.component('UseEventRef', UseEventRef) 30 | app.component('UseHash', UseHash) 31 | app.component('UseHistory', UseHistory) 32 | app.component('UseInterval', UseInterval) 33 | app.component('UseLifecycles', UseLifecycles) 34 | app.component('UseLocalStorage', UseLocalStorage) 35 | app.component('UseResize', UseResize) 36 | app.component('UseScroll', UseScroll) 37 | app.component('UseScrollRef', UseScrollRef) 38 | app.component('UseSessionStorage', UseSessionStorage) 39 | app.component('UseStorage', UseStorage) 40 | app.component('UseTimeout', UseTimeout) 41 | app.component('UseTimeoutFn', UseTimeoutFn) 42 | app.component('UseTitle', UseTitle) 43 | app.component('UseToggle', UseToggle) 44 | app.component('UseWindowScroll', UseWindowScroll) 45 | } 46 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useBoolean.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useDebounce.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useDebounceFn.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useEvent.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useEventRef.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useHash.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useHistory.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useInterval.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useLifecycles.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useLocalStorage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useResize.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useScroll.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useScrollRef.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useSessionStorage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useStorage.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useTimeout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useTimeoutFn.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useTitle.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useToggle.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /docs/.vitepress/components/useWindowScroll.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | title: 'vhook', 3 | base: '/vhook/', 4 | lang: 'ZH-CN', 5 | description: 'Collection of Vue Composition Functions', 6 | header: [], 7 | themeConfig: { 8 | repo: 'lmhcoding/vhook', 9 | docsRepo: 'lmhcoding/vhook', 10 | docsBranch: 'master', 11 | docsDir: 'docs', 12 | editLinks: true, 13 | locales: { 14 | '/': { 15 | nav: [ 16 | { 17 | text: 'Changelog', 18 | link: 'https://github.com/lmhcoding/vhook/blob/master/CHANGELOG.md' 19 | } 20 | ], 21 | sidebar: [ 22 | {text: 'Introduction', link: '/'}, 23 | { 24 | text: 'State', 25 | collapsable: false, 26 | children: [ 27 | {text: 'useTitle', link: '/state/useTitle'}, 28 | {text: 'useToggle', link: '/state/useToggle'}, 29 | {text: 'useBoolean', link: '/state/useBoolean'}, 30 | {text: 'useHash', link: '/state/useHash'}, 31 | {text: 'useHistory', link: '/state/useHistory'} 32 | ] 33 | }, 34 | { 35 | text: 'Dom', 36 | collapsable: false, 37 | children: [ 38 | {text: 'useEvent', link: '/dom/useEvent'}, 39 | {text: 'useEventRef', link: '/dom/useEventRef'}, 40 | ] 41 | }, 42 | { 43 | text: 'Lifecycles', 44 | collapsable: false, 45 | children: [ 46 | {text: 'useLifecycles', link: '/lifecycles/useLifecycles'} 47 | ] 48 | }, 49 | { 50 | text: 'SideEffects', 51 | collapsable: false, 52 | children: [ 53 | {text: 'useDebounce', link: '/effects/useDebounce'}, 54 | {text: 'useDebounceFn', link: '/effects/useDebounceFn'}, 55 | {text: 'useInterval', link: '/effects/useInterval'}, 56 | {text: 'useTimeout', link: '/effects/useTimeout'}, 57 | {text: 'useTimeoutFn', link: '/effects/useTimeoutFn'}, 58 | ] 59 | }, 60 | { 61 | text: 'Storage', 62 | collapsable: false, 63 | children: [ 64 | {text: 'useLocalStorage', link: '/storage/useLocalStorage'}, 65 | {text: 'useSessionStorage', link: '/storage/useSessionStorage'}, 66 | {text: 'useStorage', link: '/storage/useStorage'} 67 | ] 68 | }, 69 | { 70 | text: 'Sensors', 71 | collapsable: false, 72 | children: [ 73 | {text: 'useWindowScroll', link: '/sensors/useWindowScroll'}, 74 | {text: 'useResize', link: '/sensors/useResize'}, 75 | {text: 'useScroll', link: '/sensors/useScroll'}, 76 | {text: 'useScrollRef', link: '/sensors/useScrollRef'} 77 | ] 78 | } 79 | ] 80 | } 81 | } 82 | }, 83 | locales: { 84 | '/': { 85 | lang: 'ZH-CN', 86 | title: 'vhook', 87 | description: 'Collection of Vue Composition Functions' 88 | } 89 | } 90 | } 91 | 92 | module.exports = config -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import theme from 'vitepress/dist/client/theme-default' 2 | import registerComponents from '../components' 3 | 4 | theme.enhanceApp = (({ app }) => { 5 | registerComponents(app) 6 | }) 7 | 8 | export default theme 9 | -------------------------------------------------------------------------------- /docs/dom/useEvent.md: -------------------------------------------------------------------------------- 1 | # ``useEvent`` 2 | --- 3 | 4 | 用于监听事件的 ``Hook`` 5 | 6 | :::tip 7 | 会在组件卸载时自动解绑事件,当 ``target`` 为 ``Ref`` 时,``target`` 变化时也会自动解绑事件 8 | ::: 9 | 10 | 11 | 12 | ## API 13 | 14 | ```typescript 15 | const [target, clear] = useEvent(event, cb, options, target = window) 16 | ``` 17 | 18 | 19 | 20 | ## Params 21 | 22 | | 参数名 | 描述 | 类型 | 默认值 | 23 | | ------- | ------------------------------------------------------------ | ------------------------------------------------- | ------ | 24 | | event | 事件名 | string | | 25 | | cb | 事件监听回调 | Function | | 26 | | options | 传递给 ``addEventListener`` 和 ``removeEventListener`` 的第三个参数 | bolean \| AddEventListenerOptions | | 27 | | target | 绑定事件的目标 | EventTarget \| Ref \| string | Window | 28 | 29 | ## Result 30 | 31 | | 参数 | 说明 | 类型 | 32 | | ------ | ------------------ | ------------------------------------------------------------ | 33 | | target | 事件绑定的目标对象 | interface IEventTarget {readonly value: EventTarget \| null} | 34 | | clear | 用于解绑事件 | () => void | 35 | 36 | 37 | 38 | ## Example 39 | 40 | 41 | 42 | ## Code 43 | 44 | ```vue 45 | 59 | 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /docs/dom/useEventRef.md: -------------------------------------------------------------------------------- 1 | # useEventRef 2 | --- 3 | 4 | 用于绑定事件的 ``Hook`` 5 | 6 | :::tip 7 | 与 ``useEvent`` 功能相同,区别在于只接受三个参数: ``event``、``cb`` 和 ``options``,函数内部会返回一个 ``Ref``,用于在模板里设置事件绑定的对象 8 | ::: 9 | 10 | ## API 11 | 12 | ```typescript 13 | const [target, clear] = useEventRef(event, cb, options) 14 | ``` 15 | 16 | ## Params 17 | 18 | 19 | 20 | | 参数名 | 描述 | 类型 | 默认值 | 21 | | ------- | ------------------------------------------------------------ | --------------------------------- | ------ | 22 | | event | 事件名称 | string | | 23 | | cb | 事件回调 | Function | | 24 | | options | 传递给 ``addEventListener`` 和 ``removeEventListener`` 的第三个参数 | Bolean \| AddEventListenerOptions | | 25 | 26 | 27 | 28 | ## Result 29 | 30 | 31 | 32 | | 参数 | 说明 | 类型 | 33 | | ------ | --------------------------------------- | -------------------- | 34 | | target | 用于在模板中设置事件绑定的 ``DOM`` 节点 | Ref | 35 | | clear | 用于解绑事件 | () => void | 36 | 37 | ## Example 38 | 39 | 40 | 41 | 42 | 43 | ## Code 44 | 45 | ```vue 46 | 49 | 50 | 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /docs/effects/useDebounce.md: -------------------------------------------------------------------------------- 1 | # ``useDebounce`` 2 | 3 | 4 | 5 | 带防抖功能的状态 6 | 7 | 8 | 9 | ## API 10 | 11 | ```typescript 12 | const state = useDebounce(value, delay) 13 | ``` 14 | 15 | 16 | 17 | ## Params 18 | 19 | | 参数名 | 描述 | 类型 | 默认值 | 20 | | ------ | ------------ | ------ | ------ | 21 | | value | 默认状态值 | any | | 22 | | delay | 防抖延时时间 | number | 200 | 23 | 24 | 25 | 26 | ## Result 27 | 28 | 29 | 30 | | 参数名 | 描述 | 类型 | 31 | | ------ | -------------------- | ------ | 32 | | state | 带防抖功能的 ``Ref`` | Ref\ | 33 | 34 | 35 | 36 | ## Example 37 | 38 | 39 | 40 | 41 | 42 | ## Code 43 | 44 | ```vue 45 | 48 | 49 | 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /docs/effects/useDebounceFn.md: -------------------------------------------------------------------------------- 1 | # ``useDebounceFn`` 2 | 3 | 生成带防抖功能的函数 4 | 5 | 6 | 7 | ## API 8 | 9 | ```typescript 10 | const debounceFn = useDebounceFn(fn, delay) 11 | ``` 12 | 13 | 14 | 15 | ## Params 16 | 17 | | 参数名 | 描述 | 类型 | 默认值 | 18 | | ------ | ---------------------- | --------------------- | ------ | 19 | | fn | 需要添加防抖功能的函数 | (…rest: any[]) => any | | 20 | | delay | 延时时间 | number | 200 | 21 | 22 | 23 | 24 | ## Example 25 | 26 | 27 | 28 | ## Code 29 | 30 | 31 | 32 | ```vue 33 | 37 | 38 | 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /docs/effects/useInterval.md: -------------------------------------------------------------------------------- 1 | # useInterval 2 | 3 | --- 4 | 5 | 6 | 7 | 对 ``setInterval`` 的简单封装 8 | 9 | 10 | 11 | :::tip 12 | 13 | 在组件 ``mounted`` 时设置定时器,并在组件 ``unmounted`` 时清除定时器 14 | 15 | ::: 16 | 17 | 18 | 19 | ## API 20 | 21 | 22 | 23 | ```typescript 24 | const [clear, start] = useInterval(fn, delay, immediate) 25 | ``` 26 | 27 | 28 | 29 | ## Params 30 | 31 | | 参数名 | 描述 | 类型 | 默认值 | 32 | | --------- | ---------------------------- | -------- | ------ | 33 | | fn | 定时器回调 | Function | | 34 | | delay | 延时时间 | number | | 35 | | immediate | 是否立即执行回调在开启定时器 | boolean | false | 36 | 37 | 38 | 39 | ## Result 40 | 41 | 42 | 43 | | 参数名 | 描述 | 类型 | 44 | | ------ | -------------- | ---------- | 45 | | clear | 用于清除定时器 | () => void | 46 | | start | 用于启动定时器 | () => void | 47 | | | | | 48 | 49 | ## Example 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ## Code 58 | 59 | ```vue 60 | 65 | 66 | 83 | ``` 84 | 85 | -------------------------------------------------------------------------------- /docs/effects/useTimeout.md: -------------------------------------------------------------------------------- 1 | # useTimeout 2 | 3 | --- 4 | 5 | 用于在一段时间后更新值 6 | 7 | 8 | 9 | ## API 10 | 11 | ```typescript 12 | const {ready, start, stop} = useTimeout(delay, immediate) 13 | ``` 14 | 15 | 16 | 17 | ## Params 18 | 19 | 20 | 21 | | 参数名 | 描述 | 类型 | 默认值 | 22 | | --------- | ------------------ | ------- | ------ | 23 | | delay | 延时时间 | number | | 24 | | immediate | 是否立即启动定时器 | boolean | true | 25 | 26 | 27 | 28 | ## Result 29 | 30 | 31 | 32 | | 参数名 | 描述 | 类型 | 33 | | ------ | --------------- | ---------- | 34 | | ready | 定时是否结束 | boolean | 35 | | start | 开启/重启定时器 | () => void | 36 | | stop | 停止定时器 | () => void | 37 | 38 | 39 | 40 | ## Example 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ## Code 49 | 50 | 51 | 52 | ```vue 53 | 58 | 59 | 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /docs/effects/useTimeoutFn.md: -------------------------------------------------------------------------------- 1 | # ``useTimeoutFn`` 2 | 3 | --- 4 | 5 | 6 | 7 | 用于在一段时间后执行回调 8 | 9 | 10 | 11 | ## API 12 | 13 | 14 | 15 | ```typescript 16 | const { start, stop } = useTimeoutFn(fn, delay, immediate, clearEffectWhenStop) 17 | ``` 18 | 19 | 20 | 21 | ## Params 22 | 23 | | 参数名 | 描述 | 类型 | 默认值 | 24 | | ------------------- | --------------------------------------------- | -------- | ------ | 25 | | fn | 回调 | Function | | 26 | | delay | 定时时间 | number | | 27 | | immediate | 是否立即注册定时器 | boolen | true | 28 | | clearEffectWhenStop | 调用 ``stop`` 时是否停止内部的 ``watch`` 监听 | boolean | false | 29 | 30 | 31 | 32 | ## Result 33 | 34 | | 参数名 | 描述 | 类型 | 35 | | ------ | --------------- | ---------- | 36 | | start | 开启/重启定时器 | () => void | 37 | | Stop | 停止定时器 | () => void | 38 | 39 | 40 | 41 | ## Example 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ## Code 50 | 51 | ```vue 52 | 57 | 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # ``vhook`` 2 | 3 | --- 4 | 5 | ``vhook`` 是基于 ``vue3.0`` 的 ``Composition API`` 开发的组合函数库 6 | 7 | 8 | 9 | ## Inspiration 10 | 11 | 这个项目的灵感来源于一些优秀的开源 ``react Hooks``,如: [``react-use``](https://github.com/streamich/react-use)、[``ahooks``](https://github.com/alibaba/hooks)等。 12 | 13 | 14 | 15 | ## Install 16 | 17 | ```bash 18 | npm install vhook 19 | yarn add vhook 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /docs/lifecycles/useLifecycles.md: -------------------------------------------------------------------------------- 1 | # useLifecycles 2 | --- 3 | 4 | 同时使用 ``onMounted`` 和 ``onUnmounted`` 的 ``Hook`` 5 | 6 | ## API 7 | 8 | ```typescript 9 | useLifecycles(onMountedCb, onUnmountedCb) 10 | ``` 11 | 12 | ## Params 13 | 14 | | 参数名 | 描述 | 类型 | 默认值 | 15 | | ------------- | ------------------------ | --------- | ------ | 16 | | onMountedCb | ``onMounted`` 时的回调 | () => any | | 17 | | onUnmountedCb | ``onUnmounted`` 时的回调 | () => any | | 18 | 19 | 20 | 21 | ## Example 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## Code 30 | 31 | ```vue 32 | 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /docs/sensors/useResize.md: -------------------------------------------------------------------------------- 1 | # ``useResize`` 2 | 3 | --- 4 | 5 | 追踪 ``window`` 的大小 6 | 7 | 8 | 9 | ## API 10 | 11 | ```typescript 12 | const { width, height } = useResize(cb, delay) 13 | ``` 14 | 15 | 16 | 17 | ## Params 18 | 19 | | 参数名 | 描述 | 类型 | 默认值 | 20 | | ------ | ---------------------------- | --------------------------------------- | ------ | 21 | | cb | resize 时执行的回调 | (width: number, height: number) => any | null | 22 | | delay | 防抖延时时间(为0则不作防抖) | number | 200 | 23 | 24 | 25 | 26 | ## Result 27 | 28 | | 参数名 | 描述 | 类型 | 29 | | ------ | ------------------------ | --------------------------- | 30 | | width | 只读,window.innerWidth | DeepReadonly> | 31 | | height | 只读,window.innerHeight | DeepReadonly> | 32 | 33 | 34 | 35 | ## Example 36 | 37 | 38 | 39 | 40 | 41 | ## Code 42 | 43 | ```vue 44 | 48 | 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /docs/sensors/useScroll.md: -------------------------------------------------------------------------------- 1 | # ``useScroll`` 2 | 3 | --- 4 | 5 | 追踪特定 ``DOM`` 节点的滚动位置 6 | 7 | 8 | 9 | ## API 10 | 11 | ```typescript 12 | const [x, y, clear] = useScroll(target, delay) 13 | ``` 14 | 15 | 16 | 17 | ## Params 18 | 19 | | 参数名 | 描述 | 类型 | 默认值 | 20 | | ------ | ---------------------------------- | ----------------------------------------- | ------ | 21 | | target | 监听滚动的 ``DOM``,可使用css选择器 | String \| Element \| Ref | | 22 | | delay | 节流延时时间(为0则不使用节流) | number | 200 | 23 | 24 | 25 | 26 | ## Result 27 | 28 | | 参数名 | 描述 | 类型 | 29 | | ------ | ------------------ | --------------------------- | 30 | | x | element.scrollLeft | DeepReadonly> | 31 | | y | element.scrollTop | DeepReadonly> | 32 | | clear | 解绑监听事件 | () => void | 33 | 34 | 35 | 36 | ## Example 37 | 38 | 39 | 40 | ## Code 41 | 42 | ```vue 43 | 54 | 55 | 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /docs/sensors/useScrollRef.md: -------------------------------------------------------------------------------- 1 | # ``useScrollRef`` 2 | 3 | --- 4 | 5 | 追踪特定 ``DOM`` 节点的滚动位置 6 | 7 | 8 | 9 | :::tip 10 | 11 | 功能与 ``useScroll`` 相同,区别在于不用传入绑定的目标,内部会生成一个 ``Ref`` ,用于在模板中绑定 ``ref`` 12 | 13 | ::: 14 | 15 | 16 | 17 | ## API 18 | 19 | ```typescript 20 | const [target, x, y, clear] = useScrollRef(delay) 21 | ``` 22 | 23 | 24 | 25 | ## Params 26 | 27 | | 参数名 | 描述 | 类型 | 默认值 | 28 | | ------ | -------- | ------ | ------ | 29 | | delay | 节流时间 | number | 200 | 30 | 31 | 32 | 33 | ## Result 34 | 35 | | 参数名 | 描述 | 类型 | 36 | | ------ | ------------------ | --------------------------- | 37 | | x | element.scrollLeft | DeepReadonly> | 38 | | y | element.scrollTop | DeepReadonly> | 39 | | clear | 解绑监听事件 | () => void | 40 | | target | 用于设置 ``ref`` | Ref | 41 | 42 | 43 | 44 | ## Example 45 | 46 | 47 | 48 | ## Code 49 | 50 | ```vue 51 | 62 | 63 | 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /docs/sensors/useWindowScroll.md: -------------------------------------------------------------------------------- 1 | # ``useWindowScroll`` 2 | 3 | --- 4 | 5 | 追踪 ``window`` 滚动的位置 6 | 7 | 8 | 9 | ## API 10 | 11 | ```typescript 12 | const {x, y} = useWindowScroll(delay) 13 | ``` 14 | 15 | 16 | 17 | ## Params 18 | 19 | | 参数名 | 描述 | 类型 | 默认值 | 20 | | ------ | --------------------- | ------ | ------ | 21 | | delay | 节流时间(为0则不节流) | number | 200 | 22 | 23 | 24 | 25 | ## Result 26 | 27 | | 参数名 | 描述 | 类型 | 28 | | ------ | -------------- | --------------------------- | 29 | | x | window.scrollX | DeepReadonly> | 30 | | y | window.scrollY | DeepReadonly> | 31 | 32 | 33 | 34 | ## Example 35 | 36 | 37 | 38 | ## Code 39 | 40 | ```vue 41 | 45 | 46 | 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /docs/state/useBoolean.md: -------------------------------------------------------------------------------- 1 | # ``useBoolean`` 2 | --- 3 | 4 | 用于管理 ``Boolean`` 状态的 ``Hook`` 5 | 6 | 7 | 8 | ## API 9 | 10 | ```typescript 11 | const { 12 | state, 13 | toggle, 14 | setTrue, 15 | setFalse 16 | } = useBoolean(defaultValue?: boolean) 17 | ``` 18 | 19 | 20 | 21 | ## Params 22 | 23 | | 参数 | 说明 | 类型 | 默认值 | 24 | | ------------ | ------------ | ------- | ------ | 25 | | defaultValue | 初始默认状态 | boolean | false | 26 | 27 | 28 | 29 | ## Methods 30 | 31 | 该 Hook 返回以下函数: 32 | 33 | | 函数名 | 类型 | 描述 | 34 | | -------- | ------------------------ | ----------------- | 35 | | toggle | (next?: boolean) => void | 用于反转状态 | 36 | | setTrue | () => void | 将状态设置为true | 37 | | setFalse | () => void | 将状态设置为false | 38 | 39 | 40 | 41 | ## Example 42 | 43 | 44 | 45 | ## Code 46 | 47 | ```vue 48 | 54 | 55 | 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /docs/state/useHash.md: -------------------------------------------------------------------------------- 1 | # ``useHash`` 2 | 3 | --- 4 | 5 | 6 | 7 | 追踪 ``location.hash`` 的变化 8 | 9 | 10 | 11 | ## API 12 | 13 | ```typescript 14 | const { hash, setHash } = useHash() 15 | ``` 16 | 17 | 18 | 19 | ## Result 20 | 21 | | 参数名 | 描述 | 类型 | 22 | | ------- | --------------------------------------------- | --------------------------- | 23 | | hash | 一个只读状态,对应当前 ``location.hash`` 的值 | DeepReadonly> | 24 | | setHash | 用于设置新的 ``hash`` 值 | (hash: string) => void | 25 | 26 | 27 | 28 | ## Example 29 | 30 | 31 | 32 | ## Code 33 | 34 | ```vue 35 | 38 | 39 | 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /docs/state/useHistory.md: -------------------------------------------------------------------------------- 1 | # ``useHistory`` 2 | 3 | --- 4 | 5 | 6 | 7 | 追踪 ``history`` 的变化 8 | 9 | 10 | 11 | :::tip 12 | 13 | 代理拦截 ``pushState`` 以及 ``replaceState``,实现调用 ``popState`` 、 ``replaceState`` 以及 ``pushState`` 时,可以追踪到 ``history`` 的状态变化 14 | 15 | ::: 16 | 17 | 18 | 19 | ## API 20 | 21 | 22 | 23 | ```typescript 24 | const { 25 | state, 26 | hash, 27 | search, 28 | host, 29 | hostname, 30 | href, 31 | origin, 32 | pathname, 33 | port, 34 | protocol, 35 | clear 36 | } = useHistory() 37 | ``` 38 | 39 | 40 | 41 | ## Result 42 | 43 | | 参数名 | 描述 | 类型 | 44 | | -------- | ------------------------------------------------------------ | --------------------------- | 45 | | state | history.state | Any | 46 | | hash | location.hash | DeepReadonly> | 47 | | search | location.search | DeepReadonly> | 48 | | host | location.host | DeepReadonly> | 49 | | hostname | location.hostname | DeepReadonly> | 50 | | origin | location.origin | DeepReadonly> | 51 | | pathname | location.pathname | DeepReadonly> | 52 | | port | Location.port | DeepReadonly> | 53 | | protocol | location.protocol | DeepReadonly> | 54 | | clear | 清除对 ``popstate``、``pushstate``和``replacestate``的监听,不再响应变化 | () => void | 55 | 56 | 57 | 58 | ## Example 59 | 60 | 61 | 62 | 63 | 64 | ## Code 65 | 66 | ```vue 67 | 77 | 78 | 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /docs/state/useTitle.md: -------------------------------------------------------------------------------- 1 | # ``useTitle`` 2 | --- 3 | 4 | 用于设置页面的标签页标题 5 | 6 | ## API 7 | 8 | ```typescript 9 | export declare function useTitle( 10 | title: string, 11 | restoreOnUnMount?: boolean 12 | ): (title: string) => void; 13 | ``` 14 | 15 | ## Params 16 | 17 | | 参数 | 说明 | 类型 | 默认值 | 18 | | ---------------- | ------------------------ | ------- | ------ | 19 | | title | 标签页标题 | string | | 20 | | restoreOnUnMount | 组件卸载时是否恢复原标题 | boolean | false | 21 | 22 | ## Methods 23 | 24 | 该 Hook 返回以下函数: 25 | 26 | | 函数签名 | 描述 | 27 | | ------------------------ | ---------------- | 28 | | (title: string) => void | 用于设置页面标题 | 29 | 30 | ## Example 31 | 32 | 33 | 34 | 35 | ## Code 36 | --- 37 | 38 | ```vue 39 | 43 | 44 | 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /docs/state/useToggle.md: -------------------------------------------------------------------------------- 1 | # useToggle 2 | 3 | --- 4 | 5 | 6 | 7 | 用于在两个状态之间切换 8 | 9 | 10 | 11 | ## API 12 | 13 | 14 | 15 | ```typescript 16 | const { 17 | state, 18 | toggle, 19 | setDefault, 20 | setRight 21 | } = useToggle(defaultValue?: boolean) 22 | const { 23 | state, 24 | toggle, 25 | setDefault, 26 | setRight 27 | } = useToggle(defaultValue: any, reverseValue?:any) 28 | ``` 29 | 30 | 31 | 32 | ## Params 33 | 34 | | 参数 | 说明 | 类型 | 默认值 | 35 | | ------------ | ------------------------ | ------------------------------------------------ | ------ | 36 | | defaultValue | 可选,传入的默认状态值 | string \| number \| boolean \| undefined \| null | false | 37 | | reverseValue | 可选,传入的取反的状态值 | string \| number \| boolean \| undefined \| null | | 38 | 39 | 40 | 41 | ## Methods 42 | 43 | 该 Hook 返回以下函数: 44 | 45 | | 函数名 | 类型 | 描述 | 46 | | ---------- | --------------------- | ------------------------------------------------------------ | 47 | | Toggle | (next?: any) => void | 触发状态更改的函数,接受传入 ``useToggle`` 的两个参数值修改状态 | 48 | | setDefault | () => void | 重置回默认值 | 49 | | setRight | () => void | 设置为reverseValue | 50 | 51 | 52 | 53 | ## Example 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ## Code 62 | 63 | ```vue 64 | 83 | 84 | 113 | ``` 114 | 115 | -------------------------------------------------------------------------------- /docs/storage/useLocalStorage.md: -------------------------------------------------------------------------------- 1 | # ``useLocalStorage`` 2 | 3 | 具备响应式功能的 ``localStorage`` 状态 4 | 5 | 6 | 7 | ## API 8 | 9 | ```typescript 10 | const [state, setState] = useLocalStorage(key, val) 11 | ``` 12 | 13 | 14 | 15 | ## Params 16 | 17 | | 参数名 | 描述 | 类型 | 默认值 | 18 | | ------ | ------------------------------------------------------------ | ------ | ------ | 19 | | key | 存进 ``localStorage`` 中的 ``key`` | string | | 20 | | val | 存进 ``localStorage`` 中的 ``val``,会使用 ``JSON.stringify`` 进行序列化 | any | | 21 | 22 | 23 | 24 | ## Result 25 | 26 | | 参数名 | 描述 | 类型 | 27 | | -------- | ------------------------------------------------------------ | ---------------------------- | 28 | | state | 存进 ``localStorage`` 中的 ``val``,从 ``localStorage`` 中取出时会进行 ``JSON.parse`` 反序列化, 只读 | DeepReadonly> | 29 | | setState | 更新 ``localStorage``,同时 ``state`` 也会更新, 不传参数则等同于 ``removeItem`` | (Val?: T) => void | 30 | 31 | ## Example 32 | 33 | 34 | 35 | ## Code 36 | 37 | ```vue 38 | 45 | 46 | 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /docs/storage/useSessionStorage.md: -------------------------------------------------------------------------------- 1 | # ``useSessionStorage`` 2 | 3 | 具备响应式功能的 ``sessionStorage`` 状态 4 | 5 | 6 | 7 | ## API 8 | 9 | ```typescript 10 | const [state, setState] = useSessionStorage(key, val) 11 | ``` 12 | 13 | 14 | 15 | ## Params 16 | 17 | | 参数名 | 描述 | 类型 | 默认值 | 18 | | ------ | ------------------------------------------------------------ | ------ | ------ | 19 | | key | 存进 ``sessionStorage`` 中的 ``key`` | string | | 20 | | val | 存进 ``sessionStorage`` 中的 ``val``,会使用 ``JSON.stringify`` 进行序列化 | any | | 21 | 22 | 23 | 24 | ## Result 25 | 26 | | 参数名 | 描述 | 类型 | 27 | | -------- | ------------------------------------------------------------ | ---------------------------- | 28 | | state | 存进 ``sessionStorage`` 中的 ``val``,从 ``sessionStorage`` 中取出时会进行 ``JSON.parse`` 反序列化, 只读 | DeepReadonly> | 29 | | setState | 更新 ``sessionStorage``,同时 ``state`` 也会更新, 不传参数则等同于 ``removeItem`` | (Val?: T) => void | 30 | 31 | ## Example 32 | 33 | 34 | 35 | ## Code 36 | 37 | ```vue 38 | 46 | 47 | 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /docs/storage/useStorage.md: -------------------------------------------------------------------------------- 1 | # ``useStorage`` 2 | 3 | --- 4 | 5 | 提供具备响应式的 ``localStorage`` 或 ``sessionStorage`` 状态 6 | 7 | 8 | 9 | ## API 10 | 11 | ```typescript 12 | const [state, setState] = useStorage(key, val, storage) 13 | ``` 14 | 15 | 16 | 17 | ## Params 18 | | 参数名 | 描述 | 类型 | 默认值 | 19 | | ------- | ------------------------------------------------------------ | ------- | ------------ | 20 | | key | 存进 ``localStorage`` 或 ``sessionStorage`` 中的 ``key`` | string | | 21 | | val | 存进 ``localStorage`` 或 ``sessionStorage`` 中的 ``val``,会使用 ``JSON.stringify`` 进行序列化 | any | | 22 | | storage | 要使用的存储方式 | Storage | localStorage | 23 | 24 | ## Result 25 | 26 | | 参数名 | 描述 | 类型 | 27 | | -------- | ------------------------------------------------------------ | ---------------------------- | 28 | | state | 存进 ``localStorage`` 或 ``sessionStorage`` 中的 ``val``,从 ``localStorage`` 中取出时会进行 ``JSON.parse`` 反序列化, 只读 | DeepReadonly> | 29 | | setState | 更新 ``localStorage`` 或 ``sessionStorage``,同时 ``state`` 也会更新, 不传参数则等同于 ``removeItem`` | (Val?: T) => void | 30 | 31 | ## Example 32 | 33 | 34 | 35 | ## Code 36 | 37 | ```vue 38 | 56 | 57 | 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const registerComponents = require('./scripts/registerComponent') 2 | const gulp = require('gulp') 3 | 4 | exports.default = function () { 5 | gulp.watch( 6 | 'docs/.vitepress/components/**/*.vue', 7 | registerComponents 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: 'coverage', 4 | coverageProvider: 'v8', 5 | preset: 'ts-jest' 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vhook", 3 | "version": "1.0.0", 4 | "description": "Vue Composition Function", 5 | "main": "lib/index.js", 6 | "module": "esm/index.js", 7 | "author": "lmhcoding", 8 | "private": false, 9 | "sideEffects": false, 10 | "files": [ 11 | "lib/", 12 | "esm/" 13 | ], 14 | "keywords": [ 15 | "vue3", 16 | "composition api" 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https//github.com/lmhcoding/vhook" 22 | }, 23 | "homepage": "https://github.com/lmhcoding/vhook#readme", 24 | "bugs": { 25 | "url": "https://github.com/lmhcoding/vhook/issues" 26 | }, 27 | "types": "lib/index.d.ts", 28 | "typings": "lib/index.d.ts", 29 | "scripts": { 30 | "cm": "cz", 31 | "lint": "eslint {src,tests}/**/*.ts", 32 | "lint:fix": "npm run lint --fix", 33 | "lint:types": "tsc --noEmit", 34 | "lint:prettier": "prettier --write src/**/*.ts", 35 | "build:cjs": "tsc", 36 | "build:es": "tsc -m esNext --outDir esm", 37 | "build": "npm-run-all build:*", 38 | "clean": "rimraf lib", 39 | "release": "semantic-release", 40 | "test": "jest", 41 | "docs": "npm-run-all -p register docs:dev", 42 | "docs:dev": "vitepress dev docs", 43 | "docs:build": "vitepress build docs", 44 | "docs:deploy": "gh-pages -d ./docs/.vitepress/dist", 45 | "register": "gulp" 46 | }, 47 | "peerDependencies": { 48 | "vue": "^3.0.0" 49 | }, 50 | "devDependencies": { 51 | "@commitlint/cli": "^11.0.0", 52 | "@commitlint/config-conventional": "^11.0.0", 53 | "@semantic-release/changelog": "^5.0.1", 54 | "@semantic-release/git": "^9.0.0", 55 | "@semantic-release/npm": "^7.0.6", 56 | "@types/jest": "^26.0.14", 57 | "@types/lodash.throttle": "^4.1.6", 58 | "@types/throttle-debounce": "^2.1.0", 59 | "@typescript-eslint/eslint-plugin": "^4.1.1", 60 | "@typescript-eslint/parser": "^4.1.1", 61 | "@vue/test-utils": "^2.0.0-beta.5", 62 | "commitizen": "^4.2.1", 63 | "cz-conventional-changelog": "3.3.0", 64 | "eslint": "^7.9.0", 65 | "eslint-config-standard": "^14.1.1", 66 | "eslint-plugin-import": "^2.22.0", 67 | "eslint-plugin-node": "^11.1.0", 68 | "eslint-plugin-promise": "^4.2.1", 69 | "eslint-plugin-standard": "^4.0.1", 70 | "eslint-plugin-vue": "^6.2.2", 71 | "gh-pages": "^3.1.0", 72 | "globby": "^11.0.1", 73 | "gulp": "^4.0.2", 74 | "husky": "^4.3.0", 75 | "jest": "^26.4.2", 76 | "lint-staged": "^10.3.0", 77 | "lodash.camelcase": "^4.3.0", 78 | "lodash.upperfirst": "^4.3.1", 79 | "npm-run-all": "^4.1.5", 80 | "prettier": "^2.1.2", 81 | "rimraf": "^3.0.2", 82 | "semantic-release": "^17.1.1", 83 | "ts-jest": "^26.3.0", 84 | "ts-node": "^9.0.0", 85 | "typescript": "^4.0.2", 86 | "vitepress": "^0.6.0" 87 | }, 88 | "config": { 89 | "commitizen": { 90 | "path": "./node_modules/cz-conventional-changelog" 91 | } 92 | }, 93 | "husky": { 94 | "hooks": { 95 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 96 | "pre-commit": "lint-staged" 97 | } 98 | }, 99 | "lint-staged": { 100 | "src/**/**/*.ts": [ 101 | "eslint --fix", 102 | "prettier --write", 103 | "git add" 104 | ] 105 | }, 106 | "dependencies": { 107 | "throttle-debounce": "^2.3.0", 108 | "tslib": "^2.0.1", 109 | "vue": "^3.0.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@semantic-release/commit-analyzer', 4 | '@semantic-release/release-notes-generator', 5 | [ 6 | '@semantic-release/changelog', 7 | { 8 | changelogFile: 'CHANGELOG.md' 9 | } 10 | ], 11 | '@semantic-release/npm', 12 | '@semantic-release/github', 13 | [ 14 | '@semantic-release/git', 15 | { 16 | assets: [ 17 | 'src', 18 | 'lib', 19 | 'esm', 20 | 'package.json', 21 | 'yarn.lock' 22 | ] 23 | } 24 | ] 25 | ], 26 | branches: [ 27 | 'master' 28 | ], 29 | verifyConditions: [ 30 | '@semantic-release/changelog', 31 | '@semantic-release/npm', 32 | '@semantic-release/git' 33 | ], 34 | prepare: [ 35 | '@semantic-release/changelog', 36 | '@semantic-release/npm', 37 | '@semantic-release/git' 38 | ], 39 | repositoryUrl: 'https://github.com/lmhcoding/vhook' 40 | } 41 | -------------------------------------------------------------------------------- /scripts/registerComponent.js: -------------------------------------------------------------------------------- 1 | const globby = require('globby') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const prettier = require('prettier') 5 | const upperFirst = require('lodash.upperfirst') 6 | const camelCase = require('lodash.camelcase') 7 | 8 | function normalizeName (file) { 9 | return upperFirst(camelCase(file)) 10 | } 11 | 12 | function importTemplate (file) { 13 | const comp = normalizeName(file) 14 | return `const ${comp} = defineAsyncComponent(() => import('./${file}.vue'))` 15 | } 16 | 17 | function registerTemplate (file) { 18 | const comp = normalizeName(file) 19 | return `app.component('${comp}', ${comp})` 20 | } 21 | 22 | function generateRegisterFn (importStatement, registerStatement) { 23 | return ` 24 | import {defineAsyncComponent} from 'vue' 25 | 26 | ${importStatement} 27 | 28 | export default function registerComponent (app) { 29 | ${registerStatement} 30 | } 31 | ` 32 | } 33 | 34 | function format (code) { 35 | const prettierConfigPath = '../.prettierrc' 36 | return new Promise((resolve, reject) => { 37 | fs.readFile( 38 | path.resolve(__dirname, prettierConfigPath), 39 | (err, data) => { 40 | if (err) { 41 | reject(err) 42 | } 43 | const defaultConfig = JSON.parse(data.toString()) 44 | const result = prettier.format(code, { 45 | ...defaultConfig, 46 | parser: 'babel' 47 | }) 48 | resolve(result) 49 | } 50 | ) 51 | }) 52 | } 53 | 54 | const viteprssDir = 'docs/.vitepress' 55 | const componentDir = 'components' 56 | const outputPath = 'index.js' 57 | 58 | async function registerComponents () { 59 | const dir = path.join(process.cwd(), viteprssDir, componentDir) 60 | const files = await globby('**/*.vue', { 61 | cwd: dir 62 | }) 63 | const importStatement = files 64 | .map(file => importTemplate(file.replace(/\.vue/, ''))) 65 | .join('\n') 66 | const registerStatement = files 67 | .map(file => registerTemplate(file.replace(/\.vue/, ''))) 68 | .join('\n') 69 | let code = generateRegisterFn(importStatement, registerStatement) 70 | code = await format(code) 71 | fs.writeFileSync( 72 | path.resolve(dir, outputPath), 73 | code, 74 | { encoding: 'utf-8' } 75 | ) 76 | } 77 | 78 | module.exports = registerComponents 79 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTitle' 2 | export * from './useEvent' 3 | export * from './useResize' 4 | export * from './useScroll' 5 | export * from './useScrollRef' 6 | export * from './useWindowScroll' 7 | export * from './useEventRef' 8 | export * from './useToggle' 9 | export * from './useBoolean' 10 | export * from './useStorage' 11 | export * from './useLocalStorage' 12 | export * from './useSessionStorage' 13 | export * from './useLifecycles' 14 | export * from './useInterval' 15 | export * from './useHash' 16 | export * from './useHistory' 17 | export * from './useTimeout' 18 | export * from './useTimeoutFn' 19 | export * from './useDebounce' 20 | export * from './useDebounceFn' 21 | -------------------------------------------------------------------------------- /src/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useToggle } from './useToggle' 3 | 4 | interface IBoolean { 5 | state: Ref 6 | setTrue: () => void 7 | setFalse: () => void 8 | toggle: (next?: boolean) => void 9 | } 10 | 11 | export function useBoolean(defaultValue: boolean = false): IBoolean { 12 | const { state, toggle } = useToggle(defaultValue) 13 | 14 | return { 15 | state, 16 | toggle, 17 | setTrue: () => toggle(true), 18 | setFalse: () => toggle(false) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { customRef, getCurrentInstance, onUnmounted } from 'vue' 2 | 3 | export function useDebounce(value: T, delay = 200) { 4 | let timer: any 5 | const clear = () => { 6 | if (timer) { 7 | clearTimeout(timer) 8 | } 9 | } 10 | if (getCurrentInstance()) { 11 | onUnmounted(() => { 12 | clear() 13 | }) 14 | } 15 | return customRef((tracker, trigger) => ({ 16 | get() { 17 | tracker() 18 | return value 19 | }, 20 | set(val: T) { 21 | clear() 22 | timer = setTimeout(() => { 23 | value = val 24 | timer = null 25 | trigger() 26 | }, delay) 27 | } 28 | })) 29 | } 30 | -------------------------------------------------------------------------------- /src/useDebounceFn.ts: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue' 2 | import { useDebounce } from './useDebounce' 3 | 4 | export function useDebounceFn any>(fn: T, delay = 200) { 5 | const debounceValue = useDebounce(0, delay) 6 | let params: Parameters 7 | 8 | watch( 9 | debounceValue, 10 | () => { 11 | fn(...params) 12 | }, 13 | { flush: 'sync' } 14 | ) 15 | return function (...rest: Parameters) { 16 | params = rest 17 | debounceValue.value++ 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/useEvent.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-redeclare */ 2 | import { Target, getTarget } from './util' 3 | import { useLifecycles } from './useLifecycles' 4 | import { isRef, watchEffect } from 'vue' 5 | 6 | export interface WindowEventHandler { 7 | (this: Window, e: WindowEventMap[T]): any 8 | } 9 | 10 | export interface DocumentEventHandler { 11 | (this: Document, e: DocumentEventMap[T]): any 12 | } 13 | 14 | export type HandlerOptions = boolean | AddEventListenerOptions 15 | export type DocumentEvents = keyof DocumentEventMap 16 | export type WindowEvents = keyof WindowEventMap 17 | 18 | function registerEvent( 19 | target: Target, 20 | event: string, 21 | cb: EventListenerOrEventListenerObject, 22 | options?: HandlerOptions 23 | ) { 24 | const eventTarget = getTarget(target) 25 | if (eventTarget) { 26 | eventTarget.addEventListener(event, cb, options) 27 | } 28 | return eventTarget 29 | } 30 | 31 | export interface IEventTarget { 32 | readonly value: EventTarget | null 33 | } 34 | 35 | export type IEventResult = [IEventTarget, () => void] 36 | 37 | export function useEvent( 38 | event: T, 39 | handler: WindowEventHandler, 40 | options?: HandlerOptions 41 | ): IEventResult 42 | export function useEvent( 43 | event: T, 44 | handler: DocumentEventHandler, 45 | options?: HandlerOptions, 46 | target?: Target 47 | ): IEventResult 48 | export function useEvent( 49 | event: T, 50 | handler: DocumentEventHandler, 51 | options?: HandlerOptions, 52 | target?: string 53 | ): IEventResult 54 | export function useEvent( 55 | event: string, 56 | cb: EventListenerOrEventListenerObject, 57 | options?: HandlerOptions, 58 | target: Target = window 59 | ) { 60 | if (!event || !cb) { 61 | return 62 | } 63 | let eventTarget: EventTarget | null = null 64 | function register() { 65 | eventTarget = registerEvent(target, event, cb, options) 66 | } 67 | function clear() { 68 | if (eventTarget) { 69 | eventTarget.removeEventListener(event, cb, options) 70 | eventTarget = null 71 | } 72 | } 73 | useLifecycles( 74 | () => { 75 | if (isRef(target)) { 76 | watchEffect( 77 | (onInvalidate) => { 78 | register() 79 | onInvalidate(() => { 80 | clear() 81 | }) 82 | }, 83 | { flush: 'sync' } 84 | ) 85 | } else { 86 | register() 87 | } 88 | }, 89 | () => { 90 | clear() 91 | } 92 | ) 93 | const targetDom = { 94 | get value() { 95 | return eventTarget 96 | } 97 | } 98 | return [targetDom, clear] 99 | } 100 | -------------------------------------------------------------------------------- /src/useEventRef.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from 'vue' 2 | import { useEvent, DocumentEventHandler, DocumentEvents, HandlerOptions } from './useEvent' 3 | 4 | export function useEventRef( 5 | event: T, 6 | handler: DocumentEventHandler, 7 | options?: HandlerOptions 8 | ): [Ref, () => void] { 9 | const target: Ref = ref(null) 10 | const [, clear] = useEvent(event, handler, options, target) 11 | return [target, clear] 12 | } 13 | -------------------------------------------------------------------------------- /src/useHash.ts: -------------------------------------------------------------------------------- 1 | import { ref, readonly, DeepReadonly, Ref } from 'vue' 2 | import { useEvent } from './useEvent' 3 | 4 | export interface IHashResult { 5 | hash: DeepReadonly> 6 | setHash: (hash: string) => void 7 | } 8 | 9 | export function useHash(): IHashResult { 10 | const state = ref(window.location.hash) 11 | 12 | const setHash = (hash: string) => { 13 | if (hash !== state.value) { 14 | window.location.hash = hash 15 | } 16 | } 17 | 18 | const onHashChange = () => { 19 | state.value = window.location.hash 20 | } 21 | useEvent('hashchange', onHashChange) 22 | return { 23 | hash: readonly(state), 24 | setHash 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/useHistory.ts: -------------------------------------------------------------------------------- 1 | import { reactive, toRefs, ToRefs, UnwrapRef } from 'vue' 2 | import { useEvent } from './useEvent' 3 | import { def, isClient } from './util' 4 | 5 | type PathMethod = Extract 6 | 7 | function patchHistoryMethod(method: PathMethod): void { 8 | const history = window.history 9 | const origin = history[method] 10 | 11 | def( 12 | history, 13 | method, 14 | function (this: History, state: any) { 15 | const result = origin.apply(this, (arguments as unknown) as Parameters) 16 | const event: any = new Event(method.toLowerCase()) 17 | event.state = state 18 | window.dispatchEvent(event) 19 | return result 20 | }, 21 | true 22 | ) 23 | } 24 | 25 | if (isClient) { 26 | patchHistoryMethod('pushState') 27 | patchHistoryMethod('replaceState') 28 | } 29 | 30 | export interface IHistoryState { 31 | state: any 32 | hash: string 33 | search: any 34 | host: string 35 | hostname: string 36 | href: string 37 | origin: string 38 | pathname: string 39 | port: string 40 | protocol: string 41 | } 42 | 43 | function buildState(): IHistoryState { 44 | const { state } = window.history 45 | const { hash, search, host, hostname, href, origin, pathname, port, protocol } = window.location 46 | return { 47 | state, 48 | hash, 49 | search, 50 | host, 51 | hostname, 52 | href, 53 | origin, 54 | pathname, 55 | port, 56 | protocol 57 | } 58 | } 59 | 60 | export interface IHistoryResult extends ToRefs { 61 | clear: () => void 62 | } 63 | 64 | export function useHistory(): IHistoryResult { 65 | const state: UnwrapRef = reactive(buildState()) 66 | const [, clearPopStateListener] = useEvent('popstate', buildState) 67 | const [, clearPushStateListener] = useEvent('pushstate' as any, buildState) 68 | const [, clearReplaceStateListener] = useEvent('replacestate' as any, buildState) 69 | const clear = () => { 70 | clearPopStateListener() 71 | clearPushStateListener() 72 | clearReplaceStateListener() 73 | } 74 | return { 75 | ...toRefs(state), 76 | clear 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useLifecycles } from './useLifecycles' 2 | 3 | export function useInterval(fn: Function, delay: number, immediate = false) { 4 | let interval: number | null = null 5 | const clear = () => { 6 | if (interval) { 7 | clearInterval(interval) 8 | interval = null 9 | } 10 | } 11 | const start = () => { 12 | if (!interval) { 13 | interval = setInterval(fn, delay) 14 | } 15 | } 16 | useLifecycles( 17 | () => { 18 | if (fn) { 19 | immediate && fn() 20 | start() 21 | } 22 | }, 23 | () => { 24 | clear() 25 | } 26 | ) 27 | return [clear, start] 28 | } 29 | -------------------------------------------------------------------------------- /src/useLifecycles.ts: -------------------------------------------------------------------------------- 1 | import { tryOnMounted, tryOnUnmounted } from './util' 2 | 3 | interface Callback { 4 | (): any 5 | } 6 | 7 | export function useLifecycles(mountedCb: Callback, unmountCb: Callback): void { 8 | tryOnMounted(mountedCb) 9 | tryOnUnmounted(unmountCb) 10 | } 11 | -------------------------------------------------------------------------------- /src/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useStorage, IStorage } from './useStorage' 2 | 3 | export function useLocalStorage(key: string, val: T): IStorage { 4 | return useStorage(key, val) 5 | } 6 | -------------------------------------------------------------------------------- /src/useResize.ts: -------------------------------------------------------------------------------- 1 | import { DeepReadonly, readonly, Ref, ref } from 'vue' 2 | import { useEvent } from './useEvent' 3 | import { useDebounceFn } from './useDebounceFn' 4 | 5 | export interface ResizeHandler { 6 | (this: Window, e: WindowEventMap['resize']): any 7 | } 8 | 9 | export interface IResizeState { 10 | width: DeepReadonly> 11 | height: DeepReadonly> 12 | } 13 | 14 | export function useResize(handler: ResizeHandler | null = null, delay = 200): IResizeState { 15 | const width = ref(window.innerWidth) 16 | const height = ref(window.innerHeight) 17 | let cb: ResizeHandler = function (this: Window, e: WindowEventMap['resize']) { 18 | handler && handler.call(this, e) 19 | width.value = window.innerWidth 20 | height.value = window.innerHeight 21 | } 22 | if (delay) { 23 | cb = useDebounceFn(cb, delay) 24 | } 25 | useEvent('resize', cb) 26 | return { 27 | width: readonly(width), 28 | height: readonly(height) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { DeepReadonly, readonly, ref, Ref } from 'vue' 2 | import { useEvent } from './useEvent' 3 | import { throttle } from 'throttle-debounce' 4 | 5 | export type IScrollResult = [DeepReadonly>, DeepReadonly>, () => void] 6 | 7 | export function useScroll(target: string | Element | Ref, delay = 200): IScrollResult { 8 | const x = ref(0) 9 | const y = ref(0) 10 | let cb = () => { 11 | if (eventTarget.value) { 12 | x.value = ((eventTarget.value as unknown) as Element).scrollLeft 13 | y.value = ((eventTarget.value as unknown) as Element).scrollTop 14 | } 15 | } 16 | if (delay) { 17 | cb = throttle(delay, cb) 18 | } 19 | const [eventTarget, clear] = useEvent('scroll', cb, { capture: false, passive: true }, target) 20 | return [readonly(x), readonly(y), clear] 21 | } 22 | -------------------------------------------------------------------------------- /src/useScrollRef.ts: -------------------------------------------------------------------------------- 1 | import { DeepReadonly, ref, Ref } from 'vue' 2 | import { useScroll } from './useScroll' 3 | 4 | type ScrollState = DeepReadonly> 5 | 6 | export type IScrollRefResult = [Ref, ScrollState, ScrollState, () => void] 7 | 8 | export function useScrollRef(): IScrollRefResult { 9 | const target: Ref = ref(null) 10 | const state = useScroll(target) 11 | return [target, ...state] 12 | } 13 | -------------------------------------------------------------------------------- /src/useSessionStorage.ts: -------------------------------------------------------------------------------- 1 | import { useStorage, IStorage } from './useStorage' 2 | 3 | export function useSessionStorage(key: string, val: T): IStorage { 4 | return useStorage(key, val, sessionStorage) 5 | } 6 | -------------------------------------------------------------------------------- /src/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { DeepReadonly, readonly, Ref, ref } from 'vue' 2 | 3 | function getDefaultValue(storage: Storage, key: string, defaultValue: T) { 4 | const val = storage.getItem(key) 5 | if (val) { 6 | try { 7 | return JSON.parse(val) 8 | } catch { 9 | // do nothing 10 | } 11 | } 12 | return defaultValue 13 | } 14 | 15 | export type IStorage = [DeepReadonly>, (val?: T) => void] 16 | 17 | export function useStorage(key: string, val: T, storage: Storage = localStorage): IStorage { 18 | const value = getDefaultValue(storage, key, val) 19 | const state: Ref = ref(null) 20 | const setState = (val?: T) => { 21 | if (val === undefined) { 22 | storage.removeItem(key) 23 | state.value = null 24 | } else { 25 | storage.setItem(key, JSON.stringify(val)) 26 | state.value = val 27 | } 28 | } 29 | setState(value) 30 | return [readonly(state), setState] 31 | } 32 | -------------------------------------------------------------------------------- /src/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { ref, readonly, DeepReadonly, Ref } from 'vue' 2 | import { tryOnUnmounted } from './util' 3 | 4 | export interface ITimeoutResult { 5 | ready: DeepReadonly> 6 | start: () => void 7 | stop: () => void 8 | } 9 | 10 | export function useTimeout(delay = 1000, immediate = true): ITimeoutResult { 11 | const ready = ref(false) 12 | let timer: any 13 | const stop = () => { 14 | if (timer) { 15 | clearTimeout(timer) 16 | timer = null 17 | } 18 | } 19 | const initTimeout = () => { 20 | ready.value = false 21 | stop() 22 | timer = window.setTimeout(() => { 23 | ready.value = true 24 | timer = null 25 | }, delay) 26 | } 27 | immediate && initTimeout() 28 | 29 | tryOnUnmounted(() => { 30 | stop() 31 | }) 32 | 33 | return { 34 | ready: readonly(ready), 35 | start: () => initTimeout(), 36 | stop 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/useTimeoutFn.ts: -------------------------------------------------------------------------------- 1 | import { watch, WatchStopHandle } from 'vue' 2 | import { useTimeout } from './useTimeout' 3 | 4 | export function useTimeoutFn(fn: Function, delay = 1000, immediate = true, clearEffectWhenStop = false) { 5 | const { start, ready, stop } = useTimeout(delay, immediate) 6 | let stopEffect: WatchStopHandle | undefined 7 | const startEffect = () => { 8 | stopEffect = watch( 9 | ready, 10 | (hasReady) => { 11 | hasReady && fn() 12 | }, 13 | { flush: 'sync' } 14 | ) 15 | } 16 | const _stop = () => { 17 | clearEffectWhenStop && stopEffect!() 18 | stop() 19 | } 20 | startEffect() 21 | return { 22 | start: () => { 23 | clearEffectWhenStop && startEffect() 24 | start() 25 | }, 26 | stop: _stop 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { ref, watchEffect } from 'vue' 2 | import { tryOnUnmounted } from './util' 3 | 4 | export function useTitle(title: string, restoreOnUnMount = false) { 5 | const cache = document.title 6 | const titleRef = ref(title) 7 | watchEffect(() => { 8 | document.title = titleRef.value 9 | }) 10 | if (restoreOnUnMount) { 11 | tryOnUnmounted(() => { 12 | document.title = cache 13 | }) 14 | } 15 | const setTitle = (title: string) => { 16 | titleRef.value = title 17 | } 18 | return setTitle 19 | } 20 | -------------------------------------------------------------------------------- /src/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, DeepReadonly, readonly } from 'vue' 2 | 3 | export type ToggleParamType = string | number | boolean | undefined | null 4 | 5 | export interface ToggleReturn { 6 | state: DeepReadonly> 7 | toggle: (next?: boolean) => void 8 | setDefault: () => void 9 | setRight: () => void 10 | } 11 | 12 | export interface ITwoType 13 | extends Omit { 14 | state: DeepReadonly> 15 | toggle: (next?: T | U) => void 16 | } 17 | 18 | export interface IToggle { 19 | (): ToggleReturn 20 | (defaultValue: boolean): ToggleReturn 21 | ( 22 | defaultValue: T, 23 | resetValue?: U 24 | ): ITwoType 25 | } 26 | 27 | export const useToggle: IToggle = < 28 | T extends ToggleParamType = ToggleParamType, 29 | U extends ToggleParamType = ToggleParamType 30 | > ( 31 | defaultValue: T = false as T, 32 | reverseValue?: U 33 | ) => { 34 | const state = ref(defaultValue) as Ref 35 | const reverseTo = (reverseValue === undefined ? !defaultValue : reverseValue) as T | U 36 | 37 | const toggle = (next?: T | U) => { 38 | if (next !== undefined) { 39 | state.value = next 40 | } else { 41 | state.value = state.value === defaultValue ? reverseTo : defaultValue 42 | } 43 | } 44 | const setDefault = () => { 45 | state.value = defaultValue 46 | } 47 | const setRight = () => { 48 | state.value = reverseTo 49 | } 50 | return { 51 | state: readonly(state), 52 | setDefault, 53 | setRight, 54 | toggle 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/useWindowScroll.ts: -------------------------------------------------------------------------------- 1 | import { ref, readonly, DeepReadonly, Ref } from 'vue' 2 | import { useEvent } from './useEvent' 3 | import { throttle } from 'throttle-debounce' 4 | import { isClient } from './util' 5 | 6 | export interface IWindowScrollState { 7 | x: DeepReadonly> 8 | y: DeepReadonly> 9 | clear: () => void 10 | } 11 | 12 | export function useWindowScroll(delay = 200): IWindowScrollState { 13 | const x = ref(isClient ? window.scrollX : 0) 14 | const y = ref(isClient ? window.scrollY : 0) 15 | let cb = () => { 16 | x.value = window.scrollX 17 | y.value = window.scrollY 18 | } 19 | if (delay) { 20 | cb = throttle(delay, cb) 21 | } 22 | const [, clear] = useEvent('scroll', cb, { 23 | passive: true, 24 | capture: false 25 | }) 26 | return { 27 | x: readonly(x), 28 | y: readonly(y), 29 | clear 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, isRef, onMounted, onUnmounted, Ref } from 'vue' 2 | 3 | export function def(obj: T, key: keyof T, val: any, enumerable?: boolean) { 4 | Object.defineProperty(obj, key, { 5 | value: val, 6 | enumerable: !!enumerable, 7 | writable: true, 8 | configurable: true 9 | }) 10 | } 11 | 12 | export type Target = Ref | EventTarget | string 13 | 14 | export function getTarget(target: Target): EventTarget { 15 | if (typeof target === 'string') { 16 | const dom = document.querySelector(target) 17 | if (!dom && process.env.NODE_ENV !== 'production') { 18 | console.error('target is not found') 19 | throw Error(`target of selector ${target} is not found`) 20 | } 21 | return dom! 22 | } 23 | if (isRef(target)) { 24 | return target.value! 25 | } 26 | return target 27 | } 28 | 29 | export function tryOnMounted(cb: () => any): void { 30 | const instance = getCurrentInstance() 31 | if (instance) { 32 | if (instance?.isMounted) { 33 | cb() 34 | } else { 35 | onMounted(cb) 36 | } 37 | } 38 | } 39 | 40 | export function tryOnUnmounted(cb: () => any): void { 41 | if (getCurrentInstance()) { 42 | onUnmounted(cb) 43 | } 44 | } 45 | 46 | export const isClient = typeof window !== 'undefined' 47 | -------------------------------------------------------------------------------- /tests/useBoolean.test.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useBoolean } from '../src/useBoolean' 3 | 4 | describe('test useBoolean', () => { 5 | let state: Ref 6 | let toggle: (next?: boolean) => void 7 | let setTrue: () => void 8 | let setFalse: () => void 9 | beforeEach(() => { 10 | const result = useBoolean() 11 | state = result.state 12 | toggle = result.toggle 13 | setTrue = result.setTrue 14 | setFalse = result.setFalse 15 | }) 16 | 17 | test('init State should be false', () => { 18 | expect(state.value).toBe(false) 19 | }) 20 | 21 | test('state.value should be toggled after invoking toggle with none params', () => { 22 | expect(state.value).toBe(false) 23 | toggle() 24 | expect(state.value).toBe(true) 25 | toggle() 26 | expect(state.value).toBe(false) 27 | }) 28 | test('state.value should be set to specify value after invoking toggle with one parameter', () => { 29 | expect(state.value).toBe(false) 30 | toggle(true) 31 | expect(state.value).toBe(true) 32 | toggle(false) 33 | expect(state.value).toBe(false) 34 | }) 35 | test('state.value should be true after invoking setTrue', () => { 36 | setTrue() 37 | expect(state.value).toBe(true) 38 | }) 39 | test('state.value should be false after invoking setFalse', () => { 40 | toggle(true) 41 | expect(state.value).toBe(true) 42 | setFalse() 43 | expect(state.value).toBe(false) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/useDebounce.test.ts: -------------------------------------------------------------------------------- 1 | import { useDebounce } from '../src/useDebounce' 2 | import { invokeHook } from './util' 3 | 4 | beforeEach(() => { 5 | jest.useFakeTimers() 6 | }) 7 | 8 | afterEach(() => { 9 | jest.useRealTimers() 10 | jest.clearAllMocks() 11 | }) 12 | 13 | test('initial debounceValue should be equal to the params value', () => { 14 | const debounceValue = useDebounce(4) 15 | expect(debounceValue.value).toBe(4) 16 | }) 17 | 18 | test('setTimeout should be called with default delay when debounceValue changed', () => { 19 | const debounceValue = useDebounce(4) 20 | debounceValue.value = 5 21 | expect(setTimeout).toHaveBeenCalledTimes(1) 22 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 200) 23 | }) 24 | 25 | test('debounceValue should changed when setTimeout callback was invoked', () => { 26 | const debounceValue = useDebounce(4) 27 | debounceValue.value = 5 28 | expect(setTimeout).toHaveBeenCalledTimes(1) 29 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 200) 30 | jest.advanceTimersByTime(200) 31 | expect(debounceValue.value).toBe(5) 32 | }) 33 | 34 | test('timer will be cleared and recrated when updating debounceValue less than 200s', () => { 35 | const debounceValue = useDebounce(4) 36 | debounceValue.value = 5 37 | debounceValue.value = 6 38 | expect(debounceValue.value).toBe(4) 39 | expect(clearTimeout).toHaveBeenCalledTimes(1) 40 | expect(setTimeout).toHaveBeenCalledTimes(2) 41 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 200) 42 | jest.advanceTimersByTime(200) 43 | expect(debounceValue.value).toBe(6) 44 | }) 45 | 46 | test('timer should be cleared when component is unmounted and timer is not null', () => { 47 | const wrapper = invokeHook(() => { 48 | const debounceValue = useDebounce(4) 49 | debounceValue.value = 5 50 | }) 51 | wrapper.unmount() 52 | expect(clearTimeout).toHaveBeenCalledTimes(1) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/useDebounceFn.test.ts: -------------------------------------------------------------------------------- 1 | import { useDebounceFn } from '../src/useDebounceFn' 2 | 3 | let callback: (...rest: any[]) => any 4 | const identity = (v: string) => v 5 | 6 | beforeEach(() => { 7 | jest.useFakeTimers() 8 | callback = jest.fn((s: string) => { 9 | identity(s) 10 | }) 11 | }) 12 | 13 | afterEach(() => { 14 | jest.useRealTimers() 15 | jest.clearAllMocks() 16 | }) 17 | 18 | test('callback should not be called after invoking useDebounceFn', () => { 19 | useDebounceFn(callback!) 20 | expect(callback!).not.toBeCalled() 21 | }) 22 | 23 | test('setTimeout should be called after invoking the function returned by useDebounceFn', () => { 24 | const debounceFn = useDebounceFn(callback!) 25 | debounceFn() 26 | expect(setTimeout).toHaveBeenCalledTimes(1) 27 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 200) 28 | }) 29 | 30 | test('timer should be cleared when calling the function returned by useDebounceFn in 200s', () => { 31 | const debounceFn = useDebounceFn(callback!) 32 | debounceFn() 33 | debounceFn() 34 | expect(clearTimeout).toHaveBeenCalledTimes(1) 35 | expect(setTimeout).toHaveBeenCalledTimes(2) 36 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 200) 37 | }) 38 | 39 | test('callback should be called when timeout', async () => { 40 | const debounceFn = useDebounceFn(callback!) 41 | debounceFn('1') 42 | debounceFn('2') 43 | jest.advanceTimersByTime(200) 44 | expect(callback!).toHaveBeenCalledTimes(1) 45 | expect(callback!).toHaveBeenCalledWith('2') 46 | }) 47 | -------------------------------------------------------------------------------- /tests/useEvent.test.ts: -------------------------------------------------------------------------------- 1 | import { VueWrapper } from '@vue/test-utils' 2 | import { nextTick, onMounted, Ref, ref } from 'vue' 3 | import { IEventTarget, useEvent } from '../src/useEvent' 4 | import { Target } from '../src/util' 5 | import { invokeHook, patchEventTarget } from './util' 6 | 7 | const [add, remove] = patchEventTarget() 8 | const handler = jest.fn() 9 | 10 | function testUseEvent(description: string, targetFn: () => Target, isRef = false) { 11 | describe(description, () => { 12 | let wrapper: VueWrapper 13 | let clear: () => void 14 | let target: IEventTarget 15 | let testRef: Ref 16 | beforeEach(() => { 17 | if (isRef) { 18 | testRef = targetFn() as Ref 19 | } 20 | wrapper = invokeHook(() => { 21 | onMounted(() => { 22 | [target, clear] = useEvent( 23 | 'click', 24 | handler, 25 | true, 26 | !isRef ? targetFn() : testRef 27 | ) 28 | }) 29 | return { 30 | test: testRef 31 | } 32 | }) 33 | }) 34 | test('target should be equal to the target parameter', () => { 35 | expect(target.value).toEqual(document.getElementById('test')) 36 | }) 37 | test('addEventListener should be called after mounted', () => { 38 | expect(add).toHaveBeenCalledTimes(1) 39 | expect(add).toHaveBeenCalledWith('click', handler, true) 40 | }) 41 | test('callback should be called after firing an event', () => { 42 | const target = wrapper.find('#test') 43 | target.trigger('click') 44 | expect(handler).toHaveBeenCalledTimes(1) 45 | }) 46 | test('removeEventListener should be called after invoking clear', () => { 47 | clear() 48 | expect(remove).toHaveBeenCalledTimes(1) 49 | expect(remove).toHaveBeenCalledWith('click', handler, true) 50 | }) 51 | test('callback should not be called after invoking clear', () => { 52 | const target = wrapper.find('#test') 53 | clear() 54 | target.trigger('click') 55 | expect(handler).not.toBeCalled() 56 | }) 57 | test('target.value should be null after invoking clear', () => { 58 | clear() 59 | expect(target.value).toBeNull() 60 | }) 61 | test('event should be removed after unmounted', () => { 62 | const targetDiv = wrapper.find('#test') 63 | wrapper.unmount() 64 | expect(remove).toHaveBeenCalledTimes(1) 65 | expect(remove).toHaveBeenCalledWith('click', handler, true) 66 | targetDiv.trigger('click') 67 | expect(handler).not.toBeCalled() 68 | expect(target.value).toBeNull() 69 | }) 70 | if (isRef) { 71 | test('removeEventListener should be called when ref is manually set to null', async () => { 72 | const targetDiv = wrapper.find('#test') 73 | testRef.value = null 74 | await nextTick() 75 | expect(remove).toHaveBeenCalledTimes(1) 76 | expect(remove).toHaveBeenCalledWith('click', handler, true) 77 | targetDiv.trigger('click') 78 | expect(handler).not.toBeCalled() 79 | expect(target.value).toBeNull() 80 | }) 81 | } 82 | }) 83 | } 84 | 85 | testUseEvent( 86 | 'test useEvent when target is an Element', 87 | () => document.getElementById('test')! 88 | ) 89 | 90 | testUseEvent( 91 | 'test useEvent when target is a css selector', 92 | () => '#test' 93 | ) 94 | 95 | testUseEvent( 96 | 'test useEvent when target is a Ref', 97 | () => ref(null), 98 | true 99 | ) 100 | -------------------------------------------------------------------------------- /tests/useEventRef.test.ts: -------------------------------------------------------------------------------- 1 | import { VueWrapper } from '@vue/test-utils' 2 | import { useEventRef } from '../src/useEventRef' 3 | import { invokeHook, patchEventTarget } from './util' 4 | 5 | const [add, remove] = patchEventTarget() 6 | const handler = jest.fn() 7 | 8 | describe('test useEventRef', () => { 9 | let wrapper: VueWrapper 10 | let clear: () => void 11 | beforeEach(() => { 12 | wrapper = invokeHook(() => { 13 | const [target, clearFn] = useEventRef( 14 | 'click', 15 | handler, 16 | true 17 | ) 18 | clear = clearFn 19 | return { 20 | test: target 21 | } 22 | }) 23 | }) 24 | test('addEventListener should be called after mounted', () => { 25 | expect(add).toHaveBeenCalledTimes(1) 26 | expect(add).toHaveBeenCalledWith('click', handler, true) 27 | }) 28 | test('callback should be called after firing an event', () => { 29 | const target = wrapper.find('#test') 30 | target.trigger('click') 31 | expect(handler).toHaveBeenCalledTimes(1) 32 | }) 33 | test('removeEventListener should be called after invoking clear', () => { 34 | clear() 35 | expect(remove).toHaveBeenCalledTimes(1) 36 | expect(remove).toHaveBeenCalledWith('click', handler, true) 37 | }) 38 | test('callback should not be called after invoking clear', () => { 39 | const target = wrapper.find('#test') 40 | clear() 41 | target.trigger('click') 42 | expect(handler).not.toBeCalled() 43 | }) 44 | test('event should be removed after unmounted', () => { 45 | const targetDiv = wrapper.find('#test') 46 | wrapper.unmount() 47 | expect(remove).toHaveBeenCalledTimes(1) 48 | expect(remove).toHaveBeenCalledWith('click', handler, true) 49 | targetDiv.trigger('click') 50 | expect(handler).not.toBeCalled() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/useHash.test.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useHash } from '../src/useHash' 3 | 4 | let hash = '#test' 5 | const mockLocation = {} 6 | Object.defineProperty(mockLocation, 'hash', { 7 | get () { 8 | return hash 9 | }, 10 | set (val) { 11 | hash = val 12 | window.dispatchEvent(new HashChangeEvent('hashchange')) 13 | } 14 | }) 15 | Object.defineProperty(window, 'location', { 16 | value: mockLocation 17 | }) 18 | 19 | describe('test useHash', () => { 20 | let state: Ref 21 | let setHash: (hash: string) => void 22 | beforeEach(() => { 23 | const res = useHash() 24 | state = res.hash 25 | setHash = res.setHash 26 | }) 27 | test('initial state should be test', () => { 28 | expect(state.value).toBe('#test') 29 | }) 30 | test('hash should change after invoking setHash with new hash', () => { 31 | setHash('new') 32 | expect(window.location.hash).toBe('new') 33 | }) 34 | test('state.value should change after changing hash with window.location.hash', () => { 35 | setHash('new') 36 | expect(state.value).toBe('new') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/useInterval.test.ts: -------------------------------------------------------------------------------- 1 | import { useInterval } from '../src/useInterval' 2 | import { invokeHook } from './util' 3 | 4 | let callback: Function | undefined 5 | 6 | beforeEach(() => { 7 | callback = jest.fn() 8 | jest.useFakeTimers() 9 | }) 10 | 11 | afterEach(() => { 12 | jest.clearAllTimers() 13 | jest.useRealTimers() 14 | }) 15 | 16 | test('should init interval with delay = 500', () => { 17 | invokeHook(() => { 18 | useInterval(callback!, 500) 19 | }) 20 | expect(setInterval).toHaveBeenCalledTimes(1) 21 | expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 500) 22 | jest.advanceTimersByTime(500) 23 | expect(callback).toHaveBeenCalled() 24 | }) 25 | 26 | test('should clearInterval when unmounted', () => { 27 | const wrapper = invokeHook(() => { 28 | useInterval(callback!, 500) 29 | }) 30 | wrapper.unmount() 31 | expect(clearInterval).toHaveBeenCalledTimes(1) 32 | expect(clearInterval).toHaveBeenCalledWith(expect.any(Number)) 33 | jest.advanceTimersByTime(500) 34 | expect(callback).not.toBeCalled() 35 | }) 36 | 37 | test('interval should be cleared after invoking clear', () => { 38 | let clear: () => void 39 | invokeHook(() => { 40 | [clear] = useInterval(callback!, 500) 41 | }) 42 | clear!() 43 | expect(clearInterval).toHaveBeenCalledTimes(1) 44 | expect(clearInterval).toHaveBeenCalledWith(expect.any(Number)) 45 | }) 46 | 47 | test('interval will be start after invoking start', () => { 48 | let clear: () => void 49 | let start: () => void 50 | invokeHook(() => { 51 | const res = useInterval(callback!, 500) 52 | clear = res[0] 53 | start = res[1] 54 | }) 55 | clear!() 56 | expect(clearInterval).toHaveBeenCalledTimes(1) 57 | expect(clearInterval).toHaveBeenCalledWith(expect.any(Number)) 58 | start!() 59 | expect(setInterval).toHaveBeenCalled() 60 | expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 500) 61 | jest.advanceTimersByTime(500) 62 | expect(callback).toHaveBeenCalled() 63 | }) 64 | -------------------------------------------------------------------------------- /tests/useLifecycles.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { useLifecycles } from '../src/useLifecycles' 3 | 4 | describe('test useLifecycles', () => { 5 | test('callback should be called when mounted or unmounted', () => { 6 | const onMounted = jest.fn() 7 | const onUnmounted = jest.fn() 8 | const wrapper = mount({ 9 | template: '
test
', 10 | setup () { 11 | useLifecycles(onMounted, onUnmounted) 12 | } 13 | }) 14 | expect(onMounted).toHaveBeenCalledTimes(1) 15 | wrapper.unmount() 16 | expect(onUnmounted).toHaveBeenCalledTimes(1) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/useLocalStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useLocalStorage } from '../src/useLocalStorage' 3 | 4 | function testLocalStorage(val: any, newVal: any) { 5 | describe('test useLocalStorage when localStorage is localStorage and val is not an Object', () => { 6 | let state: Ref 7 | let setState: (val?: any) => void 8 | beforeEach(() => { 9 | [state, setState] = useLocalStorage('test', val) 10 | }) 11 | test('initial value of key test in localStorage is 1', () => { 12 | expect(localStorage.getItem('test')).toEqual(JSON.stringify(val)) 13 | expect(state.value).toEqual(val) 14 | }) 15 | test('value of key test in localStorage should change when invoking setState with new value', () => { 16 | setState(newVal) 17 | expect(localStorage.getItem('test')).toEqual(JSON.stringify(newVal)) 18 | expect(state.value).toEqual(newVal) 19 | }) 20 | test('key test in localStorage should be removed when invoking setState with no parameter or undefined', () => { 21 | setState() 22 | expect(localStorage.getItem('test')).toBeNull() 23 | expect(state.value).toBeNull() 24 | setState(val) 25 | expect(localStorage.getItem('test')).toEqual(JSON.stringify(val)) 26 | expect(state.value).toEqual(val) 27 | setState(undefined) 28 | expect(localStorage.getItem('test')).toBeNull() 29 | expect(state.value).toBeNull() 30 | }) 31 | }) 32 | } 33 | 34 | testLocalStorage('1', '2') 35 | testLocalStorage({ a: 1 }, { a: 2 }) 36 | -------------------------------------------------------------------------------- /tests/useResize.test.ts: -------------------------------------------------------------------------------- 1 | import { VueWrapper } from '@vue/test-utils' 2 | import { DeepReadonly, Ref } from 'vue' 3 | import { useResize, ResizeHandler } from '../src/useResize' 4 | import { invokeHook } from './util' 5 | 6 | let handler: Function 7 | let wrapper: VueWrapper 8 | let width: DeepReadonly> 9 | let height: DeepReadonly> 10 | 11 | function patchWindow () { 12 | let width = 200 13 | let height = 300 14 | const originAdd = window.addEventListener 15 | const originRemove = window.removeEventListener 16 | const originDispatch = window.dispatchEvent 17 | const addEventListener = jest.fn(function (event, cb, options) { 18 | originAdd.call(addEventListener.mock.instances[0], event, cb, options) 19 | }) 20 | const removeEventListener = jest.fn(function (event, cb, options) { 21 | originRemove.call(removeEventListener.mock.instances[0], event, cb, options) 22 | }) 23 | window.addEventListener = addEventListener 24 | window.removeEventListener = removeEventListener 25 | Object.defineProperties(window, { 26 | innerWidth: { 27 | get () { 28 | return width 29 | } 30 | }, 31 | innerHeight: { 32 | get () { 33 | return height 34 | } 35 | } 36 | }) 37 | const dispatchEvent = jest.fn(function (e: Event) { 38 | if (e.type === 'resize') { 39 | width = 1000 40 | height = 700 41 | originDispatch.call(dispatchEvent.mock.instances[0], e) 42 | } 43 | }) 44 | window.dispatchEvent = dispatchEvent as any 45 | return () => { 46 | width = 200 47 | height = 300 48 | } 49 | } 50 | 51 | const reset = patchWindow() 52 | 53 | function testResize (description: string, delay = 200) { 54 | describe(description, () => { 55 | beforeEach(() => { 56 | handler = jest.fn() 57 | jest.useFakeTimers() 58 | wrapper = invokeHook(() => { 59 | const res = useResize(handler as ResizeHandler, delay) 60 | width = res.width 61 | height = res.height 62 | }) 63 | }) 64 | 65 | afterEach(() => { 66 | jest.useRealTimers() 67 | jest.clearAllMocks() 68 | }) 69 | 70 | afterEach(() => { 71 | reset() 72 | }) 73 | test('window.addEventListener should be called when mounted', () => { 74 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 75 | expect(window.addEventListener).toHaveBeenCalledWith('resize', expect.any(Function), undefined) 76 | }) 77 | 78 | test('width/height should not be undefined', () => { 79 | expect(width).toBeDefined() 80 | expect(height).toBeDefined() 81 | expect(width.value).toBe(200) 82 | expect(height.value).toBe(300) 83 | }) 84 | 85 | test('width / height should be changed when resized', () => { 86 | window.dispatchEvent(new Event('resize')) 87 | delay && jest.advanceTimersByTime(200) 88 | expect(width.value).toBe(1000) 89 | expect(height.value).toBe(700) 90 | }) 91 | 92 | test('callback should be called when resized', () => { 93 | window.dispatchEvent(new Event('resize')) 94 | delay && jest.advanceTimersByTime(200) 95 | expect(handler).toBeCalled() 96 | }) 97 | 98 | test('window.removeEventListener should be called when unmounted', () => { 99 | wrapper.unmount() 100 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 101 | expect(window.removeEventListener).toHaveBeenCalledWith('resize', expect.any(Function), undefined) 102 | }) 103 | }) 104 | } 105 | 106 | testResize('test useResize with debounce') 107 | testResize('test useResize without debounce') 108 | -------------------------------------------------------------------------------- /tests/useSessionStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useSessionStorage } from '../src/useSessionStorage' 3 | 4 | function testSessionStorage(val: any, newVal: any) { 5 | describe('test useLocalStorage when sessionStorage is localStorage and val is not an Object', () => { 6 | let state: Ref 7 | let setState: (val?: any) => void 8 | beforeEach(() => { 9 | [state, setState] = useSessionStorage('test', val) 10 | }) 11 | test('initial value of key test in sessionStorage is 1', () => { 12 | expect(sessionStorage.getItem('test')).toEqual(JSON.stringify(val)) 13 | expect(state.value).toEqual(val) 14 | }) 15 | test('value of key test in sessionStorage should change when invoking setState with new value', () => { 16 | setState(newVal) 17 | expect(sessionStorage.getItem('test')).toEqual(JSON.stringify(newVal)) 18 | expect(state.value).toEqual(newVal) 19 | }) 20 | test('key test in sessionStorage should be removed when invoking setState with no parameter or undefined', () => { 21 | setState() 22 | expect(sessionStorage.getItem('test')).toBeNull() 23 | expect(state.value).toBeNull() 24 | setState(val) 25 | expect(sessionStorage.getItem('test')).toEqual(JSON.stringify(val)) 26 | expect(state.value).toEqual(val) 27 | setState(undefined) 28 | expect(sessionStorage.getItem('test')).toBeNull() 29 | expect(state.value).toBeNull() 30 | }) 31 | }) 32 | } 33 | 34 | testSessionStorage('1', '2') 35 | testSessionStorage({ a: 1 }, { a: 2 }) 36 | -------------------------------------------------------------------------------- /tests/useStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useStorage } from '../src/useStorage' 3 | import { invokeHook } from './util' 4 | 5 | function testStorageWithSimpleVal (storage: Storage) { 6 | describe('test useStorage when storage is storage and val is not an Object', () => { 7 | let state: Ref 8 | let setState: (val?: string) => void 9 | beforeEach(() => { 10 | invokeHook(() => { 11 | [state, setState] = useStorage('test', '1', storage) 12 | }) 13 | }) 14 | test('initial value of key test in storage is 1', () => { 15 | expect(storage.getItem('test')).toBe(JSON.stringify('1')) 16 | expect(state.value).toBe('1') 17 | }) 18 | test('value of key test in storage should change when invoking setState with new value', () => { 19 | setState('2') 20 | expect(storage.getItem('test')).toBe(JSON.stringify('2')) 21 | expect(state.value).toBe('2') 22 | }) 23 | test('key test in storage should be removed when invoking setState with no parameter or undefined', () => { 24 | setState() 25 | expect(storage.getItem('test')).toBeNull() 26 | expect(state.value).toBeNull() 27 | setState('2') 28 | expect(storage.getItem('test')).toBe(JSON.stringify('2')) 29 | expect(state.value).toBe('2') 30 | setState(undefined) 31 | expect(storage.getItem('test')).toBeNull() 32 | expect(state.value).toBeNull() 33 | }) 34 | }) 35 | } 36 | 37 | function testStorageWithObject (storage: Storage) { 38 | describe('test useStorage when storage is storage and val is an Object', () => { 39 | let state: Ref<{ a: number} | null> 40 | let setState: (val?: { a: number }) => void 41 | beforeEach(() => { 42 | [state, setState] = useStorage('test', { a: 1 }, storage) 43 | }) 44 | test('initial value of key test in storage is 1', () => { 45 | expect(storage.getItem('test')).toBe(JSON.stringify({ a: 1 })) 46 | expect(state.value).toEqual({ a: 1 }) 47 | }) 48 | test('value of key test in storage should change when invoking setState with new value', () => { 49 | setState({ a: 2 }) 50 | expect(storage.getItem('test')).toBe(JSON.stringify({ a: 2 })) 51 | expect(state.value).toEqual({ a: 2 }) 52 | }) 53 | test('key test in storage should be removed when invoking setState with no parameter or undefined', () => { 54 | setState() 55 | expect(storage.getItem('test')).toBeNull() 56 | expect(state.value).toBeNull() 57 | setState({ a: 1 }) 58 | expect(storage.getItem('test')).toBe(JSON.stringify({ a: 1 })) 59 | expect(state.value).toEqual({ a: 1 }) 60 | setState(undefined) 61 | expect(storage.getItem('test')).toBeNull() 62 | expect(state.value).toBeNull() 63 | }) 64 | }) 65 | } 66 | 67 | testStorageWithSimpleVal(localStorage) 68 | testStorageWithSimpleVal(sessionStorage) 69 | testStorageWithObject(localStorage) 70 | testStorageWithObject(sessionStorage) 71 | -------------------------------------------------------------------------------- /tests/useTimeout.test.ts: -------------------------------------------------------------------------------- 1 | import { useTimeout } from '../src/useTimeout' 2 | import { invokeHook } from './util' 3 | 4 | beforeEach(() => { 5 | jest.useFakeTimers() 6 | }) 7 | 8 | afterEach(() => { 9 | jest.clearAllTimers() 10 | jest.clearAllMocks() 11 | }) 12 | 13 | test('setTimeout should be called with default delay', () => { 14 | const { ready } = useTimeout() 15 | expect(ready.value).toBe(false) 16 | expect(setTimeout).toHaveBeenCalledTimes(1) 17 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000) 18 | jest.advanceTimersByTime(1000) 19 | expect(ready.value).toBeTruthy() 20 | }) 21 | 22 | test('setTimeout should not be called when immediate is set to false', () => { 23 | useTimeout(1000, false) 24 | expect(setTimeout).not.toBeCalled() 25 | }) 26 | 27 | test('setTimeout should be called after calling start when immediate is false', () => { 28 | const { ready, start } = useTimeout(1000, false) 29 | expect(ready.value).toBeFalsy() 30 | expect(setTimeout).not.toBeCalled() 31 | start() 32 | expect(setTimeout).toHaveBeenCalledTimes(1) 33 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000) 34 | jest.advanceTimersByTime(1000) 35 | expect(ready.value).toBeTruthy() 36 | }) 37 | 38 | test('setTimeout should be clear after calling stop', () => { 39 | const { stop } = useTimeout() 40 | stop() 41 | expect(clearTimeout).toHaveBeenCalledTimes(1) 42 | }) 43 | 44 | test('timeout should be clear when component is unmount', () => { 45 | const wrapper = invokeHook(() => { 46 | useTimeout(1000, true) 47 | }) 48 | wrapper.unmount() 49 | expect(clearTimeout).toHaveBeenCalledTimes(1) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/useTimeoutFn.test.ts: -------------------------------------------------------------------------------- 1 | import { useTimeoutFn } from '../src/useTimeoutFn' 2 | import { nextTick } from 'vue' 3 | 4 | let callback: Function | undefined 5 | 6 | beforeEach(() => { 7 | callback = jest.fn() 8 | jest.useFakeTimers() 9 | }) 10 | 11 | afterEach(() => { 12 | jest.clearAllTimers() 13 | jest.clearAllMocks() 14 | }) 15 | 16 | test('setTimeout should be called with default delay', async () => { 17 | useTimeoutFn(callback!) 18 | expect(setTimeout).toHaveBeenCalledTimes(1) 19 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000) 20 | jest.advanceTimersByTime(1000) 21 | await nextTick() 22 | expect(callback!).toHaveBeenCalledTimes(1) 23 | }) 24 | 25 | test('setTimeout should not be called when immediate is set to false', () => { 26 | useTimeoutFn(callback!, 1000, false) 27 | expect(setTimeout).not.toBeCalled() 28 | }) 29 | 30 | test('setTimeout should be called after calling start when immediate is false', async () => { 31 | const { start } = useTimeoutFn(callback!, 1000, false) 32 | expect(setTimeout).not.toBeCalled() 33 | start() 34 | expect(setTimeout).toHaveBeenCalledTimes(1) 35 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000) 36 | jest.advanceTimersByTime(1000) 37 | await nextTick() 38 | expect(callback!).toHaveBeenCalledTimes(1) 39 | }) 40 | 41 | test('setTimeout should be clear after calling stop', () => { 42 | const { stop } = useTimeoutFn(callback!, 1000) 43 | stop() 44 | expect(clearTimeout).toHaveBeenCalledTimes(1) 45 | }) 46 | 47 | test('callback should be called 1 time when clearEffectWhenStop is true and calling start after calling stop', async () => { 48 | const { start, stop } = useTimeoutFn(callback!, 1000, true, true) 49 | stop() 50 | expect(clearTimeout).toHaveBeenCalledTimes(1) 51 | start() 52 | jest.advanceTimersByTime(1000) 53 | await nextTick() 54 | expect(callback!).toHaveBeenCalledTimes(1) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/useTitle.test.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, nextTick } from 'vue' 2 | import { mount, VueWrapper } from '@vue/test-utils' 3 | import { useTitle } from '../src/useTitle' 4 | 5 | describe('test useTitle when restoreOnUnMount is true', () => { 6 | let wrapper: VueWrapper, setTitle: (title: string) => void 7 | beforeEach(() => { 8 | const comp = defineComponent({ 9 | template: '
test
', 10 | setup () { 11 | setTitle = useTitle('test', true) 12 | } 13 | }) 14 | document.title = 'init' 15 | wrapper = mount(comp) 16 | }) 17 | 18 | test('useTitle should be defined', () => { 19 | expect(useTitle).toBeDefined() 20 | }) 21 | 22 | test('document.title should be test after component mounted', () => { 23 | expect(document.title).toBe('test') 24 | }) 25 | 26 | test('document.title should change after invoking setTitle', (done) => { 27 | setTitle('change') 28 | nextTick(() => { 29 | expect(document.title).toBe('change') 30 | done() 31 | }) 32 | }) 33 | 34 | test('document.title should be reset to init after component unmounted', () => { 35 | wrapper.unmount() 36 | expect(document.title).toBe('init') 37 | }) 38 | }) 39 | 40 | describe('test useTitle when passing one parameter', () => { 41 | test('document.title should not be reset to init when component is unmounted', () => { 42 | const comp = defineComponent({ 43 | template: '
test
', 44 | setup () { 45 | useTitle('test') 46 | } 47 | }) 48 | document.title = 'init' 49 | const wrapper = mount(comp) 50 | expect(document.title).toBe('test') 51 | wrapper.unmount() 52 | expect(document.title).toBe('test') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/useToggle.test.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | import { useToggle } from '../src/useToggle' 3 | 4 | describe('test useToggle with none params', () => { 5 | let state: Ref 6 | let toggle: (next?: boolean) => void 7 | let setDefault: () => void 8 | let setRight: () => void 9 | beforeEach(() => { 10 | const result = useToggle() 11 | state = result.state 12 | toggle = result.toggle 13 | setDefault = result.setDefault 14 | setRight = result.setRight 15 | }) 16 | 17 | test('init State should be false', () => { 18 | expect(state.value).toBe(false) 19 | }) 20 | 21 | test('state.value should be toggled after invoking toggle with none params', () => { 22 | expect(state.value).toBe(false) 23 | toggle() 24 | expect(state.value).toBe(true) 25 | toggle() 26 | expect(state.value).toBe(false) 27 | }) 28 | test('state.value should be set to specify value after invoking toggle with one parameter', () => { 29 | expect(state.value).toBe(false) 30 | toggle(true) 31 | expect(state.value).toBe(true) 32 | toggle(false) 33 | expect(state.value).toBe(false) 34 | }) 35 | test('state.value should be false after invoking setDefault', () => { 36 | toggle(true) 37 | expect(state.value).toBe(true) 38 | setDefault() 39 | expect(state.value).toBe(false) 40 | }) 41 | test('state.value should be true after invoking setRight', () => { 42 | setRight() 43 | expect(state.value).toBe(true) 44 | }) 45 | }) 46 | 47 | describe('test useToggle with one parameter', () => { 48 | let state: Ref 49 | let toggle: (next?: boolean) => void 50 | let setDefault: () => void 51 | let setRight: () => void 52 | beforeEach(() => { 53 | const result = useToggle(true) 54 | state = result.state 55 | toggle = result.toggle 56 | setDefault = result.setDefault 57 | setRight = result.setRight 58 | }) 59 | 60 | test('init State should be true', () => { 61 | expect(state.value).toBe(true) 62 | }) 63 | 64 | test('state.value should be toggled after invoking toggle with none params', () => { 65 | expect(state.value).toBe(true) 66 | toggle() 67 | expect(state.value).toBe(false) 68 | toggle() 69 | expect(state.value).toBe(true) 70 | }) 71 | test('state.value should be set to specify value after invoking toggle with one parameter', () => { 72 | expect(state.value).toBe(true) 73 | toggle(false) 74 | expect(state.value).toBe(false) 75 | toggle(true) 76 | expect(state.value).toBe(true) 77 | }) 78 | test('state.value should be true after invoking setDefault', () => { 79 | toggle(false) 80 | expect(state.value).toBe(false) 81 | setDefault() 82 | expect(state.value).toBe(true) 83 | }) 84 | test('state.value should be false after invoking setRight', () => { 85 | setRight() 86 | expect(state.value).toBe(false) 87 | }) 88 | }) 89 | 90 | describe('test useToggle with two parameters', () => { 91 | let state: Ref<'abc' | 'cde'> 92 | let toggle: (next?: 'abc' | 'cde') => void 93 | let setDefault: () => void 94 | let setRight: () => void 95 | beforeEach(() => { 96 | const result = useToggle('abc', 'cde') 97 | state = result.state 98 | toggle = result.toggle 99 | setDefault = result.setDefault 100 | setRight = result.setRight 101 | }) 102 | 103 | test('init State should be abc', () => { 104 | expect(state.value).toBe('abc') 105 | }) 106 | 107 | test('state.value should be toggled after invoking toggle with none params', () => { 108 | expect(state.value).toBe('abc') 109 | toggle() 110 | expect(state.value).toBe('cde') 111 | toggle() 112 | expect(state.value).toBe('abc') 113 | }) 114 | test('state.value should be set to specify value after invoking toggle with one parameter', () => { 115 | expect(state.value).toBe('abc') 116 | toggle('cde') 117 | expect(state.value).toBe('cde') 118 | toggle('abc') 119 | expect(state.value).toBe('abc') 120 | }) 121 | test('state.value should be abc after invoking setDefault', () => { 122 | toggle('cde') 123 | expect(state.value).toBe('cde') 124 | setDefault() 125 | expect(state.value).toBe('abc') 126 | }) 127 | test('state.value should be cde after invoking setRight', () => { 128 | setRight() 129 | expect(state.value).toBe('cde') 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /tests/useWindowScroll.test.ts: -------------------------------------------------------------------------------- 1 | import { VueWrapper } from '@vue/test-utils' 2 | import { DeepReadonly, Ref } from 'vue' 3 | import { useWindowScroll } from '../src/useWindowScroll' 4 | import { invokeHook } from './util' 5 | 6 | let wrapper: VueWrapper 7 | let x: DeepReadonly> 8 | let y: DeepReadonly> 9 | let clear: () => void 10 | 11 | function patchWindow () { 12 | let x = 200 13 | let y = 300 14 | const originAdd = window.addEventListener 15 | const originRemove = window.removeEventListener 16 | const originDispatch = window.dispatchEvent 17 | const addEventListener = jest.fn(function (event, cb, options) { 18 | originAdd.call(addEventListener.mock.instances[0], event, cb, options) 19 | }) 20 | const removeEventListener = jest.fn(function (event, cb, options) { 21 | originRemove.call(removeEventListener.mock.instances[0], event, cb, options) 22 | }) 23 | window.addEventListener = addEventListener 24 | window.removeEventListener = removeEventListener 25 | Object.defineProperties(window, { 26 | scrollX: { 27 | get () { 28 | return x 29 | } 30 | }, 31 | scrollY: { 32 | get () { 33 | return y 34 | } 35 | } 36 | }) 37 | const dispatchEvent = jest.fn(function (e: Event) { 38 | if (e.type === 'scroll') { 39 | x = 1000 40 | y = 700 41 | originDispatch.call(dispatchEvent.mock.instances[0], e) 42 | } 43 | }) 44 | window.dispatchEvent = dispatchEvent as any 45 | return () => { 46 | x = 200 47 | y = 300 48 | } 49 | } 50 | 51 | const reset = patchWindow() 52 | 53 | function testWindowScroll (description: string, delay = 200) { 54 | describe(description, () => { 55 | beforeEach(() => { 56 | jest.useFakeTimers() 57 | wrapper = invokeHook(() => { 58 | const res = useWindowScroll(delay) 59 | x = res.x 60 | y = res.y 61 | clear = res.clear 62 | }) 63 | }) 64 | 65 | afterEach(() => { 66 | jest.useRealTimers() 67 | jest.clearAllMocks() 68 | }) 69 | 70 | afterEach(() => { 71 | reset() 72 | }) 73 | test('window.addEventListener should be called when mounted', () => { 74 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 75 | expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), { 76 | capture: false, 77 | passive: true 78 | }) 79 | }) 80 | 81 | test('width/height should not be undefined', () => { 82 | expect(x).toBeDefined() 83 | expect(y).toBeDefined() 84 | expect(x.value).toBe(200) 85 | expect(y.value).toBe(300) 86 | }) 87 | 88 | test('x / y should be changed when resized', () => { 89 | window.dispatchEvent(new Event('scroll')) 90 | delay && jest.advanceTimersByTime(200) 91 | expect(x.value).toBe(1000) 92 | expect(y.value).toBe(700) 93 | }) 94 | 95 | test('window.removeEventListener should be called when unmounted', () => { 96 | wrapper.unmount() 97 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 98 | expect(window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), { 99 | capture: false, 100 | passive: true 101 | }) 102 | }) 103 | test('window.removeEventListener should be called when called clear', () => { 104 | clear() 105 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 106 | expect(window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), { 107 | capture: false, 108 | passive: true 109 | }) 110 | }) 111 | }) 112 | } 113 | 114 | testWindowScroll('test useWindowScroll with throttle') 115 | testWindowScroll('test useWindowScroll without throttle') 116 | -------------------------------------------------------------------------------- /tests/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './invokeHook' 2 | export * from './patchEventTarget' 3 | -------------------------------------------------------------------------------- /tests/util/invokeHook.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import { defineComponent } from 'vue' 3 | 4 | const defaultTemplate = '
test
' 5 | 6 | export function invokeHook (setup: () => any, template = defaultTemplate) { 7 | document.body.innerHTML = ` 8 |
9 | ` 10 | const App = defineComponent({ 11 | template, 12 | setup 13 | }) 14 | // @ts-ignore 15 | return shallowMount(App, { 16 | attachTo: document.getElementById('app') 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /tests/util/patchEventTarget.ts: -------------------------------------------------------------------------------- 1 | export function patchEventTarget () { 2 | const div: any = document.createElement('div') 3 | let proto = Object.getPrototypeOf(div) 4 | while (proto) { 5 | // eslint-disable-next-line no-prototype-builtins 6 | if (proto.hasOwnProperty('addEventListener')) { 7 | const originAdd = proto.addEventListener 8 | const originRemove = proto.removeEventListener 9 | const addEventListener = jest.fn((event, cb, options) => { 10 | originAdd.call( 11 | addEventListener.mock.instances[0], 12 | event, 13 | cb, 14 | options 15 | ) 16 | }) 17 | const removeEventListener = jest.fn((event, cb, options) => { 18 | originRemove.call( 19 | removeEventListener.mock.instances[0], 20 | event, 21 | cb, 22 | options 23 | ) 24 | }) 25 | proto.addEventListener = addEventListener 26 | proto.removeEventListener = removeEventListener 27 | return [addEventListener, removeEventListener] 28 | } 29 | proto = Object.getPrototypeOf(proto) 30 | } 31 | return [] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "pretty": true, 8 | "rootDir": "src", 9 | "sourceMap": false, 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noImplicitAny": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "outDir": "lib", 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "importHelpers": true, 23 | "skipLibCheck": true 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "esm", 29 | "tests" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------