├── .nvmrc ├── .yvmrc ├── .eslintignore ├── .gitignore ├── .prettierignore ├── jest.config.js ├── .storybook ├── addons.js ├── config.js ├── webpack.config.js └── style.css ├── .github └── wip.yml ├── src ├── mocks │ ├── index.ts │ ├── router.ts │ └── store.ts ├── __stories__ │ ├── components │ │ ├── index.ts │ │ └── ShowDocs.tsx │ ├── useWindowSize.story.tsx │ ├── useGetters.story.tsx │ ├── usePrevious.story.tsx │ ├── useClickAway.story.tsx │ ├── useDate.story.tsx │ ├── useCounter.story.tsx │ ├── useAsync.story.tsx │ ├── useStore.story.tsx │ ├── useState.story.tsx │ ├── useMutations.story.tsx │ ├── useActions.story.tsx │ └── useRouter.story.tsx ├── types │ └── global.d.ts ├── useState.ts ├── useActions.ts ├── useGetters.ts ├── useMutations.ts ├── useStore.ts ├── usePrevious.ts ├── useMountedState.ts ├── useTimeout.ts ├── useRouter.ts ├── useMedia.ts ├── __tests__ │ ├── useRouter.test.ts │ ├── useMountedState.test.ts │ ├── useState.test.ts │ ├── useGetters.test.ts │ ├── useTimeout.test.ts │ ├── useMutations.test.ts │ ├── useCounter.test.ts │ ├── useActions.test.ts │ ├── useMedia.test.ts │ ├── useClickAway.test.ts │ ├── useDate.test.ts │ ├── useStore.test.ts │ ├── usePrevious.test.ts │ ├── useWindowSize.test.ts │ └── useAsync.test.ts ├── useCounter.ts ├── useDate.ts ├── useWindowSize.ts ├── useClickAway.ts ├── index.ts ├── util │ └── renderHook.ts ├── helpers │ └── vuex │ │ ├── index.ts │ │ └── interface.ts └── useAsync.ts ├── commitlint.config.js ├── .prettierrc ├── .babelrc ├── .vscode ├── settings.json ├── launch.json ├── hook.code-snippets └── docs.code-snippets ├── .travis.yml ├── .eslintrc ├── .editorconfig ├── docs ├── useClickAway.md ├── useAsync.md ├── useDate.md ├── useRouter.md ├── useStore.md ├── useWindowSize.md ├── usePrevious.md ├── useGetters.md ├── useCounter.md ├── useState.md ├── useMutations.md └── useActions.md ├── tsconfig.json ├── .all-contributorsrc ├── LICENSE ├── CHANGELOG.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.16.0 2 | -------------------------------------------------------------------------------- /.yvmrc: -------------------------------------------------------------------------------- 1 | 1.17.0 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | storybook-static/ 3 | lib/ 4 | esm/ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | storybook-static/ 3 | coverage/ 4 | lib/ 5 | esm/ 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | storybook-static/ 3 | lib/ 4 | esm/ 5 | package.json 6 | CHANGELOG.md 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | testEnvironment: 'jsdom', 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-notes/register'; 3 | -------------------------------------------------------------------------------- /.github/wip.yml: -------------------------------------------------------------------------------- 1 | locations: 2 | - title 3 | - label_name 4 | - commit_subject 5 | terms: 6 | - do not merge 7 | - ⛔ 8 | -------------------------------------------------------------------------------- /src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createRouter } from './router'; 2 | export { default as createStore } from './store'; 3 | -------------------------------------------------------------------------------- /src/__stories__/components/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | export { default as ShowDocs } from './ShowDocs'; 3 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Global compile-time constants 2 | // eslint-disable-next-line no-underscore-dangle 3 | declare let __DEV__: boolean; 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | ignores: [(commit) => commit.includes('init')], 4 | }; 5 | -------------------------------------------------------------------------------- /src/useState.ts: -------------------------------------------------------------------------------- 1 | import createVuexHelper, { Helper, useState } from './helpers/vuex'; 2 | 3 | export default createVuexHelper(Helper.State); 4 | -------------------------------------------------------------------------------- /src/useActions.ts: -------------------------------------------------------------------------------- 1 | import createVuexHelper, { Helper, useActions } from './helpers/vuex'; 2 | 3 | export default createVuexHelper(Helper.Actions); 4 | -------------------------------------------------------------------------------- /src/useGetters.ts: -------------------------------------------------------------------------------- 1 | import createVuexHelper, { Helper, useGetters } from './helpers/vuex'; 2 | 3 | export default createVuexHelper(Helper.Getters); 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "quoteProps": "consistent", 4 | "trailingComma": "all", 5 | "arrowParens": "always", 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /src/useMutations.ts: -------------------------------------------------------------------------------- 1 | import createVuexHelper, { Helper, useMutations } from './helpers/vuex'; 2 | 3 | export default createVuexHelper(Helper.Mutations); 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { "node": "current" } 7 | } 8 | ], 9 | "@babel/preset-typescript" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.autoFixOnSave": true, 3 | "eslint.enable": true, 4 | "npm.packageManager": "yarn", 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/useStore.ts: -------------------------------------------------------------------------------- 1 | import { computed, getCurrentInstance } from '@vue/composition-api'; 2 | import { Store } from 'vuex'; 3 | 4 | export default function useStore() { 5 | const vm = getCurrentInstance() as Vue; 6 | const store = computed(() => vm.$store as Store); 7 | return store; 8 | } 9 | -------------------------------------------------------------------------------- /src/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, Ref } from '@vue/composition-api'; 2 | 3 | export default function usePrevious(state: Ref | (() => T)) { 4 | const previous = ref(); 5 | 6 | watch(state, (_, oldVal) => { 7 | previous.value = oldVal; 8 | }); 9 | 10 | return previous; 11 | } 12 | -------------------------------------------------------------------------------- /src/useMountedState.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { ref, onMounted } from '@vue/composition-api'; 3 | 4 | export default function useMountedState() { 5 | const isMounted = ref(false); 6 | 7 | onMounted(async () => { 8 | await Vue.nextTick(); 9 | isMounted.value = true; 10 | }); 11 | 12 | return isMounted; 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 10.16.0 4 | 5 | install: 6 | - yarn 7 | 8 | script: 9 | - yarn lint 10 | - yarn lint:types 11 | - yarn lint:prettier 12 | - yarn test 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | - yarn release 17 | 18 | cache: 19 | yarn: true 20 | directories: 21 | - node_modules 22 | -------------------------------------------------------------------------------- /src/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from '@vue/composition-api'; 2 | 3 | export default function useTimeout(delay = 0) { 4 | const ready = ref(false); 5 | let timerId: number; 6 | 7 | onMounted(() => { 8 | timerId = window.setTimeout(() => { 9 | ready.value = true; 10 | }, delay); 11 | }); 12 | 13 | onUnmounted(() => { 14 | window.clearTimeout(timerId); 15 | }); 16 | 17 | return ready; 18 | } 19 | -------------------------------------------------------------------------------- /src/useRouter.ts: -------------------------------------------------------------------------------- 1 | import { computed, getCurrentInstance } from '@vue/composition-api'; 2 | import VueRouter, { Route } from 'vue-router'; 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | $router: VueRouter; 7 | $route: Route; 8 | } 9 | } 10 | 11 | export default function useRouter() { 12 | const vm = getCurrentInstance() as Vue; 13 | const route = computed(() => vm.$route); 14 | return { route, router: vm.$router }; 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "eslint-config-airbnb-base", 6 | "plugin:import/typescript", 7 | "plugin:prettier/recommended" 8 | ], 9 | "env": { 10 | "browser": true, 11 | "es6": true, 12 | "node": true 13 | }, 14 | "rules": { 15 | "no-undef": "off", 16 | "no-unused-vars": "off", 17 | "global-require": "off", 18 | "import/no-extraneous-dependencies": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/mocks/router.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | export default function createRouter(Vue?: VueConstructor) { 5 | if (Vue) Vue.use(VueRouter); 6 | return new VueRouter({ 7 | routes: [ 8 | { 9 | path: '/', 10 | name: 'index', 11 | meta: { title: 'Vue Hooks' }, 12 | }, 13 | { 14 | path: '*', 15 | name: '404', 16 | meta: { title: '404 - Not Found' }, 17 | }, 18 | ], 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/__stories__/components/ShowDocs.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import { RenderContext } from 'vue'; 4 | import { ofType } from 'vue-tsx-support'; 5 | 6 | export interface DocsProps { 7 | md: { default: string }; 8 | } 9 | 10 | const ShowDocs = ({ props }: RenderContext) => ( 11 |
12 | ); 13 | 14 | // @ts-ignore 15 | export default ofType().convert(ShowDocs); 16 | -------------------------------------------------------------------------------- /src/useMedia.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from '@vue/composition-api'; 2 | 3 | export default function useMedia(query, defaultState = false) { 4 | let mql; 5 | const matches = ref(defaultState); 6 | const updateMatches = () => { 7 | if (mql) matches.value = mql.matches; 8 | }; 9 | 10 | onMounted(() => { 11 | mql = window.matchMedia(query); 12 | mql.addListener(updateMatches); 13 | matches.value = mql.matches; 14 | }); 15 | 16 | onUnmounted(() => { 17 | mql.removeListener(updateMatches); 18 | }); 19 | 20 | return matches; 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/useRouter.test.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | describe('useRouter', () => { 5 | it('should be defined', () => { 6 | expect(useRouter).toBeDefined(); 7 | }); 8 | 9 | it('should update route', () => { 10 | renderHook(() => { 11 | const { route, router } = useRouter(); 12 | expect(route.value.name).toBe('index'); 13 | expect(route.value.meta.title).toBe('Vue Hooks'); 14 | router.push('/test'); 15 | expect(route.value.name).toBe('404'); 16 | expect(route.value.meta.title).toBe('404 - Not Found'); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/useCounter.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-return-assign: off */ 2 | import { ref } from '@vue/composition-api'; 3 | 4 | export default function useCounter(initialValue = 0) { 5 | const count = ref(initialValue); 6 | const inc = (delta = 1) => (count.value += delta); 7 | const dec = (delta = 1) => (count.value -= delta); 8 | const get = () => count.value; 9 | const set = (val: number) => (count.value = val); 10 | const reset = (val = initialValue) => { 11 | initialValue = val; // eslint-disable-line no-param-reassign 12 | return set(val); 13 | }; 14 | const actions = { inc, dec, get, set, reset }; 15 | 16 | return [count, actions] as const; 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": ["--runInBand"], 12 | "cwd": "${workspaceFolder}", 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/useMountedState.test.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { onMounted } from '@vue/composition-api'; 3 | import { useMountedState } from '..'; 4 | import renderHook from '../util/renderHook'; 5 | 6 | describe('useMountedState', () => { 7 | it('should be defined', () => { 8 | expect(useMountedState).toBeDefined(); 9 | }); 10 | 11 | it('should return true on mounted', () => { 12 | renderHook(() => { 13 | const isMounted = useMountedState(); 14 | expect(isMounted.value).toBe(false); 15 | 16 | onMounted(async () => { 17 | await Vue.nextTick(); 18 | expect(isMounted.value).toBe(true); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/useDate.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from '@vue/composition-api'; 2 | import dayjs from 'dayjs'; 3 | import relativeTime from 'dayjs/plugin/relativeTime'; 4 | 5 | dayjs.extend(relativeTime); 6 | 7 | export function useDate(d: dayjs.ConfigType = Date.now(), timeout: number = 0) { 8 | const date = ref(dayjs(d)); 9 | 10 | if (timeout) { 11 | let timerId: number; 12 | 13 | onMounted(() => { 14 | timerId = window.setInterval(() => { 15 | date.value = dayjs(Date.now()); 16 | }, timeout); 17 | }); 18 | 19 | onUnmounted(() => { 20 | window.clearInterval(timerId); 21 | }); 22 | } 23 | 24 | return date; 25 | } 26 | 27 | export { dayjs }; 28 | -------------------------------------------------------------------------------- /src/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, onMounted, onUnmounted } from '@vue/composition-api'; 2 | 3 | export default function useWindowSize() { 4 | const width = ref(window.innerWidth); 5 | const height = ref(window.innerHeight); 6 | const update = () => { 7 | width.value = window.innerWidth; 8 | height.value = window.innerHeight; 9 | }; 10 | 11 | const widthPixel = computed(() => `${width.value}px`); 12 | const heightPixel = computed(() => `${height.value}px`); 13 | 14 | onMounted(() => { 15 | window.addEventListener('resize', update); 16 | }); 17 | 18 | onUnmounted(() => { 19 | window.removeEventListener('resize', update); 20 | }); 21 | 22 | return { width, height, widthPixel, heightPixel }; 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/useState.test.ts: -------------------------------------------------------------------------------- 1 | import { useState } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | describe('useState', () => { 5 | it('should be defined', () => { 6 | expect(useState).toBeDefined(); 7 | }); 8 | 9 | it('should update state', () => { 10 | type Inject = { count1: number; count2: number }; 11 | const { vm } = renderHook(() => ({ 12 | ...useState({ count1: 'count' }), 13 | ...useState('test', { count2: 'count' }), 14 | })); 15 | expect(vm.count1).toBe(0); 16 | expect(vm.count2).toBe(0); 17 | 18 | vm.$store.commit('increment'); 19 | vm.$store.commit('test/decrement'); 20 | 21 | expect(vm.count1).toBe(1); 22 | expect(vm.count2).toBe(-1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/useGetters.test.ts: -------------------------------------------------------------------------------- 1 | import { useGetters } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | describe('useGetters', () => { 5 | it('should be defined', () => { 6 | expect(useGetters).toBeDefined(); 7 | }); 8 | 9 | it('should update getters', () => { 10 | type Inject = { plusOne: number; minusOne: number }; 11 | const { vm } = renderHook(() => ({ 12 | ...useGetters(['plusOne']), 13 | ...useGetters('test', ['minusOne']), 14 | })); 15 | expect(vm.plusOne).toBe(1); 16 | expect(vm.minusOne).toBe(-1); 17 | 18 | vm.$store.commit('increment'); 19 | vm.$store.commit('test/decrement'); 20 | 21 | expect(vm.plusOne).toBe(2); 22 | expect(vm.minusOne).toBe(-2); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # Use 4 spaces for the Python files 13 | [*.py] 14 | indent_size = 4 15 | max_line_length = 80 16 | 17 | # The JSON files contain newlines inconsistently 18 | [*.json] 19 | insert_final_newline = ignore 20 | 21 | # Minified JavaScript files shouldn't be changed 22 | [**.min.js] 23 | indent_style = ignore 24 | insert_final_newline = ignore 25 | 26 | # Makefiles always use tabs for indentation 27 | [Makefile] 28 | indent_style = tab 29 | 30 | # Batch files use tabs for indentation 31 | [*.bat] 32 | indent_style = tab 33 | 34 | [*.md] 35 | trim_trailing_whitespace = false 36 | -------------------------------------------------------------------------------- /.vscode/hook.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "vue-hook": { 3 | "scope": "typescript", 4 | "prefix": "use", 5 | "body": [ 6 | "import { $1 } from '@vue/composition-api';", 7 | "", 8 | "export default function use${2:Hook}() {", 9 | " $3", 10 | "}", 11 | "" 12 | ], 13 | "description": "create a vue hook" 14 | }, 15 | "vue-hook-test": { 16 | "scope": "typescript", 17 | "prefix": "test", 18 | "body": [ 19 | "import { use${1:Hook} } from '..';", 20 | "import renderHook from '../util/renderHook';", 21 | "", 22 | "describe('use${1:Hook}', () => {", 23 | " it('should be defined', () => {", 24 | " expect(use${1:Hook}).toBeDefined();", 25 | " });", 26 | "});", 27 | "" 28 | ], 29 | "description": "test a vue hook" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/__tests__/useTimeout.test.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from '@vue/composition-api'; 2 | import { useTimeout } from '..'; 3 | import renderHook from '../util/renderHook'; 4 | 5 | describe('useTimeout', () => { 6 | it('should be defined', () => { 7 | expect(useTimeout).toBeDefined(); 8 | }); 9 | 10 | it('should return true after 3000ms', () => { 11 | const { vm } = renderHook(() => { 12 | jest.useFakeTimers(); 13 | const ready = useTimeout(3000); 14 | expect(ready.value).toBe(false); 15 | 16 | onMounted(() => { 17 | expect(jest.getTimerCount()).toBe(1); 18 | jest.runOnlyPendingTimers(); 19 | expect(ready.value).toBe(true); 20 | }); 21 | 22 | onUnmounted(() => { 23 | expect(jest.getTimerCount()).toBe(0); 24 | }); 25 | }); 26 | 27 | vm.$destroy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /docs/useClickAway.md: -------------------------------------------------------------------------------- 1 | # useClickAway 2 | 3 | Trigger an event when click outside the target element 4 | 5 | # Usage 6 | 7 | ```jsx {8,23} 8 | import { defineComponent } from '@vue/composition-api'; 9 | import { useClickAway } from '@hanxx/vue-hooks'; 10 | 11 | const Demo = defineComponent({ 12 | setup(props, ctx) { 13 | const [count, { inc, dec, set, reset }] = useCounter(); 14 | // @ts-ignore 15 | const { element } = useClickAway(() => inc()); 16 | return { 17 | element, 18 | count, 19 | inc, 20 | dec, 21 | set, 22 | reset, 23 | }; 24 | }, 25 | 26 | render(this: Vue & Inject) { 27 | const { count } = this; 28 | return ( 29 |
30 | click outside the button to increase counter 31 |
32 | 33 |
34 | ); 35 | }, 36 | }); 37 | ``` 38 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addParameters } from '@storybook/vue'; 2 | import { themes } from '@storybook/theming'; 3 | import Vue from 'vue'; 4 | import Vuex from 'vuex'; 5 | import VueRouter from 'vue-router'; 6 | import VueCompositionAPI from '@vue/composition-api'; 7 | import hooks from '../src'; 8 | import 'github-markdown-css'; 9 | import 'prismjs/themes/prism-tomorrow.css'; 10 | import './style.css'; 11 | 12 | addParameters({ 13 | options: { 14 | theme: themes.dark, 15 | panelPosition: 'right', 16 | hierarchySeparator: /\//, 17 | hierarchyRootSeparator: /\|/, 18 | }, 19 | }); 20 | 21 | Vue.use(Vuex); 22 | Vue.use(VueRouter); 23 | Vue.use(VueCompositionAPI); 24 | Vue.use(hooks); 25 | 26 | function loadStories() { 27 | const req = require.context('../src', true, /\.story\.tsx$/); 28 | req.keys().forEach((mod) => req(mod)); 29 | } 30 | 31 | configure(loadStories, module); 32 | -------------------------------------------------------------------------------- /src/useClickAway.ts: -------------------------------------------------------------------------------- 1 | import { Ref, onMounted, onUnmounted, ref } from '@vue/composition-api'; 2 | 3 | export interface ReturnValue { 4 | element: Ref; 5 | } 6 | 7 | export default function useClickAway( 8 | onClickAway: (event?: Event) => void, 9 | dom?: Ref, 10 | eventName: keyof DocumentEventMap = 'click', 11 | ): ReturnValue { 12 | const element = dom || ref(null); 13 | 14 | const handler = (event: Event) => { 15 | // @ts-ignore 16 | if (!element.value || element.value.contains(event.target)) { 17 | return; 18 | } 19 | onClickAway(event); 20 | }; 21 | 22 | onMounted(() => { 23 | document.addEventListener(eventName, handler); 24 | }); 25 | 26 | onUnmounted(() => { 27 | document.removeEventListener(eventName, handler); 28 | }); 29 | 30 | return { 31 | element, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "allowUmdGlobalAccess": true, 8 | "allowSyntheticDefaultImports": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "pretty": true, 13 | "rootDir": "src", 14 | "sourceMap": false, 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noImplicitAny": false, 20 | "noFallthroughCasesInSwitch": true, 21 | "outDir": "lib", 22 | "lib": ["dom", "dom.iterable", "esnext"] 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "lib", 27 | "esm", 28 | "**/__tests__/**/*", 29 | "**/__stories__/**/*", 30 | "src/util/renderHook.ts", 31 | "src/mocks" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "vue-hooks", 3 | "projectOwner": "lianghx-319", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "commit": false, 9 | "commitConvention": "none", 10 | "contributors": [ 11 | { 12 | "login": "u3u", 13 | "name": "u3u", 14 | "avatar_url": "https://avatars2.githubusercontent.com/u/20062482?v=4", 15 | "profile": "https://qwq.cat", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "example", 20 | "test" 21 | ] 22 | }, 23 | { 24 | "login": "lianghx-319", 25 | "name": "Han", 26 | "avatar_url": "https://avatars2.githubusercontent.com/u/27187946?v=4", 27 | "profile": "https://github.com/lianghx-319", 28 | "contributions": [ 29 | "code" 30 | ] 31 | } 32 | ], 33 | "repoHost": "https://github.com", 34 | "skipCi": true, 35 | "contributorsPerLine": 7 36 | } 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useWindowSize } from './useWindowSize'; 2 | export { default as useCounter } from './useCounter'; 3 | export { default as usePrevious } from './usePrevious'; 4 | export { default as useStore } from './useStore'; 5 | export { default as useState } from './useState'; 6 | export { default as useGetters } from './useGetters'; 7 | export { default as useMutations } from './useMutations'; 8 | export { default as useActions } from './useActions'; 9 | export { default as useRouter } from './useRouter'; 10 | export { default as useMountedState } from './useMountedState'; 11 | export { default as useTimeout } from './useTimeout'; 12 | export { default as useMedia } from './useMedia'; 13 | export { default as useAsync } from './useAsync'; 14 | export { default as useClickAway } from './useClickAway'; 15 | 16 | // 兼容旧版需要 Vue.use() 17 | export default function install() { 18 | // eslint-disable-next-line no-console 19 | console.warn('@hanxx/vue-hooks dont need to call Vue.use() anymore'); 20 | } 21 | -------------------------------------------------------------------------------- /src/__stories__/useWindowSize.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useWindowSize } from '..'; 7 | import { ShowDocs } from './components'; 8 | 9 | type Inject = { 10 | width: number; 11 | height: number; 12 | }; 13 | 14 | const Docs = () => ; 15 | 16 | const Demo = defineComponent({ 17 | setup() { 18 | const { width, height } = useWindowSize(); 19 | return { width, height }; 20 | }, 21 | 22 | render(this: Vue & Inject) { 23 | const { width, height } = this; 24 | return ( 25 |
26 |
width: {width}px
27 |
height: {height}px
28 |
29 | ); 30 | }, 31 | }); 32 | 33 | storiesOf('useWindowSize', module) 34 | // @ts-ignore 35 | .add('docs', () => Docs) 36 | .add('demo', () => Demo); 37 | -------------------------------------------------------------------------------- /src/util/renderHook.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import Vue from 'vue'; 3 | import { shallowMount, createLocalVue } from '@vue/test-utils'; 4 | import VueCompositionAPI, { defineComponent } from '@vue/composition-api'; 5 | import { createRouter, createStore } from '../mocks'; 6 | 7 | const localVue = createLocalVue(); 8 | const router = createRouter(localVue); 9 | const store = createStore(localVue); 10 | 11 | localVue.use(VueCompositionAPI); 12 | 13 | export default function renderHook(setup) { 14 | const App = defineComponent({ 15 | template: ` 16 |
17 |
20 | `, 21 | 22 | setup, 23 | }); 24 | 25 | // @ts-ignore 26 | return shallowMount(App, { 27 | localVue, 28 | router, 29 | store, 30 | stubs: ['router-view'], 31 | attachToDocument: true, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) lianghx-319 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 | -------------------------------------------------------------------------------- /src/__tests__/useMutations.test.ts: -------------------------------------------------------------------------------- 1 | import { useMutations } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | interface InjectMutations { 5 | increment: Function; 6 | decrement: Function; 7 | } 8 | 9 | describe('useMutations', () => { 10 | it('should be defined', () => { 11 | expect(useMutations).toBeDefined(); 12 | }); 13 | 14 | it('should be defined mutations', () => { 15 | const { vm } = renderHook(() => ({ 16 | ...useMutations(['increment']), 17 | ...useMutations('test', ['decrement']), 18 | })); 19 | 20 | expect(vm.increment).toBeDefined(); 21 | expect(vm.decrement).toBeDefined(); 22 | }); 23 | 24 | it('should update count state', () => { 25 | const { vm } = renderHook(() => ({ 26 | ...useMutations(['increment']), 27 | ...useMutations('test', ['decrement']), 28 | })); 29 | 30 | expect(vm.$store.state.count).toBe(0); 31 | expect(vm.$store.state.test.count).toBe(0); 32 | 33 | vm.increment(); 34 | vm.decrement(); 35 | 36 | expect(vm.$store.state.count).toBe(1); 37 | expect(vm.$store.state.test.count).toBe(-1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const prism = require('markdown-it-prism'); 2 | const highlightLines = require('markdown-it-highlight-lines'); 3 | const linkAttributes = require('markdown-it-link-attributes'); 4 | 5 | module.exports = ({ config }) => { 6 | config.module.rules.push({ 7 | test: /\.md$/, 8 | use: [ 9 | { 10 | loader: require.resolve('markdown-it-loader'), 11 | options: { 12 | use: [ 13 | prism, 14 | highlightLines, 15 | [ 16 | linkAttributes, 17 | { 18 | pattern: /^https?:/, 19 | attrs: { 20 | class: 'external-link', 21 | target: '_blank', 22 | }, 23 | }, 24 | ], 25 | ], 26 | }, 27 | }, 28 | ], 29 | }); 30 | 31 | config.module.rules.push({ 32 | test: /\.(ts|tsx)$/, 33 | use: [ 34 | { 35 | loader: require.resolve('babel-loader'), 36 | options: { 37 | presets: ['@babel/env', '@babel/typescript', '@vue/jsx'], 38 | }, 39 | }, 40 | ], 41 | }); 42 | 43 | config.resolve.extensions.push('.ts', '.tsx'); 44 | return config; 45 | }; 46 | -------------------------------------------------------------------------------- /docs/useAsync.md: -------------------------------------------------------------------------------- 1 | # useAsync 2 | 3 | Vue hook for handling async state such as loading, success, error 4 | 5 | # Usage 6 | 7 | ```jsx {13,14,15,33,34,35} 8 | import { defineComponent } from '@vue/composition-api'; 9 | import { useAsync } from '@hanxx/vue-hooks'; 10 | // async function like ajax etc 11 | const sleep = (ms = 0) => 12 | new Promise((resolve) => { 13 | setTimeout(() => { 14 | resolve('success'); 15 | }, ms); 16 | }); 17 | 18 | const Demo = defineComponent({ 19 | setup() { 20 | const { run, loading, resp, error } = useAsync(sleep, { 21 | manual: true, 22 | params: [2000], 23 | }); 24 | 25 | return { 26 | run, 27 | loading, 28 | resp, 29 | error, 30 | }; 31 | }, 32 | 33 | render(this: Vue & Inject) { 34 | const { run, loading, resp, error } = this; 35 | return ( 36 |
37 |
loading state: {loading.toString()}
38 |
resp state: {resp}
39 |
error state: {error}
40 | 43 |
44 | ); 45 | }, 46 | }); 47 | ``` 48 | -------------------------------------------------------------------------------- /src/__tests__/useCounter.test.ts: -------------------------------------------------------------------------------- 1 | import { useCounter } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | describe('useCounter', () => { 5 | it('should be defined', () => { 6 | expect(useCounter).toBeDefined(); 7 | }); 8 | 9 | it('should be update counter', () => { 10 | renderHook(() => { 11 | const [count, { inc, dec, get, set, reset }] = useCounter(); 12 | 13 | expect(count.value).toBe(0); 14 | expect(get()).toBe(0); 15 | inc(); 16 | expect(count.value).toBe(1); 17 | expect(get()).toBe(1); 18 | inc(2); 19 | expect(count.value).toBe(3); 20 | expect(get()).toBe(3); 21 | dec(); 22 | expect(count.value).toBe(2); 23 | expect(get()).toBe(2); 24 | dec(5); 25 | expect(count.value).toBe(-3); 26 | expect(get()).toBe(-3); 27 | set(100); 28 | expect(count.value).toBe(100); 29 | expect(get()).toBe(100); 30 | reset(); 31 | expect(count.value).toBe(0); 32 | expect(get()).toBe(0); 33 | reset(25); 34 | expect(count.value).toBe(25); 35 | expect(get()).toBe(25); 36 | reset(); 37 | expect(count.value).toBe(25); 38 | expect(get()).toBe(25); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__stories__/useGetters.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useStore, useGetters } from '..'; 7 | import { ShowDocs } from './components'; 8 | import { createStore } from '../mocks'; 9 | 10 | type Inject = { 11 | plusOne: number; 12 | minusOne: number; 13 | }; 14 | 15 | const Docs = () => ; 16 | 17 | const Demo = defineComponent({ 18 | store: createStore(), 19 | 20 | setup() { 21 | const store = useStore(); 22 | const getters = { 23 | ...useGetters(['plusOne']), 24 | ...useGetters('test', ['minusOne']), 25 | }; 26 | 27 | store.value.dispatch('incrementAsync'); 28 | store.value.dispatch('test/decrementAsync'); 29 | 30 | return { ...getters }; 31 | }, 32 | 33 | render(this: Vue & Inject) { 34 | const { plusOne, minusOne } = this; 35 | return ( 36 |
37 |
plusOne: {plusOne}
38 |
test/minusOne: {minusOne}
39 |
40 | ); 41 | }, 42 | }); 43 | 44 | storiesOf('useGetters', module) 45 | // @ts-ignore 46 | .add('docs', () => Docs) 47 | .add('demo', () => Demo); 48 | -------------------------------------------------------------------------------- /src/__tests__/useActions.test.ts: -------------------------------------------------------------------------------- 1 | import { useActions } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | interface InjectActions { 5 | incrementAsync: (delay?: number) => void; 6 | decrementAsync: (delay?: number) => void; 7 | } 8 | 9 | describe('useActions', () => { 10 | it('should be defined', () => { 11 | expect(useActions).toBeDefined(); 12 | }); 13 | 14 | it('should be defined actions', () => { 15 | const { vm } = renderHook(() => ({ 16 | ...useActions(['incrementAsync']), 17 | ...useActions('test', ['decrementAsync']), 18 | })); 19 | 20 | expect(vm.incrementAsync).toBeDefined(); 21 | expect(vm.decrementAsync).toBeDefined(); 22 | }); 23 | 24 | it('should async update count state', () => { 25 | const { vm } = renderHook(() => ({ 26 | ...useActions(['incrementAsync']), 27 | ...useActions('test', ['decrementAsync']), 28 | })); 29 | 30 | expect(vm.$store.state.count).toBe(0); 31 | expect(vm.$store.state.test.count).toBe(0); 32 | 33 | jest.useFakeTimers(); 34 | 35 | vm.incrementAsync(0); 36 | vm.decrementAsync(0); 37 | 38 | setTimeout(() => { 39 | expect(vm.$store.state.count).toBe(1); 40 | expect(vm.$store.state.test.count).toBe(-1); 41 | }); 42 | 43 | jest.runAllTimers(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__tests__/useMedia.test.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from '@vue/composition-api'; 2 | import { useMedia } from '..'; 3 | import renderHook from '../util/renderHook'; 4 | 5 | const matchMediaMock = require('match-media-mock').create(); 6 | 7 | matchMediaMock.setConfig({ type: 'screen', width: 1280, height: 800 }); 8 | window.matchMedia = matchMediaMock; 9 | 10 | describe('useMedia', () => { 11 | it('should be defined', () => { 12 | expect(useMedia).toBeDefined(); 13 | }); 14 | 15 | it('should update matches', () => { 16 | const { vm } = renderHook(() => { 17 | const pc = useMedia('(min-width: 768px)'); 18 | const sp = useMedia('(max-width: 768px)'); 19 | expect(pc.value).toBe(false); 20 | expect(sp.value).toBe(false); 21 | 22 | onMounted(() => { 23 | expect(pc.value).toBe(true); 24 | expect(sp.value).toBe(false); 25 | 26 | matchMediaMock.setConfig({ type: 'screen', width: 375, height: 667 }); 27 | 28 | expect(pc.value).toBe(false); 29 | expect(sp.value).toBe(true); 30 | }); 31 | 32 | onUnmounted(() => { 33 | matchMediaMock.setConfig({ type: 'screen', width: 1280, height: 800 }); 34 | 35 | expect(pc.value).toBe(false); 36 | expect(sp.value).toBe(true); 37 | }); 38 | }); 39 | 40 | vm.$destroy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__stories__/usePrevious.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint no-plusplus: off, import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent, ref } from '@vue/composition-api'; 6 | import { usePrevious } from '..'; 7 | import { ShowDocs } from './components'; 8 | 9 | type Inject = { 10 | count: number; 11 | prevCount: number; 12 | increment: () => void; 13 | decrement: () => void; 14 | }; 15 | 16 | const Docs = () => ; 17 | 18 | const Demo = defineComponent({ 19 | setup() { 20 | const count = ref(0); 21 | const prevCount = usePrevious(count); 22 | 23 | const increment = () => count.value++; 24 | const decrement = () => count.value--; 25 | 26 | return { count, prevCount, increment, decrement }; 27 | }, 28 | 29 | render(this: Vue & Inject) { 30 | const { count, prevCount } = this; 31 | return ( 32 |
33 |
now: {count}
34 |
before: {String(prevCount)}
35 | 36 | 37 |
38 | ); 39 | }, 40 | }); 41 | 42 | storiesOf('usePrevious', module) 43 | // @ts-ignore 44 | .add('docs', () => Docs) 45 | .add('demo', () => Demo); 46 | -------------------------------------------------------------------------------- /docs/useDate.md: -------------------------------------------------------------------------------- 1 | # useDate 2 | 3 | Vue hook that process date via [`dayjs`](https://github.com/iamkun/dayjs). 4 | 5 | ## Usage 6 | 7 | ```jsx {6,11} 8 | import { defineComponent } from '@vue/composition-api'; 9 | import { useDate } from '@hanxx/vue-hooks'; 10 | 11 | const Demo = defineComponent({ 12 | setup() { 13 | const date = useDate(Date.now(), 1000); 14 | return { date }; 15 | }, 16 | 17 | render() { 18 | const { date } = this; 19 | return ( 20 |
    21 |
  • Date: {date.toString()}
  • 22 |
  • Standard Format: {date.format('YYYY-MM-DD HH:mm:ss')}
  • 23 |
  • Relative Time: {date.from(date.add(1, 'd'))}
  • 24 |
25 | ); 26 | }, 27 | }); 28 | ``` 29 | 30 | ## Reference 31 | 32 | ```typescript 33 | function useDate(date?: dayjs.ConfigType, interval?: number): Ref; 34 | ``` 35 | 36 | ### `Arguments` 37 | 38 | - `date` 39 | 40 | Date to process. 41 | 42 | - Type: [`dayjs.ConfigType`](https://github.com/iamkun/dayjs/blob/19affc84bbec84bad840e310b390db5f92b2499a/types/index.d.ts#L5) 43 | - Default: `Date.now()` 44 | 45 | - `interval` 46 | 47 | The update interval of the date, if it is `0`, it will not be updated. 48 | 49 | - Type: `number` 50 | - Default: `0` 51 | 52 | ### `ReturnValue` 53 | 54 | - [`Ref`](https://github.com/iamkun/dayjs/blob/19affc84bbec84bad840e310b390db5f92b2499a/types/index.d.ts#L15-L95) 55 | -------------------------------------------------------------------------------- /src/__tests__/useClickAway.test.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { useClickAway } from '..'; 3 | import renderHook from '../util/renderHook'; 4 | 5 | interface InjectToVm { 6 | nav: any; 7 | } 8 | 9 | describe('useClickAway', () => { 10 | it('should be defined', () => { 11 | expect(useClickAway).toBeDefined(); 12 | }); 13 | 14 | it('should trigger handler when click div[id=app]', async () => { 15 | const clickHandler = sinon.stub(); 16 | const wrapper = renderHook(() => { 17 | const { element: nav } = useClickAway(() => { 18 | clickHandler(); 19 | }); 20 | 21 | return { 22 | nav, 23 | }; 24 | }); 25 | 26 | wrapper.find('#app').trigger('click'); 27 | 28 | await wrapper.vm.$nextTick(); 29 | 30 | expect(clickHandler.called).toBeTruthy(); 31 | 32 | wrapper.vm.$destroy(); 33 | }); 34 | 35 | it('should not trigger handler when click nav', async () => { 36 | const clickHandler = sinon.stub(); 37 | const wrapper = renderHook(() => { 38 | const { element: nav } = useClickAway(() => { 39 | clickHandler(); 40 | }); 41 | 42 | return { 43 | nav, 44 | }; 45 | }); 46 | 47 | wrapper.find('nav').trigger('click'); 48 | 49 | await wrapper.vm.$nextTick(); 50 | 51 | expect(clickHandler.called).toBeFalsy(); 52 | 53 | wrapper.vm.$destroy(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /docs/useRouter.md: -------------------------------------------------------------------------------- 1 | # useRouter 2 | 3 | > You need to [use a plugin](https://github.com/u3u/vue-hooks#usage) before using this hook. 4 | 5 | Vue hook for [vue-router](https://router.vuejs.org). 6 | 7 | ## Usage 8 | 9 | ```jsx {6,11,19,23} 10 | import { defineComponent, onMounted, onUnmounted } from '@vue/composition-api'; 11 | import { useRouter } from '@hanxx/vue-hooks'; 12 | 13 | const Demo = defineComponent({ 14 | setup() { 15 | const { route, router } = useRouter(); 16 | let timerId; 17 | 18 | onMounted(() => { 19 | timerId = window.setInterval(() => { 20 | router.replace(route.value.meta.next); 21 | }, 5e3); 22 | }); 23 | 24 | onUnmounted(() => { 25 | window.clearInterval(timerId); 26 | }); 27 | 28 | return { route }; 29 | }, 30 | 31 | render() { 32 | const { route } = this; 33 | return ( 34 |
    35 | {Object.keys(route).map((key) => ( 36 |
  • 37 | {key}:
    {JSON.stringify(route[key], null, 2)}
    38 |
  • 39 | ))} 40 |
41 | ); 42 | }, 43 | }); 44 | ``` 45 | 46 | ## Reference 47 | 48 | ```typescript 49 | function useRouter(): { 50 | route: Ref; 51 | router: VueRouter; 52 | }; 53 | ``` 54 | 55 | ### `ReturnValue` 56 | 57 | - `route`: [`Ref`](https://router.vuejs.org/api/#route-object-properties) 58 | - `router`: [`VueRouter`](https://router.vuejs.org/api/#router-instance-methods) 59 | -------------------------------------------------------------------------------- /docs/useStore.md: -------------------------------------------------------------------------------- 1 | # useStore 2 | 3 | > You need to [use a plugin](https://github.com/u3u/vue-hooks#usage) before using this hook. 4 | 5 | Vue hook for [vuex](https://vuex.vuejs.org). 6 | 7 | ## Usage 8 | 9 | ```jsx {6,12,16} 10 | import { defineComponent, computed } from '@vue/composition-api'; 11 | import { useStore } from '@hanxx/vue-hooks'; 12 | 13 | const Demo = defineComponent({ 14 | setup() { 15 | const store = useStore(); 16 | const plusOne = computed(() => store.value.state.count + 1); 17 | 18 | const increment = () => store.value.commit('increment'); 19 | const incrementAsync = () => store.value.dispatch('incrementAsync'); 20 | 21 | return { store, plusOne, increment, incrementAsync }; 22 | }, 23 | 24 | render() { 25 | const { store, plusOne } = this; 26 | return ( 27 |
28 |
count: {store.state.count}
29 |
plusOne: {plusOne}
30 | 31 | 32 |
33 | ); 34 | }, 35 | }); 36 | ``` 37 | 38 | ## Reference 39 | 40 | ```typescript 41 | function useStore(): Ref>; 42 | ``` 43 | 44 | ### `ReturnValue` 45 | 46 | - [`Ref>`](https://vuex.vuejs.org/api/#vuex-store-instance-properties) 47 | 48 | _[`TState`](https://www.typescriptlang.org/docs/handbook/generics.html) is used to specify the type of `store.state`_ 49 | -------------------------------------------------------------------------------- /docs/useWindowSize.md: -------------------------------------------------------------------------------- 1 | # useWindowSize 2 | 3 | Vue hook that tracks dimensions of the browser window. 4 | 5 | ## Usage 6 | 7 | ```jsx {6,11} 8 | import { defineComponent } from '@vue/composition-api'; 9 | import { useWindowSize } from '@hanxx/vue-hooks'; 10 | 11 | const Demo = defineComponent({ 12 | setup() { 13 | const { width, height } = useWindowSize(); 14 | return { width, height }; 15 | }, 16 | 17 | render() { 18 | const { width, height } = this; 19 | return ( 20 |
21 |
width: {width}px
22 |
height: {height}px
23 |
24 | ); 25 | }, 26 | }); 27 | ``` 28 | 29 | ## Reference 30 | 31 | ```typescript 32 | function useWindowSize(): { 33 | width: Ref; 34 | height: Ref; 35 | widthPixel: Ref; 36 | heightPixel: Ref; 37 | }; 38 | ``` 39 | 40 | ### `ReturnValue` 41 | 42 | - `width`: [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 43 | - `height`: [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 44 | - `widthPixel`: [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 45 | - `heightPixel`: [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 46 | -------------------------------------------------------------------------------- /src/__stories__/useClickAway.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useCounter, useClickAway } from '..'; 7 | import { ShowDocs } from './components'; 8 | 9 | type Inject = { 10 | element: string; 11 | count: number; 12 | }; 13 | 14 | declare module '@vue/composition-api/dist/component/component' { 15 | interface SetupContext { 16 | readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }; 17 | } 18 | } 19 | 20 | const Docs = () => ; 21 | 22 | const Demo = defineComponent({ 23 | setup(props, ctx) { 24 | const [count, { inc, dec, set, reset }] = useCounter(); 25 | // @ts-ignore 26 | const { element } = useClickAway(() => inc()); 27 | return { 28 | element, 29 | count, 30 | inc, 31 | dec, 32 | set, 33 | reset, 34 | }; 35 | }, 36 | 37 | render(this: Vue & Inject) { 38 | const { count } = this; 39 | return ( 40 |
41 | click outside the button to increase counter 42 |
43 | 44 |
45 | ); 46 | }, 47 | }); 48 | 49 | storiesOf('useClickAway', module) 50 | // @ts-ignore 51 | .add('docs', () => Docs) 52 | .add('demo', () => Demo); 53 | -------------------------------------------------------------------------------- /src/mocks/store.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import Vuex, { Module } from 'vuex'; 3 | 4 | export interface CounterState { 5 | count: number; 6 | } 7 | 8 | export default function createStore(Vue?: VueConstructor) { 9 | if (Vue) Vue.use(Vuex); 10 | return new Vuex.Store({ 11 | state: { 12 | count: 0, 13 | }, 14 | 15 | actions: { 16 | incrementAsync(context, delay = 1000) { 17 | setTimeout(() => { 18 | context.commit('increment'); 19 | }, delay); 20 | }, 21 | }, 22 | 23 | mutations: { 24 | increment(state) { 25 | Object.assign(state, { count: state.count + 1 }); 26 | }, 27 | }, 28 | 29 | getters: { 30 | plusOne: (state) => state.count + 1, 31 | }, 32 | 33 | modules: { 34 | test: { 35 | namespaced: true, 36 | 37 | state: { 38 | count: 0, 39 | }, 40 | 41 | actions: { 42 | decrementAsync(context, delay = 1000) { 43 | setTimeout(() => { 44 | context.commit('decrement'); 45 | }, delay); 46 | }, 47 | }, 48 | 49 | mutations: { 50 | decrement(state) { 51 | Object.assign(state, { count: state.count - 1 }); 52 | }, 53 | }, 54 | 55 | getters: { 56 | minusOne: (state) => state.count - 1, 57 | }, 58 | } as Module, 59 | }, 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/__tests__/useDate.test.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from '@vue/composition-api'; 2 | import { useDate, dayjs } from '../useDate'; 3 | import renderHook from '../util/renderHook'; 4 | 5 | describe('useDate', () => { 6 | it('should be defined', () => { 7 | expect(useDate).toBeDefined(); 8 | }); 9 | 10 | it('should be have default date', () => { 11 | renderHook(() => { 12 | const date = useDate(); 13 | expect(date.value).toBeInstanceOf(dayjs); 14 | }); 15 | }); 16 | 17 | it('should be same date', () => { 18 | renderHook(() => { 19 | const date1 = useDate('2019-05-20'); 20 | const date2 = useDate('2019-05-21'); 21 | expect(date2.value.add(-1, 'day').isSame(date1.value)).toBe(true); 22 | }); 23 | }); 24 | 25 | it('should update date', () => { 26 | jest.useFakeTimers(); 27 | const { vm } = renderHook(() => { 28 | const date = useDate(Date.now(), 1000); 29 | let timerId; 30 | 31 | onMounted(() => { 32 | timerId = setInterval(() => { 33 | expect(date.value.isSame(Date.now(), 's')).toBe(true); 34 | }, 1000); 35 | jest.runOnlyPendingTimers(); 36 | }); 37 | 38 | onUnmounted(() => { 39 | clearInterval(timerId); 40 | expect(jest.getTimerCount()).toBe(0); 41 | }); 42 | }); 43 | 44 | setTimeout(() => vm.$destroy(), 3000); 45 | expect(jest.getTimerCount()).toBe(3); 46 | jest.runOnlyPendingTimers(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/usePrevious.md: -------------------------------------------------------------------------------- 1 | # usePrevious 2 | 3 | Vue hook that returns the previous value. 4 | 5 | ## Usage 6 | 7 | ```jsx {7,12,16} 8 | import { defineComponent, ref } from '@vue/composition-api'; 9 | import { usePrevious } from '@hanxx/vue-hooks'; 10 | 11 | const Demo = defineComponent({ 12 | setup() { 13 | const count = ref(0); 14 | const prevCount = usePrevious(count); 15 | 16 | const increment = () => count.value++; 17 | const decrement = () => count.value--; 18 | 19 | return { count, prevCount, increment, decrement }; 20 | }, 21 | 22 | render() { 23 | const { count, prevCount } = this; 24 | return ( 25 |
26 |
now: {count}
27 |
before: {String(prevCount)}
28 | 29 | 30 |
31 | ); 32 | }, 33 | }); 34 | ``` 35 | 36 | ## Reference 37 | 38 | ```typescript 39 | function usePrevious(state: Ref | (() => T)): Ref; 40 | ``` 41 | 42 | ### `Arguments` 43 | 44 | - `state` 45 | 46 | `props` or `Ref` 47 | 48 | - Type: [`Ref | (() => T)`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 49 | 50 | ### `ReturnValue` 51 | 52 | - [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 53 | 54 | _[`T`](https://www.typescriptlang.org/docs/handbook/generics.html) depends on your `arguments` type_ 55 | -------------------------------------------------------------------------------- /src/__stories__/useDate.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useDate, dayjs } from '../useDate'; 7 | import { ShowDocs } from './components'; 8 | 9 | type Inject = { 10 | date: dayjs.Dayjs; 11 | }; 12 | 13 | const Docs = () => ; 14 | 15 | const Demo = defineComponent({ 16 | setup() { 17 | const date = useDate(Date.now(), 1000); 18 | return { date }; 19 | }, 20 | 21 | render(this: Vue & Inject) { 22 | const { date } = this; 23 | const symbols = [1, -1]; 24 | const unitTypeShort = ['d', 'M', 'y', 'h', 'm', 's', 'ms']; 25 | const randomSymbolIndex = Math.floor(Math.random() * symbols.length); 26 | const randomUnitTypeIndex = Math.floor(Math.random() * unitTypeShort.length); // prettier-ignore 27 | const inc = (randomUnitTypeIndex + 1) * symbols[randomSymbolIndex]; 28 | const unit = unitTypeShort[randomUnitTypeIndex] as dayjs.UnitTypeShort; 29 | 30 | return ( 31 |
    32 |
  • Date: {date.toString()}
  • 33 |
  • Standard Format: {date.format('YYYY-MM-DD HH:mm:ss')}
  • 34 |
  • 35 | Relative Time ({inc} {unit}): {date.from(date.add(inc, unit))} 36 |
  • 37 |
38 | ); 39 | }, 40 | }); 41 | 42 | storiesOf('useDate', module) 43 | // @ts-ignore 44 | .add('docs', () => Docs) 45 | .add('demo', () => Demo); 46 | -------------------------------------------------------------------------------- /src/__stories__/useCounter.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useCounter } from '..'; 7 | import { ShowDocs } from './components'; 8 | 9 | type Inject = { 10 | count: number; 11 | inc: Function; 12 | dec: Function; 13 | set: Function; 14 | reset: Function; 15 | }; 16 | 17 | const Docs = () => ; 18 | 19 | const Demo = defineComponent({ 20 | setup() { 21 | const [count, { inc, dec, set, reset }] = useCounter(); 22 | return { 23 | count, 24 | inc, 25 | dec, 26 | set, 27 | reset, 28 | }; 29 | }, 30 | 31 | render(this: Vue & Inject) { 32 | const { count, inc, dec, set, reset } = this; 33 | return ( 34 |
35 |
count: {count}
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | ); 45 | }, 46 | }); 47 | 48 | storiesOf('useCounter', module) 49 | // @ts-ignore 50 | .add('docs', () => Docs) 51 | .add('demo', () => Demo); 52 | -------------------------------------------------------------------------------- /src/__stories__/useAsync.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useAsync } from '..'; 7 | import { ShowDocs } from './components'; 8 | 9 | type Inject = { 10 | loading: boolean; 11 | error: Error | null; 12 | resp: any; 13 | run: (...args: any[]) => Promise; 14 | }; 15 | 16 | const sleep = (ms = 0) => 17 | new Promise((resolve) => { 18 | setTimeout(() => { 19 | resolve('success'); 20 | }, ms); 21 | }); 22 | 23 | const Docs = () => ; 24 | 25 | const Demo = defineComponent({ 26 | setup() { 27 | const { run, loading, resp, error } = useAsync(sleep, { 28 | manual: true, 29 | params: [2000], 30 | }); 31 | 32 | return { 33 | run, 34 | loading, 35 | resp, 36 | error, 37 | }; 38 | }, 39 | 40 | render(this: Vue & Inject) { 41 | const { run, loading, resp, error } = this; 42 | return ( 43 |
44 |
loading state: {loading.toString()}
45 |
resp state: {resp}
46 |
error state: {error}
47 | 50 |
51 | ); 52 | }, 53 | }); 54 | 55 | storiesOf('useAsync', module) 56 | // @ts-ignore 57 | .add('docs', () => Docs) 58 | .add('demo', () => Demo); 59 | -------------------------------------------------------------------------------- /src/helpers/vuex/index.ts: -------------------------------------------------------------------------------- 1 | import { computed, getCurrentInstance } from '@vue/composition-api'; 2 | import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'; 3 | import { useState, useGetters, useMutations, useActions } from './interface'; 4 | 5 | export enum Helper { 6 | State, 7 | Getters, 8 | Mutations, 9 | Actions, 10 | } 11 | 12 | type Helpers = useState | useGetters | useMutations | useActions; 13 | 14 | function handleComputed(mappedFn: Function) { 15 | // TypeError: Cannot read property '_modulesNamespaceMap' of undefined 16 | // You must get `runtimeVM` in real time in the calculation properties. 17 | const vm = getCurrentInstance(); 18 | return computed(() => mappedFn.call(vm)); 19 | } 20 | 21 | function handleMethods(mappedFn: Function): T { 22 | const vm = getCurrentInstance(); 23 | return mappedFn.bind(vm); 24 | } 25 | 26 | const helpers = { 27 | [Helper.State]: { fn: mapState, handler: handleComputed }, 28 | [Helper.Getters]: { fn: mapGetters, handler: handleComputed }, 29 | [Helper.Mutations]: { fn: mapMutations, handler: handleMethods }, 30 | [Helper.Actions]: { fn: mapActions, handler: handleMethods }, 31 | }; 32 | 33 | export default function createVuexHelper(h: Helper) { 34 | const helper = helpers[h]; 35 | 36 | return ((...args) => { 37 | // @ts-ignore 38 | const mapper = (helper.fn as T)(...args); 39 | const dictionary = {}; 40 | Object.keys(mapper).forEach((key) => { 41 | dictionary[key] = helper.handler(mapper[key]); 42 | }); 43 | 44 | return dictionary; 45 | }) as T; 46 | } 47 | 48 | export * from './interface'; 49 | -------------------------------------------------------------------------------- /src/__stories__/useStore.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { Store } from 'vuex'; 5 | import { storiesOf } from '@storybook/vue'; 6 | import { defineComponent, computed } from '@vue/composition-api'; 7 | import { useStore } from '..'; 8 | import { ShowDocs } from './components'; 9 | import { createStore } from '../mocks'; 10 | import { CounterState } from '../mocks/store'; 11 | 12 | type Inject = { 13 | store: Store; 14 | plusOne: number; 15 | increment: () => void; 16 | incrementAsync: () => void; 17 | }; 18 | 19 | const Docs = () => ; 20 | 21 | const Demo = defineComponent({ 22 | store: createStore(), 23 | 24 | setup() { 25 | const store = useStore(); 26 | const plusOne = computed(() => store.value.state.count + 1); 27 | 28 | const increment = () => store.value.commit('increment'); 29 | const incrementAsync = () => store.value.dispatch('incrementAsync'); 30 | 31 | return { store, plusOne, increment, incrementAsync }; 32 | }, 33 | 34 | render(this: Vue & Inject) { 35 | const { store, plusOne } = this; 36 | return ( 37 |
38 |
count: {store.state.count}
39 |
plusOne: {plusOne}
40 | 41 | 42 |
43 | ); 44 | }, 45 | }); 46 | 47 | storiesOf('useStore', module) 48 | // @ts-ignore 49 | .add('docs', () => Docs) 50 | .add('demo', () => Demo); 51 | -------------------------------------------------------------------------------- /src/__stories__/useState.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent, computed } from '@vue/composition-api'; 6 | import { useStore, useState } from '..'; 7 | import { ShowDocs } from './components'; 8 | import { createStore } from '../mocks'; 9 | 10 | type Inject = { 11 | count: number; 12 | count2: number; 13 | plusOne: number; 14 | minusOne: number; 15 | }; 16 | 17 | const Docs = () => ; 18 | 19 | const Demo = defineComponent({ 20 | store: createStore(), 21 | 22 | setup() { 23 | const store = useStore(); 24 | const state = { 25 | ...useState(['count']), 26 | ...useState('test', { count2: 'count' }), 27 | }; 28 | 29 | const plusOne = computed(() => state.count.value + 1); 30 | const minusOne = computed(() => state.count2.value - 1); 31 | 32 | store.value.dispatch('incrementAsync'); 33 | store.value.dispatch('test/decrementAsync'); 34 | 35 | return { ...state, plusOne, minusOne }; 36 | }, 37 | 38 | render(this: Vue & Inject) { 39 | const { count, count2, plusOne, minusOne } = this; 40 | return ( 41 |
42 |
count: {count}
43 |
plusOne: {plusOne}
44 |
test/count: {count2}
45 |
test/minusOne: {minusOne}
46 |
47 | ); 48 | }, 49 | }); 50 | 51 | storiesOf('useState', module) 52 | // @ts-ignore 53 | .add('docs', () => Docs) 54 | .add('demo', () => Demo); 55 | -------------------------------------------------------------------------------- /src/__tests__/useStore.test.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex'; 2 | import { useStore } from '..'; 3 | import renderHook from '../util/renderHook'; 4 | 5 | describe('useStore', () => { 6 | it('should be defined', () => { 7 | expect(useStore).toBeDefined(); 8 | }); 9 | 10 | it('should have store property', () => { 11 | const { vm } = renderHook(() => ({ store: useStore() })); 12 | expect(vm).toHaveProperty('store'); 13 | }); 14 | 15 | it('should update store', () => { 16 | type Inject = { store: Store }; 17 | const { vm } = renderHook(() => ({ store: useStore() })); 18 | expect(vm.store.state.count).toBe(0); 19 | expect(vm.store.getters.plusOne).toBe(1); 20 | expect(vm.store.state.test.count).toBe(0); 21 | expect(vm.store.getters['test/minusOne']).toBe(-1); 22 | 23 | vm.store.commit('increment'); 24 | vm.store.commit('test/decrement'); 25 | 26 | expect(vm.store.state.count).toBe(1); 27 | expect(vm.store.getters.plusOne).toBe(2); 28 | expect(vm.store.state.test.count).toBe(-1); 29 | expect(vm.store.getters['test/minusOne']).toBe(-2); 30 | 31 | vm.store.dispatch('incrementAsync', 0); 32 | vm.store.dispatch('test/decrementAsync', 0); 33 | 34 | expect(vm.store.state.count).toBe(1); 35 | expect(vm.store.getters.plusOne).toBe(2); 36 | expect(vm.store.state.test.count).toBe(-1); 37 | expect(vm.store.getters['test/minusOne']).toBe(-2); 38 | 39 | setTimeout(() => { 40 | expect(vm.store.state.count).toBe(2); 41 | expect(vm.store.getters.plusOne).toBe(3); 42 | expect(vm.store.state.test.count).toBe(-2); 43 | expect(vm.store.getters['test/minusOne']).toBe(-3); 44 | }, 0); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/usePrevious.test.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { ref, reactive } from '@vue/composition-api'; 3 | import { usePrevious } from '..'; 4 | import renderHook from '../util/renderHook'; 5 | 6 | describe('usePrevious', () => { 7 | it('should be defined', () => { 8 | expect(usePrevious).toBeDefined(); 9 | }); 10 | 11 | it('should be previous wrapper count', async () => { 12 | const { vm } = renderHook(() => { 13 | const count = ref(0); 14 | const prevCount = usePrevious(count); 15 | 16 | expect(count.value).toBe(0); 17 | expect(prevCount.value).toBeUndefined(); 18 | 19 | count.value += 1; 20 | 21 | return { 22 | count, 23 | prevCount, 24 | }; 25 | }); 26 | 27 | await Vue.nextTick(); 28 | expect(vm.count).toBe(1); 29 | expect(vm.prevCount).toBe(0); 30 | 31 | vm.count -= 1; 32 | 33 | await Vue.nextTick(); 34 | expect(vm.count).toBe(0); 35 | expect(vm.prevCount).toBe(1); 36 | }); 37 | 38 | it('should be previous state count', async () => { 39 | const { vm } = renderHook(() => { 40 | const state = reactive({ count: 0 }); 41 | const prevCount = usePrevious(() => state.count); 42 | 43 | expect(state.count).toBe(0); 44 | expect(prevCount.value).toBeUndefined(); 45 | 46 | state.count += 1; 47 | 48 | return { 49 | state, 50 | prevCount, 51 | }; 52 | }); 53 | 54 | await Vue.nextTick(); 55 | expect(vm.state.count).toBe(1); 56 | expect(vm.prevCount).toBe(0); 57 | 58 | vm.state.count -= 1; 59 | 60 | await Vue.nextTick(); 61 | expect(vm.state.count).toBe(0); 62 | expect(vm.prevCount).toBe(1); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /docs/useGetters.md: -------------------------------------------------------------------------------- 1 | # useGetters 2 | 3 | > You need to [use a plugin](https://github.com/u3u/vue-hooks#usage) before using this hook. 4 | 5 | Vue hook for [`mapGetters`](https://vuex.vuejs.org/api/#mapgetters). 6 | 7 | ## Usage 8 | 9 | ```jsx {8,9,15,19} 10 | import { defineComponent } from '@vue/composition-api'; 11 | import { useStore, useGetters } from '@hanxx/vue-hooks'; 12 | 13 | const Demo = defineComponent({ 14 | setup() { 15 | const store = useStore(); 16 | const getters = { 17 | ...useGetters(['plusOne']), 18 | ...useGetters('test', ['minusOne']), 19 | }; 20 | 21 | store.value.dispatch('incrementAsync'); 22 | store.value.dispatch('test/decrementAsync'); 23 | 24 | return { ...getters }; 25 | }, 26 | 27 | render() { 28 | const { plusOne, minusOne } = this; 29 | return ( 30 |
31 |
plusOne: {plusOne}
32 |
test/minusOne: {minusOne}
33 |
34 | ); 35 | }, 36 | }); 37 | ``` 38 | 39 | ## Reference 40 | 41 | ```typescript 42 | function useGetters( 43 | namespace?: string, 44 | map: Array | Object, 45 | ): Object>; 46 | ``` 47 | 48 | > The usage of the `useGetters` hook is exactly the same as the usage of [`mapGetters`](https://vuex.vuejs.org/api/#mapgetters) (the same parameters) 49 | > The only difference is that the return value of `useGetters` is the [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) dictionary. For each item in the dictionary, you need to use `.value` to get its actual value. 50 | 51 | _Please refer to the documentation of [`mapGetters`](https://vuex.vuejs.org/api/#mapgetters) for details._ 52 | -------------------------------------------------------------------------------- /docs/useCounter.md: -------------------------------------------------------------------------------- 1 | # useCounter 2 | 3 | Vue hook that tracks a numeric value. 4 | 5 | ## Usage 6 | 7 | ```jsx {6,17} 8 | import { defineComponent } from '@vue/composition-api'; 9 | import { useCounter } from '@hanxx/vue-hooks'; 10 | 11 | const Demo = defineComponent({ 12 | setup() { 13 | const [count, { inc, dec, set, reset }] = useCounter(); 14 | return { 15 | count, 16 | inc, 17 | dec, 18 | set, 19 | reset, 20 | }; 21 | }, 22 | 23 | render() { 24 | const { count, inc, dec, set, reset } = this; 25 | return ( 26 |
27 |
count: {count}
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | ); 37 | }, 38 | }); 39 | ``` 40 | 41 | ## Reference 42 | 43 | ```typescript {6-10} 44 | function useCounter( 45 | initialValue?: number, 46 | ): [ 47 | Ref, 48 | { 49 | inc: (delta?: number) => number; 50 | dec: (delta?: number) => number; 51 | get: () => number; 52 | set: (val: number) => number; 53 | reset: (val?: number) => number; 54 | }, 55 | ]; 56 | ``` 57 | 58 | ### `Arguments` 59 | 60 | - `initialValue` 61 | 62 | Initial value of the counter. 63 | 64 | - Type: `number` 65 | - Default: `0` 66 | 67 | ### `ReturnValue` 68 | 69 | 0. [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) 70 | 1. `Actions` 71 | -------------------------------------------------------------------------------- /docs/useState.md: -------------------------------------------------------------------------------- 1 | # useState 2 | 3 | > You need to [use a plugin](https://github.com/u3u/vue-hooks#usage) before using this hook. 4 | 5 | Vue hook for [`mapState`](https://vuex.vuejs.org/api/#mapstate). 6 | 7 | ## Usage 8 | 9 | ```jsx {7,8,14,18} 10 | import { defineComponent, computed } from '@vue/composition-api'; 11 | import { useState } from '@hanxx/vue-hooks'; 12 | 13 | const Demo = defineComponent({ 14 | setup() { 15 | const state = { 16 | ...useState(['count']), 17 | ...useState('test', { count2: 'count' }), 18 | }; 19 | 20 | const plusOne = computed(() => state.count.value + 1); 21 | const minusOne = computed(() => state.count2.value - 1); 22 | 23 | return { ...state, plusOne, minusOne }; 24 | }, 25 | 26 | render() { 27 | const { count, count2, plusOne, minusOne } = this; 28 | return ( 29 |
30 |
count: {count}
31 |
plusOne: {plusOne}
32 |
test/count: {count2}
33 |
test/minusOne: {minusOne}
34 |
35 | ); 36 | }, 37 | }); 38 | ``` 39 | 40 | ## Reference 41 | 42 | ```typescript 43 | function useState( 44 | namespace?: string, 45 | map: Array | Object, 46 | ): Object>; 47 | ``` 48 | 49 | > The usage of the `useState` hook is exactly the same as the usage of [`mapState`](https://vuex.vuejs.org/api/#mapstate) (the same parameters) 50 | > The only difference is that the return value of `useState` is the [`Ref`](https://github.com/vuejs/composition-api/blob/a7a68bda5d32139c6cf05b45e385cf8d4ce86707/src/reactivity/ref.ts#L8-L10) dictionary. For each item in the dictionary, you need to use `.value` to get its actual value. 51 | 52 | _Please refer to the documentation of [`mapState`](https://vuex.vuejs.org/api/#mapstate) for details._ 53 | -------------------------------------------------------------------------------- /.storybook/style.css: -------------------------------------------------------------------------------- 1 | .markdown-body, 2 | .sb-show-main { 3 | color: rgba(255, 255, 255, 0.8); 4 | } 5 | 6 | .markdown-body { 7 | box-sizing: border-box; 8 | min-width: 200px; 9 | max-width: 980px; 10 | margin: 0 auto; 11 | padding: 45px; 12 | } 13 | 14 | @media (max-width: 767px) { 15 | .markdown-body { 16 | padding: 15px; 17 | } 18 | } 19 | 20 | .markdown-body h1, 21 | .markdown-body h2 { 22 | border-bottom-color: rgba(255, 255, 255, 0.1); 23 | } 24 | 25 | .markdown-body pre { 26 | position: relative; 27 | } 28 | 29 | .sb-show-main a, 30 | .markdown-body a { 31 | color: #1ea7fd; 32 | transition: color 300ms ease; 33 | } 34 | 35 | a.router-link-exact-active { 36 | color: #999; 37 | text-decoration: none; 38 | pointer-events: none; 39 | } 40 | 41 | .fade-enter-active { 42 | animation: fade 300ms ease; 43 | } 44 | 45 | .fade-leave-active { 46 | animation: fade 300ms ease reverse; 47 | } 48 | 49 | @keyframes fade { 50 | from { 51 | opacity: 0; 52 | } 53 | 54 | to { 55 | opacity: 1; 56 | } 57 | } 58 | 59 | .highlighted-line { 60 | display: block; 61 | margin: 0 -1em; 62 | padding: 0 1em; 63 | background-color: #3d3d3d; 64 | } 65 | 66 | pre[class*='language-'], 67 | code[class*='language-'] { 68 | font-family: Menlo, Andale Mono, Consolas, Courier, monospace; 69 | font-size: 85%; 70 | } 71 | 72 | code[class*='language-']::before { 73 | position: absolute; 74 | z-index: 3; 75 | top: 0.8em; 76 | right: 1em; 77 | font-size: 0.75rem; 78 | color: rgba(255, 255, 255, 0.4); 79 | } 80 | 81 | code[class*='language-javascript']::before { 82 | content: 'js'; 83 | } 84 | 85 | code[class*='language-jsx']::before { 86 | content: 'jsx'; 87 | } 88 | 89 | code[class*='language-typescript']::before { 90 | content: 'ts'; 91 | } 92 | 93 | code[class*='language-tsx']::before { 94 | content: 'tsx'; 95 | } 96 | -------------------------------------------------------------------------------- /docs/useMutations.md: -------------------------------------------------------------------------------- 1 | # useMutations 2 | 3 | > You need to [use a plugin](https://github.com/u3u/vue-hooks#usage) before using this hook. 4 | 5 | Vue hook for [`mapMutations`](https://vuex.vuejs.org/api/#mapmutations). 6 | 7 | ## Usage 8 | 9 | ```jsx {17,18,24,29,36,37} 10 | import { defineComponent } from '@vue/composition-api'; 11 | import { useState, useGetters, useMutations } from '@hanxx/vue-hooks'; 12 | 13 | const Demo = defineComponent({ 14 | setup() { 15 | const state = { 16 | ...useState(['count']), 17 | ...useState('test', { count2: 'count' }), 18 | }; 19 | 20 | const getters = { 21 | ...useGetters(['plusOne']), 22 | ...useGetters('test', ['minusOne']), 23 | }; 24 | 25 | const mutations = { 26 | ...useMutations(['increment']), 27 | ...useMutations('test', ['decrement']), 28 | }; 29 | 30 | return { 31 | ...state, 32 | ...getters, 33 | ...mutations, 34 | }; 35 | }, 36 | 37 | render() { 38 | const { count, count2, plusOne, minusOne } = this; 39 | return ( 40 |
41 |
count: {count}
42 |
plusOne: {plusOne}
43 |
test/count: {count2}
44 |
test/minusOne: {minusOne}
45 | 46 | 47 |
48 | ); 49 | }, 50 | }); 51 | ``` 52 | 53 | ## Reference 54 | 55 | ```typescript 56 | function mapMutations( 57 | namespace?: string, 58 | map: Array | Object, 59 | ): Object; 60 | ``` 61 | 62 | > The usage of the `useMutations` hook is exactly the same as the usage of [`mapMutations`](https://vuex.vuejs.org/api/#mapmutations). 63 | 64 | _Please refer to the documentation of [`mapMutations`](https://vuex.vuejs.org/api/#mapmutations) for details._ 65 | -------------------------------------------------------------------------------- /src/__stories__/useMutations.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useState, useGetters, useMutations } from '..'; 7 | import { ShowDocs } from './components'; 8 | import { createStore } from '../mocks'; 9 | 10 | type Inject = { 11 | count: number; 12 | count2: number; 13 | plusOne: number; 14 | minusOne: number; 15 | increment: () => void; 16 | decrement: () => void; 17 | }; 18 | 19 | const Docs = () => ; 20 | 21 | const Demo = defineComponent({ 22 | store: createStore(), 23 | 24 | setup() { 25 | const state = { 26 | ...useState(['count']), 27 | ...useState('test', { count2: 'count' }), 28 | }; 29 | 30 | const getters = { 31 | ...useGetters(['plusOne']), 32 | ...useGetters('test', ['minusOne']), 33 | }; 34 | 35 | const mutations = { 36 | ...useMutations(['increment']), 37 | ...useMutations('test', ['decrement']), 38 | }; 39 | 40 | return { 41 | ...state, 42 | ...getters, 43 | ...mutations, 44 | }; 45 | }, 46 | 47 | render(this: Vue & Inject) { 48 | const { count, count2, plusOne, minusOne } = this; 49 | return ( 50 |
51 |
count: {count}
52 |
plusOne: {plusOne}
53 |
test/count: {count2}
54 |
test/minusOne: {minusOne}
55 | 56 | 57 |
58 | ); 59 | }, 60 | }); 61 | 62 | storiesOf('useMutations', module) 63 | // @ts-ignore 64 | .add('docs', () => Docs) 65 | .add('demo', () => Demo); 66 | -------------------------------------------------------------------------------- /docs/useActions.md: -------------------------------------------------------------------------------- 1 | # useActions 2 | 3 | > You need to [use a plugin](https://github.com/u3u/vue-hooks#usage) before using this hook. 4 | 5 | Vue hook for [`mapActions`](https://vuex.vuejs.org/api/#mapactions). 6 | 7 | ## Usage 8 | 9 | ```jsx {17,18,24,29,36,37} 10 | import { defineComponent } from '@vue/composition-api'; 11 | import { useState, useGetters, useActions } from '@hanxx/vue-hooks'; 12 | 13 | const Demo = defineComponent({ 14 | setup() { 15 | const state = { 16 | ...useState(['count']), 17 | ...useState('test', { count2: 'count' }), 18 | }; 19 | 20 | const getters = { 21 | ...useGetters(['plusOne']), 22 | ...useGetters('test', ['minusOne']), 23 | }; 24 | 25 | const actions = { 26 | ...useActions(['incrementAsync']), 27 | ...useActions('test', ['decrementAsync']), 28 | }; 29 | 30 | return { 31 | ...state, 32 | ...getters, 33 | ...actions, 34 | }; 35 | }, 36 | 37 | render() { 38 | const { count, count2, plusOne, minusOne } = this; 39 | return ( 40 |
41 |
count: {count}
42 |
plusOne: {plusOne}
43 |
test/count: {count2}
44 |
test/minusOne: {minusOne}
45 | 46 | 49 |
50 | ); 51 | }, 52 | }); 53 | ``` 54 | 55 | ## Reference 56 | 57 | ```typescript 58 | function useActions( 59 | namespace?: string, 60 | map: Array | Object, 61 | ): Object; 62 | ``` 63 | 64 | > The usage of the `useActions` hook is exactly the same as the usage of [`mapActions`](https://vuex.vuejs.org/api/#mapactions). 65 | 66 | _Please refer to the documentation of [`mapActions`](https://vuex.vuejs.org/api/#mapactions) for details._ 67 | -------------------------------------------------------------------------------- /src/__tests__/useWindowSize.test.ts: -------------------------------------------------------------------------------- 1 | import { useWindowSize } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | 4 | interface InjectWindowSize { 5 | width: number; 6 | height: number; 7 | widthPixel: string; 8 | heightPixel: string; 9 | } 10 | 11 | enum SizeType { 12 | width = 'width', 13 | height = 'height', 14 | } 15 | 16 | const resize = { 17 | [SizeType.width](value: number) { 18 | (window.innerWidth as number) = value; 19 | }, 20 | [SizeType.height](value: number) { 21 | (window.innerHeight as number) = value; 22 | }, 23 | }; 24 | 25 | // simulate window resize 26 | function triggerResize(type: SizeType, value = 0) { 27 | const dispatchResize = resize[type]; 28 | dispatchResize(value); 29 | window.dispatchEvent(new Event('resize')); 30 | } 31 | 32 | describe('useWindowSize', () => { 33 | it('should be defined', () => { 34 | expect(useWindowSize).toBeDefined(); 35 | }); 36 | 37 | it('should update width', () => { 38 | const { vm } = renderHook(useWindowSize); 39 | triggerResize(SizeType.width, 1280); 40 | expect(vm.width).toBe(1280); 41 | expect(vm.widthPixel).toBe('1280px'); 42 | triggerResize(SizeType.width, 375); 43 | expect(vm.width).toBe(375); 44 | expect(vm.widthPixel).toBe('375px'); 45 | }); 46 | 47 | it('should update height', () => { 48 | const { vm } = renderHook(useWindowSize); 49 | triggerResize(SizeType.height, 800); 50 | expect(vm.height).toBe(800); 51 | expect(vm.heightPixel).toBe('800px'); 52 | triggerResize(SizeType.height, 667); 53 | expect(vm.height).toBe(667); 54 | expect(vm.heightPixel).toBe('667px'); 55 | }); 56 | 57 | it('should remove the listener', () => { 58 | const { vm } = renderHook(useWindowSize); 59 | triggerResize(SizeType.width, 750); 60 | vm.$destroy(); 61 | triggerResize(SizeType.width, 375); 62 | expect(vm.width).toBe(750); 63 | expect(vm.widthPixel).toBe('750px'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/helpers/vuex/interface.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Dispatch, Commit } from 'vuex'; 3 | import { Ref } from '@vue/composition-api'; 4 | 5 | type Dictionary = { [key: string]: T }; 6 | type Computed = Ref; 7 | type MutationMethod = (...args: any[]) => void; 8 | type ActionMethod = (...args: any[]) => Promise; 9 | type CustomVue = Vue & Dictionary; 10 | 11 | interface Mapper { 12 | (map: string[]): Dictionary; 13 | (map: Dictionary): Dictionary; 14 | } 15 | 16 | interface MapperWithNamespace { 17 | (namespace: string, map: string[]): Dictionary; 18 | (namespace: string, map: Dictionary): Dictionary; 19 | } 20 | 21 | interface FunctionMapper { 22 | ( 23 | map: Dictionary<(this: CustomVue, fn: F, ...args: any[]) => any>, 24 | ): Dictionary; 25 | } 26 | 27 | interface FunctionMapperWithNamespace { 28 | ( 29 | namespace: string, 30 | map: Dictionary<(this: CustomVue, fn: F, ...args: any[]) => any>, 31 | ): Dictionary; 32 | } 33 | 34 | interface MapperForState { 35 | ( 36 | map: Dictionary<(this: CustomVue, state: S, getters: any) => any>, 37 | ): Dictionary; 38 | } 39 | 40 | interface MapperForStateWithNamespace { 41 | ( 42 | namespace: string, 43 | map: Dictionary<(this: CustomVue, state: S, getters: any) => any>, 44 | ): Dictionary; 45 | } 46 | 47 | export type useState = Mapper & 48 | MapperWithNamespace & 49 | MapperForState & 50 | MapperForStateWithNamespace; 51 | 52 | export type useGetters = Mapper & MapperWithNamespace; 53 | 54 | export type useMutations = Mapper & 55 | MapperWithNamespace & 56 | FunctionMapper & 57 | FunctionMapperWithNamespace; 58 | 59 | export type useActions = Mapper & 60 | MapperWithNamespace & 61 | FunctionMapper & 62 | FunctionMapperWithNamespace; 63 | -------------------------------------------------------------------------------- /src/__stories__/useActions.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | import 'vue-tsx-support/enable-check'; 3 | import Vue from 'vue'; 4 | import { storiesOf } from '@storybook/vue'; 5 | import { defineComponent } from '@vue/composition-api'; 6 | import { useState, useGetters, useActions } from '..'; 7 | import { ShowDocs } from './components'; 8 | import { createStore } from '../mocks'; 9 | 10 | type Inject = { 11 | count: number; 12 | count2: number; 13 | plusOne: number; 14 | minusOne: number; 15 | incrementAsync: (delay?: number) => void; 16 | decrementAsync: (delay?: number) => void; 17 | }; 18 | 19 | const Docs = () => ; 20 | 21 | const Demo = defineComponent({ 22 | store: createStore(), 23 | 24 | setup() { 25 | const state = { 26 | ...useState(['count']), 27 | ...useState('test', { count2: 'count' }), 28 | }; 29 | 30 | const getters = { 31 | ...useGetters(['plusOne']), 32 | ...useGetters('test', ['minusOne']), 33 | }; 34 | 35 | const actions = { 36 | ...useActions(['incrementAsync']), 37 | ...useActions('test', ['decrementAsync']), 38 | }; 39 | 40 | return { 41 | ...state, 42 | ...getters, 43 | ...actions, 44 | }; 45 | }, 46 | 47 | render(this: Vue & Inject) { 48 | const { count, count2, plusOne, minusOne } = this; 49 | return ( 50 |
51 |
count: {count}
52 |
plusOne: {plusOne}
53 |
test/count: {count2}
54 |
test/minusOne: {minusOne}
55 | 56 | 59 |
60 | ); 61 | }, 62 | }); 63 | 64 | storiesOf('useActions', module) 65 | // @ts-ignore 66 | .add('docs', () => Docs) 67 | .add('demo', () => Demo); 68 | -------------------------------------------------------------------------------- /.vscode/docs.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "storybook": { 3 | "scope": "typescriptreact", 4 | "prefix": "docs", 5 | "body": [ 6 | "/* eslint import/no-extraneous-dependencies: off */", 7 | "import 'vue-tsx-support/enable-check';", 8 | "import Vue from 'vue';", 9 | "import { storiesOf } from '@storybook/vue';", 10 | "import { defineComponent } from '@vue/composition-api';", 11 | "import { use${1:Hook} } from '..';", 12 | "import { ShowDocs } from './components';", 13 | "", 14 | "type Inject = {", 15 | " ${2:data}: ${3:any};", 16 | "};", 17 | "", 18 | "const Docs = () => ;", 19 | "", 20 | "const Demo = defineComponent({", 21 | " setup() {", 22 | " const ${2:data} = use${1:Hook}();", 23 | " return ${2:data};", 24 | " },", 25 | "", 26 | " render(this: Vue & Inject) {", 27 | " const { ${2:data} } = this;", 28 | " return
{${2:data}}
;", 29 | " },", 30 | "});", 31 | "", 32 | "storiesOf('use${1:Hook}', module)", 33 | " // @ts-ignore", 34 | " .add('docs', () => Docs)", 35 | " .add('demo', () => Demo);", 36 | "" 37 | ], 38 | "description": "storybook for hook" 39 | }, 40 | "docs": { 41 | "scope": "markdown", 42 | "prefix": "docs", 43 | "body": [ 44 | "# use${1:Hook}", 45 | "", 46 | "${2:Introduction...}", 47 | "", 48 | "## Usage", 49 | "", 50 | "```jsx {6,11}", 51 | "import { defineComponent } from '@vue/composition-api';", 52 | "import { use${1:Hook} } from '@hanxx/vue-hooks';", 53 | "", 54 | "const Demo = defineComponent({", 55 | " setup() {", 56 | " const ${3:data} = use${1:Hook}();", 57 | " return ${3:data};", 58 | " },", 59 | "", 60 | " render() {", 61 | " const { ${3:data} } = this;", 62 | " return
{${3:data}}
;", 63 | " },", 64 | "});", 65 | "```", 66 | "" 67 | ], 68 | "description": "docs for hook" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/useAsync.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, onMounted } from '@vue/composition-api'; 2 | 3 | export interface ReturnValue { 4 | loading: Ref; 5 | error: Ref; 6 | resp: Ref; 7 | run: (...args: any[]) => Promise; 8 | } 9 | 10 | export interface Options { 11 | manual?: boolean; 12 | initialData?: Result; 13 | onSuccess?: (data: Result, params?: any[]) => void; 14 | onError?: (e: Error, params?: any[]) => void; 15 | params?: any[]; 16 | } 17 | 18 | function useAsync( 19 | fn: (...args: any[]) => Promise, 20 | options?: Options, 21 | ): ReturnValue { 22 | // initial state 23 | const defaultOptions = options || ({} as Options); 24 | const { manual, onError, onSuccess, params = [] } = defaultOptions; 25 | const initialData = defaultOptions.initialData || ({} as Result); 26 | 27 | const loading = ref(!manual); 28 | const error = ref(null); 29 | const data = ref(initialData); 30 | 31 | // execute async function 32 | function run(...args: any[]) { 33 | loading.value = true; 34 | return fn(...args) 35 | .then((resp: Result) => { 36 | if (resp) { 37 | data.value = resp; 38 | return resp; 39 | } 40 | return initialData; 41 | }) 42 | .then((returns: Result) => { 43 | if (typeof onSuccess === 'function') { 44 | onSuccess(returns, params); 45 | } 46 | }) 47 | .catch((err: Error) => { 48 | error.value = err; 49 | if (typeof onError === 'function') { 50 | onError(err, params); 51 | } 52 | }) 53 | .finally(() => { 54 | loading.value = false; 55 | }); 56 | } 57 | 58 | function start(...args: any[]): Promise { 59 | if (manual) { 60 | return run(...args); 61 | } 62 | return Promise.resolve(initialData); 63 | } 64 | 65 | onMounted(() => { 66 | if (!manual) { 67 | run(...params); 68 | } 69 | }); 70 | 71 | return { 72 | loading, 73 | error, 74 | resp: data, 75 | run: start, 76 | }; 77 | } 78 | 79 | export default useAsync; 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2020-05-24) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** move to dev dependencies ([8a64b14](https://github.com/lianghx-319/vue-hooks/commit/8a64b14)) 7 | * **hooks:** `vm.$router` should not be computed wrapper ([8483d43](https://github.com/lianghx-319/vue-hooks/commit/8483d43)) 8 | * **hooks:** fix `usePrevious` does not work when passing state ([8567a04](https://github.com/lianghx-319/vue-hooks/commit/8567a04)) 9 | * **hooks:** improve vuex interface ([fdae9b1](https://github.com/lianghx-319/vue-hooks/commit/fdae9b1)) 10 | * **hooks:** improve vuex interface again ([87bd2a5](https://github.com/lianghx-319/vue-hooks/commit/87bd2a5)) 11 | * **hooks:** remove useCounter typeof and use as const ([#132](https://github.com/lianghx-319/vue-hooks/issues/132)) ([721dfff](https://github.com/lianghx-319/vue-hooks/commit/721dfff)) 12 | * **hooks:** using runtime plugin ([e0f556d](https://github.com/lianghx-319/vue-hooks/commit/e0f556d)) 13 | * **release:** semantic release ([c5d2ee8](https://github.com/lianghx-319/vue-hooks/commit/c5d2ee8)) 14 | * **ts:** forgot to export the type definition file ([5cb25ec](https://github.com/lianghx-319/vue-hooks/commit/5cb25ec)) 15 | * **util:** `vm` must have a `setup` function ([d6ffe8b](https://github.com/lianghx-319/vue-hooks/commit/d6ffe8b)) 16 | 17 | 18 | ### Features 19 | 20 | * replace with vue composition api ([5ee9248](https://github.com/lianghx-319/vue-hooks/commit/5ee9248)), closes [#52](https://github.com/lianghx-319/vue-hooks/issues/52) 21 | * **hooks:** add `useActions` hook ([1106ade](https://github.com/lianghx-319/vue-hooks/commit/1106ade)) 22 | * **hooks:** add `useCounter` hook ([200086e](https://github.com/lianghx-319/vue-hooks/commit/200086e)) 23 | * **hooks:** add `useGetters` hook ([49376cb](https://github.com/lianghx-319/vue-hooks/commit/49376cb)) 24 | * **hooks:** add `useMedia` hook ([dee851e](https://github.com/lianghx-319/vue-hooks/commit/dee851e)) 25 | * **hooks:** add `useMountedState` hook ([1e70d5e](https://github.com/lianghx-319/vue-hooks/commit/1e70d5e)) 26 | * **hooks:** add `useMutations` hook ([b38358c](https://github.com/lianghx-319/vue-hooks/commit/b38358c)) 27 | * **hooks:** add `usePrevious` hook ([f4d0dc8](https://github.com/lianghx-319/vue-hooks/commit/f4d0dc8)) 28 | * **hooks:** add `useRef` hook ([f600ab8](https://github.com/lianghx-319/vue-hooks/commit/f600ab8)) 29 | * **hooks:** add `useRouter` hook ([df8bdda](https://github.com/lianghx-319/vue-hooks/commit/df8bdda)) 30 | * **hooks:** add `useState` hook ([607dfd1](https://github.com/lianghx-319/vue-hooks/commit/607dfd1)) 31 | * **hooks:** add `useStore` hook ([109995a](https://github.com/lianghx-319/vue-hooks/commit/109995a)) 32 | * **hooks:** add `useTimeout` hook ([61db981](https://github.com/lianghx-319/vue-hooks/commit/61db981)) 33 | * **hooks:** add async hook ([#10](https://github.com/lianghx-319/vue-hooks/issues/10)) ([7e54ea5](https://github.com/lianghx-319/vue-hooks/commit/7e54ea5)) 34 | * **hooks:** export hooks in entry ([3f0d5dd](https://github.com/lianghx-319/vue-hooks/commit/3f0d5dd)) 35 | * **util:** add vue runtime ([f7ff827](https://github.com/lianghx-319/vue-hooks/commit/f7ff827)) 36 | 37 | 38 | ### Reverts 39 | 40 | * **hooks:** remove `useRef` hook ([999bc89](https://github.com/lianghx-319/vue-hooks/commit/999bc89)) 41 | 42 | 43 | ### BREAKING CHANGES 44 | 45 | * replace to [`@vue/composition-api`](https://github.com/vuejs/composition-api) 46 | -------------------------------------------------------------------------------- /src/__stories__/useRouter.story.tsx: -------------------------------------------------------------------------------- 1 | /* eslint spaced-comment: off, import/no-extraneous-dependencies: off */ 2 | /// 3 | import 'vue-tsx-support/enable-check'; 4 | import Vue from 'vue'; 5 | import VueRouter, { Route } from 'vue-router'; 6 | import { storiesOf } from '@storybook/vue'; 7 | import { 8 | defineComponent, 9 | ref, 10 | watch, 11 | onMounted, 12 | onUnmounted, 13 | } from '@vue/composition-api'; 14 | import { useRouter } from '..'; 15 | import { ShowDocs } from './components'; 16 | 17 | type Inject = { 18 | time: number; 19 | route: Route; 20 | router: VueRouter; 21 | }; 22 | 23 | const Docs = () => ; 24 | 25 | const Home: any = () => ( 26 | 27 | 32 | 33 | ); 34 | 35 | const About: any = () => ( 36 | 42 | ); 43 | 44 | const NotFound: any = () => ( 45 | 46 | 51 | 52 | ); 53 | 54 | const Demo = defineComponent({ 55 | router: new VueRouter({ 56 | routes: [ 57 | { 58 | path: '/', 59 | name: 'index', 60 | meta: { title: 'Home Page', next: '/about' }, 61 | component: Home, 62 | }, 63 | { 64 | path: '/about', 65 | name: 'about', 66 | meta: { title: 'About Page', next: '/github' }, 67 | component: About, 68 | }, 69 | { 70 | path: '*', 71 | name: '404', 72 | meta: { title: '404 - Not Found', next: '/' }, 73 | component: NotFound, 74 | }, 75 | ], 76 | }), 77 | 78 | setup() { 79 | const { route, router } = useRouter(); 80 | const time = ref(5); 81 | let timerId; 82 | 83 | watch(route, () => { 84 | time.value = 5; 85 | }); 86 | 87 | watch(time, () => { 88 | if (time.value <= 0) { 89 | router.replace(route.value.meta.next); 90 | } 91 | }); 92 | 93 | onMounted(() => { 94 | // eslint-disable-next-line no-plusplus 95 | timerId = window.setInterval(() => time.value--, 1e3); 96 | }); 97 | 98 | onUnmounted(() => { 99 | window.clearInterval(timerId); 100 | }); 101 | 102 | return { time, route, router }; 103 | }, 104 | 105 | render(this: Vue & Inject) { 106 | const { time, route } = this; 107 | 108 | return ( 109 |
110 | 119 |
120 |

121 | {route.meta.title} ({time}s) 122 |

123 | 124 | 125 | 126 | 127 | 128 |
129 |
130 | Route Details 131 |
    132 | {Object.keys(route).map((key) => ( 133 |
  • 134 | {key}:
    {JSON.stringify(route[key], null, 2)}
    135 |
  • 136 | ))} 137 |
138 |
139 |
140 | ); 141 | }, 142 | }); 143 | 144 | storiesOf('useRouter', module) 145 | // @ts-ignore 146 | .add('docs', () => Docs) 147 | .add('demo', () => Demo); 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hanxx/vue-hooks", 3 | "description": "⚡️ Awesome Vue Hooks", 4 | "version": "1.0.0", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "keywords": [ 9 | "vue", 10 | "vue-use", 11 | "vue-hooks", 12 | "vue-function-api", 13 | "vue-composition-api" 14 | ], 15 | "main": "lib/index.js", 16 | "module": "esm/index.js", 17 | "types": "lib/index.d.ts", 18 | "typings": "lib/index.d.ts", 19 | "files": [ 20 | "lib/", 21 | "esm/", 22 | "types" 23 | ], 24 | "scripts": { 25 | "start": "yarn storybook", 26 | "storybook": "start-storybook -p 3000", 27 | "storybook:build": "build-storybook", 28 | "test": "jest", 29 | "test:watch": "jest --watch", 30 | "build:cjs": "tsc", 31 | "build:es": "tsc -m esNext --outDir esm", 32 | "build": "yarn clean && yarn build:cjs && yarn build:es", 33 | "clean": "rimraf lib esm", 34 | "lint": "eslint 'src/**/*.ts'", 35 | "lint:types": "tsc --noEmit", 36 | "lint:prettier": "prettier '**/*.{ts,md,mdx}' --check", 37 | "format": "prettier '**/*.{ts,md,mdx}' --write", 38 | "release": "semantic-release", 39 | "prepublishOnly": "yarn test && yarn build" 40 | }, 41 | "authors": [ 42 | { 43 | "name": "u3u", 44 | "email": "qwq@qwq.cat", 45 | "url": "https://qwq.cat" 46 | }, 47 | { 48 | "name": "lianghx-319", 49 | "email": "xsytby1112@gmail.com" 50 | } 51 | ], 52 | "license": "MIT", 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/lianghx-319/vue-hooks" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/lianghx-319/vue-hooks/issues" 59 | }, 60 | "homepage": "https://github.com/lianghx-319/vue-hooks#readme", 61 | "engines": { 62 | "node": ">=10.16.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/core": "^7.5.5", 66 | "@babel/preset-env": "^7.5.5", 67 | "@babel/preset-typescript": "^7.3.3", 68 | "@commitlint/cli": "^8.1.0", 69 | "@commitlint/config-conventional": "^8.1.0", 70 | "@semantic-release/changelog": "^3.0.4", 71 | "@semantic-release/git": "^7.0.16", 72 | "@storybook/addon-knobs": "^5.1.11", 73 | "@storybook/addon-notes": "^5.1.11", 74 | "@storybook/theming": "^5.1.11", 75 | "@storybook/vue": "^5.1.11", 76 | "@types/jest": "^24.0.18", 77 | "@types/node": "^12.7.2", 78 | "@types/storybook__vue": "^5.0.2", 79 | "@typescript-eslint/parser": "^2.0.0", 80 | "@vue/babel-preset-jsx": "^1.1.0", 81 | "@vue/composition-api": "^0.6.4", 82 | "@vue/test-utils": "^1.0.0-beta.29", 83 | "all-contributors-cli": "^6.8.1", 84 | "babel-loader": "^8.0.6", 85 | "babel-preset-vue": "^2.0.2", 86 | "dayjs": "^1.8.28", 87 | "eslint": "^6.2.2", 88 | "eslint-config-airbnb-base": "^14.0.0", 89 | "eslint-config-prettier": "^6.1.0", 90 | "eslint-plugin-import": "^2.18.2", 91 | "eslint-plugin-prettier": "^3.1.0", 92 | "github-markdown-css": "^3.0.1", 93 | "jest": "^24.8.0", 94 | "lint-staged": "^9.2.3", 95 | "markdown-it": "^9.1.0", 96 | "markdown-it-highlight-lines": "^1.0.2", 97 | "markdown-it-link-attributes": "^3.0.0", 98 | "markdown-it-loader": "^0.7.0", 99 | "markdown-it-prism": "^2.0.2", 100 | "match-media-mock": "^0.1.1", 101 | "prettier": "^2.0.5", 102 | "prismjs": "^1.17.1", 103 | "rimraf": "^3.0.0", 104 | "semantic-release": "^15.13.24", 105 | "sinon": "^9.0.2", 106 | "typescript": "^3.5.3", 107 | "vue": "^2.6.10", 108 | "vue-loader": "^15.7.1", 109 | "vue-router": "^3.1.2", 110 | "vue-template-compiler": "^2.6.10", 111 | "vue-tsx-support": "^2.3.1", 112 | "vuex": "^3.1.1", 113 | "yorkie": "^2.0.0" 114 | }, 115 | "release": { 116 | "plugins": [ 117 | "@semantic-release/commit-analyzer", 118 | "@semantic-release/release-notes-generator", 119 | "@semantic-release/changelog", 120 | "@semantic-release/npm", 121 | "@semantic-release/git", 122 | "@semantic-release/github" 123 | ] 124 | }, 125 | "gitHooks": { 126 | "pre-commit": "lint-staged", 127 | "commit-msg": "commitlint -E GIT_PARAMS" 128 | }, 129 | "lint-staged": { 130 | "*.ts": [ 131 | "eslint --fix" 132 | ], 133 | "*.{ts,md,mdx}": [ 134 | "prettier --write", 135 | "prettier --check", 136 | "git add" 137 | ] 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/__tests__/useAsync.test.ts: -------------------------------------------------------------------------------- 1 | import { useAsync } from '..'; 2 | import renderHook from '../util/renderHook'; 3 | import { ReturnValue } from '../useAsync'; 4 | 5 | const asyncFunction = ( 6 | waiting: number, 7 | resolveValue?: any, 8 | shouldError?: boolean, 9 | ) => 10 | new Promise((resolve, reject) => { 11 | setTimeout(() => { 12 | if (shouldError) { 13 | reject(new Error('execute error')); 14 | } 15 | resolve(resolveValue); 16 | }, waiting); 17 | }); 18 | 19 | const sleep = (timer = 1) => 20 | new Promise((resolve) => setTimeout(resolve, timer)); 21 | 22 | const timer = 10; 23 | const result = 'success'; 24 | 25 | describe('useAsync', () => { 26 | it('should be defined', () => { 27 | expect(useAsync).toBeDefined(); 28 | }); 29 | 30 | it('should be executed while initialization', async () => { 31 | const { vm } = renderHook(() => { 32 | const { loading, resp } = useAsync(asyncFunction, { 33 | params: [timer, result], 34 | }); 35 | expect(loading.value).toBeTruthy(); 36 | expect(resp.value).toEqual({}); 37 | return { loading, resp }; 38 | }); 39 | 40 | await sleep(timer * 2); 41 | 42 | expect(vm.loading).toBeFalsy(); 43 | expect(vm.resp).toEqual('success'); 44 | }); 45 | 46 | it('should be executed after 1000ms', async () => { 47 | const { vm } = renderHook(() => { 48 | const { loading, run } = useAsync(asyncFunction, { 49 | manual: true, 50 | }); 51 | 52 | expect(loading.value).toBeFalsy(); 53 | 54 | return { loading, run }; 55 | }); 56 | 57 | vm.run(timer, result); 58 | 59 | expect(vm.loading).toBeTruthy(); 60 | 61 | await sleep(timer * 2); 62 | 63 | expect(vm.loading).toBeFalsy(); 64 | }); 65 | 66 | it('should not work executed with `run` while not manual', async () => { 67 | const { vm } = renderHook(() => { 68 | const { loading, run } = useAsync(asyncFunction, { 69 | manual: false, 70 | }); 71 | 72 | expect(loading.value).toBeTruthy(); 73 | 74 | return { loading, run }; 75 | }); 76 | 77 | await sleep(timer * 2); 78 | 79 | expect(vm.loading).toBeFalsy(); 80 | 81 | vm.run(); 82 | 83 | expect(vm.loading).toBeFalsy(); 84 | }); 85 | 86 | it('should be error', async () => { 87 | const { vm } = renderHook(() => { 88 | const { error } = useAsync(asyncFunction, { 89 | params: [timer, result, true], 90 | }); 91 | 92 | expect(error.value).toBeFalsy(); 93 | return { 94 | error, 95 | }; 96 | }); 97 | 98 | await sleep(timer * 2); 99 | 100 | expect(vm.error).toBeTruthy(); 101 | }); 102 | 103 | it('should initialData work', async () => { 104 | const initialData = { 105 | payload: [], 106 | code: 0, 107 | msg: 'ok', 108 | }; 109 | 110 | const { vm } = renderHook(() => { 111 | const { resp } = useAsync(asyncFunction, { 112 | initialData, 113 | params: [timer, result], 114 | }); 115 | expect(resp.value).toEqual(initialData); 116 | return { resp }; 117 | }); 118 | 119 | await sleep(timer * 2); 120 | 121 | expect(vm.resp).toEqual('success'); 122 | }); 123 | 124 | it('should execute onSuccess', async () => { 125 | const params = [timer, result]; 126 | const onSuccess = (resp, _params) => { 127 | expect(resp).toEqual('success'); 128 | expect(_params).toEqual(params); 129 | }; 130 | 131 | renderHook(() => { 132 | const { resp } = useAsync(asyncFunction, { 133 | onSuccess, 134 | params, 135 | }); 136 | expect(resp.value).toEqual({}); 137 | return { resp }; 138 | }); 139 | 140 | await sleep(timer * 2); 141 | 142 | expect.assertions(3); 143 | }); 144 | 145 | it('should execute onError', async () => { 146 | const params = [timer, result, true]; 147 | const onError = (error, _params) => { 148 | expect(error).toBeTruthy(); 149 | expect(_params).toEqual(params); 150 | }; 151 | 152 | renderHook(() => { 153 | const { resp } = useAsync(asyncFunction, { 154 | onError, 155 | params, 156 | }); 157 | expect(resp.value).toEqual({}); 158 | return { resp }; 159 | }); 160 | 161 | await sleep(timer * 2); 162 | 163 | expect.assertions(3); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-hooks [![NPM Version](https://badgen.net/npm/v/@hanxx/vue-hooks)](https://www.npmjs.com/package/@hanxx/vue-hooks) [![Bundle Size](https://badgen.net/bundlephobia/minzip/@hanxx/vue-hooks)](https://bundlephobia.com/result?p=@hanxx/vue-hooks) [![Build Status](https://img.shields.io/travis/lianghx-319/vue-hooks/master.svg)](https://travis-ci.org/lianghx-319/vue-hooks) [![Code Coverage](https://img.shields.io/codecov/c/github/lianghx-319/vue-hooks.svg)](https://codecov.io/gh/lianghx-319/vue-hooks) [![Total alerts](https://img.shields.io/lgtm/alerts/g/lianghx-319/vue-hooks.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/lianghx-319/vue-hooks/alerts/) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/lianghx-319/vue-hooks.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/lianghx-319/vue-hooks/context:javascript) [![Netlify Status](https://api.netlify.com/api/v1/badges/24ca2187-6118-491b-b4d1-684a823b3565/deploy-status)](https://app.netlify.com/sites/nervous-noyce-43dffd/deploys) 2 | 3 | > ⚡️ Awesome Vue Hooks Fork From [@u3u/vue-hooks](https://github.com/u3u/vue-hooks) 4 | 5 | First of all, the original repository has no response for more features request, so I start this repository and add some common useful features. Welcome PRs. 6 | 7 | Using [`@vue/composition-api`](https://github.com/vuejs/composition-api) to implement useful vue hooks. 8 | Vue 3.0 has not been released yet, it allows you to use functional-based components in advance. 9 | 10 | Another useful vue-hooks repository is [vue-use-web](https://github.com/Tarektouati/vue-use-web), so some duplicated features will not implement here. 11 | 12 | ## Install 13 | 14 | ```sh 15 | yarn add @vue/composition-api @hanxx/vue-hooks 16 | ``` 17 | 18 | ## Documentation 19 | 20 | Docs are available at 21 | 22 | ## Usage 23 | 24 | > Now don't need `Vue.use(hooks)` to install plugin 25 | 26 | ```js 27 | // main.js 28 | import Vue from 'vue'; 29 | import VueCompositionAPI from '@vue/composition-api'; 30 | 31 | Vue.use(VueCompositionAPI); // Don't forget to use the plugin! 32 | ``` 33 | 34 | > If use `useDate`, remember installing `dayjs` 35 | 36 | ```sh 37 | yarn add dayjs 38 | ``` 39 | 40 | ```js 41 | // You can use dayjs directly here 42 | import { useDate, dayjs } from '@hanxx/vue-hooks/lib/useDate'; 43 | ``` 44 | 45 | > If haven't use `useDate`, `dayjs` is not a necessary dependence 46 | 47 | ```jsx 48 | import { defineComponent } from '@vue/composition-api'; 49 | import { useWindowSize } from '@hanxx/vue-hooks'; 50 | 51 | export default defineComponent({ 52 | setup() { 53 | const { width, height, widthPixel, heightPixel } = useWindowSize(); 54 | return { width, height, widthPixel, heightPixel }; 55 | }, 56 | 57 | render() { 58 | const { width, height, widthPixel, heightPixel } = this; 59 | return ( 60 |
61 | dynamic window size: {width}, {height} 62 |
63 | ); 64 | }, 65 | }); 66 | ``` 67 | 68 | ## Contributing 69 | 70 | 1. Fork it! 71 | 2. Create your feature branch: `git checkout -b feat/new-hook` 72 | 3. Commit your changes: `git commit -am 'feat(hooks): add a new hook'` 73 | 4. Push to the branch: `git push origin feat/new-hook` 74 | 5. Submit a pull request :D 75 | 76 | ## Contributors 77 | 78 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |

u3u

💻 📖 💡 ⚠️

Han

💻
89 | 90 | 91 | 92 | 93 | 94 | 95 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome! 96 | 97 | ## License 98 | 99 | [MIT](./LICENSE) 100 | --------------------------------------------------------------------------------