├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .github ├── release-template.md ├── vue-condition-watcher_lifecycle.jpeg └── vue-conditions-watcher.gif ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README-zh_TW.md ├── README.md ├── _internal ├── composable │ ├── useCache.ts │ ├── useHistory.ts │ └── usePromiseQueue.ts ├── index.ts ├── package.json ├── tsconfig.json ├── types.ts └── utils │ ├── common.ts │ ├── createEvents.ts │ └── helper.ts ├── core ├── index.ts ├── package.json ├── tsconfig.json ├── types.ts └── use-condition-watcher.ts ├── examples ├── vue2 │ ├── .browserslistrc │ ├── .eslintrc.js │ ├── .gitignore │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.vue │ │ ├── api.js │ │ ├── main.js │ │ ├── router │ │ └── index.js │ │ └── views │ │ ├── Home.vue │ │ └── InfiniteScrolling.vue └── vue3 │ ├── index.html │ ├── package.json │ ├── shims-vue.d.ts │ ├── src │ ├── App.vue │ ├── api.ts │ ├── main.ts │ ├── router.ts │ ├── styles │ │ └── index.css │ └── views │ │ └── Home.vue │ ├── tsconfig.json │ └── vite.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── test ├── setup.ts ├── tsconfig.json ├── use-condition-watcher.test.ts └── utils.test.ts ├── tsconfig.json ├── turbo.json └── vitest.config.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | setup: 4 | docker: 5 | - image: gplane/pnpm:latest 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | key: dependency-cache-{{ checksum "pnpm-lock.yaml" }} 10 | - run: 11 | name: Install 12 | command: pnpm install --frozen-lockfile 13 | - save_cache: 14 | key: dependency-cache-{{ checksum "pnpm-lock.yaml" }} 15 | paths: 16 | - node_modules 17 | test: 18 | docker: 19 | - image: gplane/pnpm:latest 20 | steps: 21 | - checkout 22 | - restore_cache: 23 | key: dependency-cache-{{ checksum "pnpm-lock.yaml" }} 24 | - run: 25 | name: Test and Lint 26 | command: pnpm test:all 27 | workflows: 28 | version: 2 29 | build_and_test: 30 | jobs: 31 | - setup 32 | - test: 33 | requires: 34 | - setup 35 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | esm/**/* 3 | node_modules 4 | examples/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "impliedStrict": true, 8 | "experimentalObjectRestSpread": true 9 | }, 10 | "allowImportExportEverywhere": true 11 | }, 12 | "plugins": [ 13 | "@typescript-eslint" 14 | ], 15 | "extends": [ 16 | "eslint:recommended", 17 | "plugin:@typescript-eslint/recommended", 18 | "prettier" 19 | ], 20 | "env": { 21 | "es6": true, 22 | "browser": true, 23 | "node": true 24 | }, 25 | "rules": { 26 | "func-names": [ 27 | "error", 28 | "as-needed" 29 | ], 30 | "no-shadow": "error", 31 | "prefer-const": 0, 32 | "@typescript-eslint/explicit-function-return-type": 0, 33 | "@typescript-eslint/no-use-before-define": 0, 34 | "@typescript-eslint/camelcase": 0, 35 | "@typescript-eslint/no-var-requires": 0, 36 | "@typescript-eslint/no-explicit-any": 0, 37 | "@typescript-eslint/no-unused-vars": 0, 38 | "@typescript-eslint/explicit-module-boundary-types": 0, 39 | "@typescript-eslint/ban-ts-comment": 0, 40 | "@typescript-eslint/ban-types": [ 41 | "error", 42 | { 43 | "types": { 44 | "String": true, 45 | "Boolean": true, 46 | "Number": true, 47 | "Symbol": true, 48 | "{}": true, 49 | "Object": true, 50 | "object": false, 51 | "Function": true 52 | }, 53 | "extendDefaults": true 54 | } 55 | ] 56 | }, 57 | "ignorePatterns": ["dist/", "node_modules", "scripts", "examples"] 58 | } 59 | -------------------------------------------------------------------------------- /.github/release-template.md: -------------------------------------------------------------------------------- 1 | 2 | # 2.0.0-beta.0 (2022-07-17) 3 | This is the first 2.0 beta version. Still change until the stable release. Documentation will also be updated once stable. 4 | 5 | # Breakings 6 | ## `loading` is renamed to `isLoading` 7 | ``` diff 8 | - const { loading } = useConditionWatcher({...}) 9 | + const { isLoading } = useConditionWatcher({...}) 10 | ``` 11 | 12 | ## Change `export` to `export default` 13 | ``` diff 14 | - import { useConditionWatcher } from 'vue-condition-watcher' 15 | + import useConditionWatcher from 'vue-condition-watcher' 16 | ``` 17 | 18 | # What's Changed 19 | 20 | - chore: Switch to pnpm & turborepo & vitest 21 | - breaking: rename loading to isLoading 22 | - fix: `beforeFetch` receive conditions type 23 | 24 | ### Full Changelog: https://github.com/runkids/vue-condition-watcher/compare/1.4.7...2.0.0-beta.0 25 | -------------------------------------------------------------------------------- /.github/vue-condition-watcher_lifecycle.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runkids/vue-condition-watcher/20e06980aa3d4b3a15ec13eb637298edf8959d53/.github/vue-condition-watcher_lifecycle.jpeg -------------------------------------------------------------------------------- /.github/vue-conditions-watcher.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runkids/vue-condition-watcher/20e06980aa3d4b3a15ec13eb637298edf8959d53/.github/vue-conditions-watcher.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | esm 4 | *.log 5 | *.tgz 6 | .env 7 | .next 8 | .DS_Store 9 | .idea 10 | examples/**/yarn.lock 11 | examples/**/node_modules 12 | package-lock.json 13 | *.eslintcache 14 | */**/.turbo 15 | .turbo 16 | .rollup.cache 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.tgz 4 | .env 5 | .next 6 | .DS_Store 7 | examples 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "printWidth": 120 7 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [1.5.0](https://github.com/runkids/vue-condition-watcher/releases/tag/1.5.0) (2023-01-06) 2 | * Fix: `watchEffect` maximum recursive updates exceeded. 3 | 4 | ### [1.4.7](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.7) (2022-04-15) 5 | * Chore: Improve Types 6 | 7 | ### [1.4.6](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.6) (2022-04-14) 8 | * Fix: Type: DeepReadonly to Readonly 9 | 10 | ### [1.4.5](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.5) (2022-04-14) 11 | * [chore(afterFetch & beforeFetch): improve types #14](https://github.com/runkids/vue-condition-watcher/pull/14) 12 | ### [1.4.4](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.4) (2022-04-11) 13 | * [chore(types): improve types #13](https://github.com/runkids/vue-condition-watcher/pull/13) 14 | ### [1.4.3](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.3) (2022-01-13) 15 | * Fix: Sync query string to conditions convert type bug fix, need check the initial array value type before update conditions. 16 | ```js 17 | // If query string &types=1,2,3 18 | const conditions = { 19 | types: [] 20 | } 21 | // the conditions.types convert value will be ['1', '2', '3'] 22 | 23 | const conditions = { 24 | types: ['1'] 25 | } 26 | // the conditions.types convert value will be ['1', '2', '3'] 27 | 28 | const conditions = { 29 | types: [1] 30 | } 31 | // the conditions.types convert value will be [1, 2, 3] 32 | ``` 33 | ### [1.4.2](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.2) (2022-01-10) 34 | * Fix: Cache not work on globally. 35 | ### Changed 36 | * `loading`: when `!data.value & !error.value` will be `true`. 37 | * `data`: change default value `null` to `undefined` 38 | * `error`: change default value `null` to `undefined` 39 | 40 | ### Add Return value 41 | * `isFetching`: The status of the request being processed. 42 | 43 | ### [1.4.1](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.1) (2022-01-09) 44 | * Fix bug for vue2 45 | ``` 46 | error in ./node_modules/vue-condition-watcher/esm/core/utils/helper.js 47 | Module parse failed: Unexpected token (9:66) 48 | You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders 49 | | export const hasWindow = () => typeof window != STR_UNDEFINED; 50 | | export const hasDocument = () => typeof document != STR_UNDEFINED; 51 | > export const isDocumentVisibility = () => hasDocument() && window?.document?.visibilityState === 'visible'; 52 | | export const hasRequestAnimationFrame = () => hasWindow() && typeof window['requestAnimationFrame'] != STR_UNDEFINED; 53 | | export const isNil = (val) => val === null || val === undefined; 54 | 55 | @ ./node_modules/vue-condition-watcher/esm/core/useConditionWatcher.js 6:0-96 10:15-27 15:15-27 49:35-43 140:12-23 140:39-47 144:12-15 56 | @ ./node_modules/vue-condition-watcher/esm/index.js 57 | @ ./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/views/InfiniteScrolling.vue?vue&type=script&lang=js& 58 | @ ./src/views/InfiniteScrolling.vue?vue&type=script&lang=js& 59 | @ ./src/views/InfiniteScrolling.vue 60 | @ ./src/router/index.js 61 | @ ./src/main.js 62 | @ multi (webpack)-dev-server/client?http://192.168.68.114:8081&sockPath=/sockjs-node (webpack)/hot/dev-server.js ./src/main.js 63 | ``` 64 | 65 | ### [1.4.0](https://github.com/runkids/vue-condition-watcher/releases/tag/1.4.0) (2022-01-09) 66 | ### Changed 67 | * Change `data` type: `ref` to `shallowRef` 68 | * `mutate` now support callback function 69 | ```js 70 | const finalData = mutate((currentData) => { 71 | currentData[0].name = 'runkids' 72 | return currentData 73 | }) 74 | ``` 75 | * `resetConditions` can receiver object to update conditions 76 | ```js 77 | const config = { 78 | conditions: { 79 | a: '1', 80 | b: '2' 81 | } 82 | } 83 | // to default conditions value 84 | resetConditions() // { a: '1', b: '2' } 85 | 86 | // update by key value 87 | resetConditions({ 88 | b: '3' 89 | }) // { a: '1', b: '3' } 90 | ``` 91 | ### Features: 92 | 93 | * **Add new configs for Polling feature**: 94 | 1. pollingInterval: default is 0 95 | 2. pollingWhenHidden: default is false 96 | 3. pollingWhenOffline: default is false 97 | * **Add new configs for Revalidate on Focu feature**: 98 | 4. revalidateOnFocus: default is false 99 | * **Add new configs for Cache & Preload** 100 | 5. cacheProvider: `() => new Map()` 101 | 102 | --------------------------------- 103 | ### [1.3.0](https://github.com/runkids/vue-condition-watcher/releases/tag/1.3.0) (2022-01-07) 104 | ### Features: 105 | * **Add Configs**: 106 | 1. `manual`: you can manual fetching data now, just use `execute` to fetch data. 107 | 2. `history`: the history mode you can sync conditions with URL, base on vue-router (V3 & V4 support) 108 | 109 | * **Add Return Values**: 110 | 1. `mutate`: use `mutate` to directly modify data 111 | 112 | ## BREAKING CHANGES: 113 | * **Modify**: 114 | 1. Changed `data` from `shallowRef` to `ref`. 115 | 116 | * **Deprecated**: 117 | 1. `queryOptions` are now removed, replace `queryOptions` with `config.history`. The `sync` no need inject router now just use `router` 118 | ```js 119 | const router = useRouter() 120 | 121 | // Before 122 | Provider(router) 123 | useConditionWatcher( 124 | { 125 | fetcher, 126 | conditions, 127 | }, 128 | { 129 | sync: 'router' 130 | } 131 | ) 132 | 133 | // After 134 | useConditionWatcher( 135 | { 136 | fetcher, 137 | conditions, 138 | history: { 139 | sync: router 140 | } 141 | }, 142 | ) 143 | ``` 144 | --------------------------------- 145 | 146 | ### [1.2.3](https://github.com/runkids/vue-condition-watcher/releases/tag/1.2.3) (2022-01-06) 147 | ### Fix 148 | * types entry 149 | 150 | ### [1.2.2](https://github.com/runkids/vue-condition-watcher/releases/tag/1.2.2) (2022-01-06) 151 | ### Fix 152 | * Type generation 153 | 154 | ### [1.2.1](https://github.com/runkids/vue-condition-watcher/releases/tag/1.2.1) (2022-01-02) 155 | ### Modify 156 | * Modify execute type 157 | 158 | ### [1.2.0](https://github.com/runkids/vue-condition-watcher/releases/tag/1.2.0) (2022-01-02) 159 | ### Add 160 | * Add fetch events `onFetchSuccess`, `onFetchError`, `onFetchFinally` 161 | ### Changed 162 | * `onConditionsChange` return type changed. 163 | ```js 164 | //Before is array 165 | onConditionsChange(([newCond, oldCond]) => {}) 166 | //After is arguments 167 | onConditionsChange((newCond, oldCond) => {}) 168 | ``` 169 | 170 | ### [1.1.4](https://github.com/runkids/vue-condition-watcher/releases/tag/1.1.4) (2022-01-02) 171 | ### Changed 172 | * Change return value `data`, `error`, `loading` to readonly 173 | * Change `data` type: `ref` to `shallowRef` 174 | 175 | ### [1.1.3](https://github.com/runkids/vue-condition-watcher/releases/tag/1.1.3) (2022-01-02) 176 | ### Fix 177 | * Refactor Queue class to hook function 178 | 179 | ### [1.1.2](https://github.com/runkids/vue-condition-watcher/releases/tag/1.1.2) (2022-01-02) 180 | ### Fix 181 | * Keep requests first in - first out 182 | 183 | ### [1.1.1](https://github.com/runkids/vue-condition-watcher/releases/tag/1.1.1) (2022-01-01) 184 | ### Fix 185 | * Build files bug 186 | 187 | ### [1.1.0](https://github.com/runkids/vue-condition-watcher/releases/tag/1.1.0) (2022-01-01) 188 | ### Fix 189 | * `immediate` not working in sync router mode 190 | ### Add 191 | * Return Value: `resetConditions` 192 | * Return Value: `onConditionsChange` 193 | ### [1.0.1](https://github.com/runkids/vue-condition-watcher/releases/tag/1.0.1) (2022-01-01) 194 | 195 | ### Fix 196 | * Build files bug 197 | ### [1.0.0](https://github.com/runkids/vue-condition-watcher/releases/tag/1.0.0) (2022-01-01) 198 | 199 | ### Changed 200 | * Deprecate `refresh` 201 | * You can use async & await function now in `beforeFetch`, `afterFetch`. 202 | * `afterFetch` should be return an data. 203 | 204 | ### Add 205 | * Config: `initialData` 206 | * Config: `immediate` 207 | * Config: `onFetchError` 208 | * Return Value: `execute` 209 | 210 | ### [0.1.12](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.12) (2021-08-11) 211 | 212 | ### Chore 213 | 214 | * Refactor types 215 | 216 | ### Other 217 | 218 | * Requires Node.js 12.0.0 or higher 219 | 220 | ### [0.1.11](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.11) (2021-07-19) 221 | 222 | ### Fix 223 | 224 | * Use stable version for vue-demi 225 | 226 | ### [0.1.10](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.10) (2021-03-29) 227 | 228 | ### Fix 229 | 230 | * No sync query use vue2 vue-router 231 | 232 | ### [0.1.9](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.9) (2021-03-26) 233 | 234 | ### Feature 235 | 236 | * Add query option property navigation 237 | 238 | ### [0.1.8](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.8) (2021-02-17) 239 | 240 | ### Fix 241 | 242 | * Utils bug 243 | 244 | ### [0.1.7](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.7) (2021-02-17) 245 | 246 | ### Fix 247 | 248 | * Utils bug 249 | 250 | ### [0.1.6](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.6) (2020-08-04) 251 | 252 | ### Fix 253 | 254 | * Can not find modules 255 | 256 | ### [0.1.5](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.5) (2020-08-04) 257 | 258 | ### Chore 259 | 260 | * Remove peer dependency 261 | * Modify readme 262 | 263 | ### [0.1.4](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.4) (2020-07-05) 264 | 265 | ### Feature 266 | 267 | * Add function `afterFetch` and return `data`. 268 | * Add demo for vue2 with vue-infinite-scroll 269 | 270 | ### [0.1.3](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.3) (2020-07-05) 271 | 272 | ### Chore 273 | 274 | * Remove rfdc ([cae3ea7](https://github.com/runkids/vue-condition-watcher/commit/cae3ea792ace46526e8a993ddf90dbaa5c37c8eb)) 275 | 276 | ### [0.1.2](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.2) (2020-07-05) 277 | 278 | ### Bug Fix 279 | 280 | * Check new condition and prev condition is equivalent before fetch data. ([24680f2](https://github.com/runkids/vue-condition-watcher/commit/24680f22b1ee6c1b7c820c5a4722cb77c80fabeb)) 281 | 282 | ### [0.1.1](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.1) (2020-07-05) 283 | 284 | ### Chore 285 | 286 | * Move rfdc to dependencies. 287 | * Update vue-demi version 288 | 289 | ### [0.1.0](https://github.com/runkids/vue-condition-watcher/releases/tag/0.1.0) (2020-07-04) 290 | 291 | ### Bug Fix 292 | 293 | * Fix history back and forward bug ([f997f31](https://github.com/runkids/vue-condition-watcher/commit/f997f3117e8ff848905f547f5c063e3319c3ae6f)) 294 | * If not use router, fetch data when instance created ([0f7daba](https://github.com/runkids/vue-condition-watcher/commit/0f7dababcf1dd3255e216e758230012deb50907d)) 295 | 296 | ### Refactor 297 | 298 | * Remove watchEffect, change use watch ([fa0f03e](https://github.com/runkids/vue-condition-watcher/commit/fa0f03e51340e0d10de97bdc400edf115728cbc6)) 299 | 300 | ### Chore 301 | 302 | * Add vue2 with vue-composition-api example 303 | 304 | ### Feature 305 | 306 | * support vue2 with vue-composition-api 307 | 308 | ### [0.0.11](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.10) (2020-07-03) 309 | 310 | ### Chore 311 | 312 | * Update Vue to beta 18 313 | * Use vue-demi to support vue2.x 314 | 315 | ### [0.0.10](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.10) (2020-06-30) 316 | 317 | ### Chore 318 | 319 | * Update Vue to beta 16 320 | * Update Vue Router to Alpha 13 321 | 322 | ### [0.0.9](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.9) (2020-06-13) 323 | 324 | ### Chore 325 | 326 | * Update Vue to beta 15 327 | 328 | ### [0.0.8](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.8) (2020-05-25) 329 | 330 | ### Feature 331 | 332 | * Sync the state with the query string and initialize off of that so that back/forward work. 333 | 334 | ### [0.0.7](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.7) (2020-05-24) 335 | 336 | ### Feature 337 | 338 | * Sync the state with the query string and initialize off of that so that refresh work. 339 | (back/forward) not finish 340 | 341 | ### [0.0.6](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.6) (2020-05-23) 342 | 343 | ### Bug Fix 344 | 345 | * loading state should return true when fetching data ([1bf4e93](https://github.com/runkids/vue-condition-watcher/commit/1bf4e93b4ca6450bd4d4db1c389323260ec2b6ea)) 346 | * use rdfc deep copy conditions ([1bf4e93](https://github.com/runkids/vue-condition-watcher/commit/1bf4e93b4ca6450bd4d4db1c389323260ec2b6ea)) 347 | 348 | ### [0.0.5](https://github.com/runkids/vue-condition-watcher/releases/tag/0.0.5) (2020-05-23) 349 | 350 | ### Code Refactoring 351 | 352 | * rename beforeFetchData to beforeFetch ([2450c1d](https://github.com/runkids/vue-condition-watcher/commit/2450c1d0a7faacb9e2408e5aebf4b277eefdaa20)) 353 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Willy Hong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-zh_TW.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 中文 2 | 3 | # vue-condition-watcher 4 | 5 | [![CircleCI](https://circleci.com/gh/runkids/vue-condition-watcher.svg?style=svg)](https://circleci.com/gh/runkids/vue-condition-watcher) [![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/) [![vue3](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://composition-api.vuejs.org/) [![npm](https://img.shields.io/npm/v/vue-condition-watcher.svg)](https://www.npmjs.com/package/vue-condition-watcher) [![npm](https://img.shields.io/npm/dt/vue-condition-watcher.svg)](https://www.npmjs.com/package/vue-condition-watcher) [![bundle size](https://badgen.net/bundlephobia/minzip/vue-condition-watcher)](https://bundlephobia.com/result?p=vue-condition-watcher) [![npm](https://img.shields.io/npm/l/vue-condition-watcher.svg)](https://github.com/runkids/vue-condition-watcher/blob/master/LICENSE) 6 | 7 | ## 介紹 8 | 9 | `vue-condition-watcher` 是 Vue 組合 API,以 `conditions` 為核心,可用在請求資料情境,還能簡單地使用 `conditions` 參數來自動獲取資料 10 | > Node.js 需大於或等於 12.0.0 版本 11 | 12 | ## 功能 13 | 14 | ✔ 每當 `conditions` 變動,會自動獲取數據
15 | ✔ 送出請求前會自動過濾掉 `null` `undefined` `[]` `''`
16 | ✔ 重新整理網頁會自動依照 URL 的 query string 初始化 `conditions`,且會自動對應型別 ( string, number, array, date )
17 | ✔ 每當 `conditions` 變動,會自動同步 URL query string,並且讓上一頁下一頁都可以正常運作
18 | ✔ 避免 `race condition`,確保請求先進先出,也可以避免重複請求
19 | ✔ 在更新 `data` 前,可做到依賴請求 ( Dependent Request )
20 | ✔ 輕鬆處理分頁的需求,簡單客製自己的分頁邏輯
21 | ✔ 當網頁重新聚焦或是網絡斷線恢復自動重新請求資料
22 | ✔ 支援輪詢,可動態調整輪詢週期
23 | ✔ 緩存機制讓資料可以更快呈現,不用再等待 loading 動畫
24 | ✔ 不需要等待回傳結果,可手動改變 `data` 讓使用者體驗更好
25 | ✔ 支援 TypeScript
26 | ✔ 支援 Vue 2 & 3,感謝 [vue-demi](https://github.com/vueuse/vue-demi) 27 | 28 | 29 | 30 | ## Navigation 31 | 32 | - [安裝](#installation) 33 | - [快速開始](#快速開始) 34 | - [Configs](#configs) 35 | - [Return Values](#return-values) 36 | - [執行請求](#執行請求) 37 | - [阻止預請求](#阻止預請求) 38 | - [手動觸發請求](#手動觸發請求) 39 | - [攔截請求](#攔截請求) 40 | - [變異資料](#變異資料) 41 | - [Conditions 改變事件](#conditions-改變事件) 42 | - [請求事件](#請求事件) 43 | - [輪詢](#輪詢) 44 | - [緩存](#緩存) 45 | - [History 模式](#history-模式) 46 | - [生命週期](#生命週期) 47 | - [分頁處理](#分頁處理) 48 | - [Changelog](https://github.com/runkids/vue-condition-watcher/blob/master/CHANGELOG.md) 49 | 50 | ## Demo 51 | 52 | [👉 (推薦) 這邊下載 Vue3 版本範例](https://github.com/runkids/vue-condition-watcher/tree/master/examples/vue3) (使用 [Vite](https://github.com/vuejs/vite)) 53 | 54 | ```bash 55 | cd examples/vue3 56 | yarn 57 | yarn serve 58 | ```` 59 | 60 | [👉 這邊下載 Vue2 @vue/composition-api 版本範例](https://github.com/runkids/vue-condition-watcher/tree/master/examples/vue2) 61 | 62 | ```bash 63 | cd examples/vue2 64 | yarn 65 | yarn serve 66 | ```` 67 | 68 | ### 👉 線上 Demo 69 | 70 | - [Demo with Vue 3 on StackBlitz](https://stackblitz.com/edit/vitejs-vite-tsvfqu?devtoolsheight=33&embed=1&file=src/views/Home.vue) 71 | 72 | ## 入門 73 | 74 | ### 安裝 75 | 76 | 在你的專案執行 yarn 77 | 78 | ```bash 79 | yarn add vue-condition-watcher 80 | ``` 81 | 82 | 或是使用 NPM 83 | 84 | ```bash 85 | npm install vue-condition-watcher 86 | ``` 87 | 88 | CDN 89 | 90 | ```javascript 91 | https://unpkg.com/vue-condition-watcher/dist/index.js 92 | ``` 93 | 94 | ### 快速開始 95 | 96 | 這是一個使用 `vue-next` 和 `vue-router-next` 的簡單範例。 97 | 98 | 首先建立一個 `fetcher` function, 你可以用原生的 `fetch` 或是 `Axios` 這類的套件。接著 import `useConditionWatcher` 並開始使用它。 99 | 100 | ```javascript 101 | createApp({ 102 | template: ` 103 |
104 | 105 | 106 |
107 |
108 | {{ !loading ? data : 'Loading...' }} 109 |
110 |
{{ error }}
111 | `, 112 | setup() { 113 | const fetcher = params => axios.get('/user/', {params}) 114 | const router = useRouter() 115 | 116 | const { conditions, data, loading, error } = useConditionWatcher( 117 | { 118 | fetcher, 119 | conditions: { 120 | name: '' 121 | }, 122 | history: { 123 | sync: router 124 | } 125 | } 126 | ) 127 | return { conditions, data, loading, error } 128 | }, 129 | }) 130 | .use(router) 131 | .mount(document.createElement('div')) 132 | ``` 133 | 134 | 您可以使用 `data`、`error` 和 `loading` 的值來確定請求的當前狀態。 135 | 136 | 當 `conditions.name` 值改變,將會觸發 [生命週期](#lifecycle) 重新發送請求. 137 | 138 | 你可以在 `config.history` 設定 sync 為 `sync: router`。 這將會同步 `conditions` 的變化到 URL 的 query string。 139 | 140 | ### 基礎用法 141 | 142 | ```js 143 | const { conditions, data, error, loading, execute, resetConditions, onConditionsChange } = useConditionWatcher(config) 144 | ``` 145 | 146 | ### Configs 147 | 148 | - `fetcher`: (⚠️ 必要) 請求資料的 promise function。 149 | - `conditions`: (⚠️ 必要) `conditions` 預設值。 150 | - `defaultParams`: 每次請求預設會帶上的參數,不可修改。 151 | - `initialData`: `data` 預設回傳 null,如果想定義初始的資料可以使用這個參數設定。 152 | - `immediate`: 如果不想一開始自動請求資料,可以將此參數設定為 `false`,直到 `conditions` 改變或是執行 `execute` 才會執行請求。 153 | - `manual`: 改為手動執行 `execute` 以觸發請求,就算 `conditions` 改變也不會自動請求。 154 | - `history`: 基於 vue-router (v3 & v4),啟用同步 `conditions` 到 URL 的 Query String。當網頁重新整理後會同步 Query String 至 `conditions` 155 | - `pollingInterval`: 啟用輪詢,以毫秒為單位可以是 `number` 或是 `ref(number)` 156 | - `pollingWhenHidden`: 每當離開聚焦畫面繼續輪詢,預設是關閉的 157 | - `pollingWhenOffline`: 每當網路斷線繼續輪詢,預設是關閉的 158 | - `revalidateOnFocus`: 重新聚焦畫面後,重新請求一次,預設是關閉的 159 | - `cacheProvider`: `vue-condition-watch` 背後會緩存資料,可傳入此參數自訂 `cacheProvider` 160 | - `beforeFetch`: 你可以在請求前最後修改 `conditions`,也可以在此階段終止請求。 161 | - `afterFetch`: 你可以在 `data` 更新前調整 `data` 的結果 162 | - `onFetchError`: 當請求發生錯誤觸發,可以在`data` 和 `error` 更新前調整 `error`& `data` 163 | 164 | ### Return Values 165 | 166 | - `conditions`:
167 | Type: `reactive`
168 | reactive 型態的物件 (基於 config 的 conditions),是 `vue-conditions-watcher`主要核心,每當 `conditions` 改變都會觸發[生命週期](#lifecycle)。
169 | - `data`:
170 | Type: `👁‍🗨 readonly & ⚠️ ref`
171 | Default Value: `undefined`
172 | `config.fetcher` 的回傳結果
173 | - `error`:
174 | Type: `👁‍🗨 readonly & ⚠️ ref`
175 | Default Value: `undefined`
176 | `config.fetcher` 錯誤返回結果
177 | - `isFetching`:
178 | Type: `👁‍🗨 readonly & ⚠️ ref`
179 | Default Value: `false`
180 | 請求正在處理中的狀態
181 | - `loading`: 當 `!data.value & !error.value` 就會是 `true` 182 | - `execute`: 基於目前的 `conditions` 和 `defaultParams` 再次觸發請求。
183 | - `mutate`: 可以使用此方法修改 `data`
184 | **🔒 ( `data`預設是唯獨不可修改的 )**
185 | - `resetConditions`: 重置 `conditions` 回初始值 186 | - `onConditionsChange`: 在 `conditions` 發生變化時觸發,回傳新值以及舊值 187 | - `onFetchSuccess`: 請求成功觸發,回傳原始的請求結果 188 | - `onFetchError`: 請求失敗觸發,回傳原始的請求失敗結果 189 | - `onFetchFinally`: 請求結束時觸發 190 | 191 | ### 執行請求 192 | 193 | `conditions` 是響應式的, 每當 `conditions` 變化將自動觸發請求 194 | 195 | ```js 196 | const { conditions } = useConditionWatcher({ 197 | fetcher, 198 | conditions: { 199 | page: 0 200 | }, 201 | defaultParams: { 202 | opt_expand: 'date' 203 | } 204 | }) 205 | 206 | conditions.page = 1 // fetch data with payload { page: 1, opt_expand: 'date' } 207 | 208 | conditions.page = 2 // fetch data with payload { page: 2, opt_expand: 'date' } 209 | ``` 210 | 211 | 如果有需要你可以執行 `execute` 這個 function 再次發送請求 212 | 213 | ```js 214 | const { conditions, execute: refetch } = useConditionWatcher({ 215 | fetcher, 216 | conditions: { 217 | page: 0 218 | }, 219 | defaultParams: { 220 | opt_expand: 'date' 221 | } 222 | }) 223 | 224 | refetch() // fetch data with payload { page: 0, opt_expand: 'date' } 225 | ``` 226 | 227 | 一次完整更新 `conditions`,**只會觸發一次請求** 228 | 229 | ```js 230 | const { conditions, resetConditions } = useConditionWatcher({ 231 | fetcher, 232 | immediate: false, 233 | conditions: { 234 | page: 0, 235 | name: '', 236 | date: [] 237 | }, 238 | }) 239 | 240 | // 初始化 conditions 將會觸發 `onConditionsChange` 事件 241 | resetConditions({ 242 | name: 'runkids', 243 | date: ['2022-01-01', '2022-01-02'] 244 | }) 245 | 246 | // 重置 conditions 247 | function reset () { 248 | // 直接用 `resetConditions` function 來重置初始值. 249 | resetConditions() 250 | } 251 | ``` 252 | 253 | ### 阻止預請求 254 | 255 | `vue-conditions-watcher` 會在一開始先請求一次,如果不想這樣做可以設定 `immediate` 為 `false`,將不會一開始就發送請求直到你呼叫 `execute` function 或是改變 `conditions` 256 | 257 | ```js 258 | const { execute } = useConditionWatcher({ 259 | fetcher, 260 | conditions, 261 | immediate: false, 262 | }) 263 | 264 | execute() 265 | ``` 266 | 267 | ### 手動觸發請求 268 | 269 | `vue-condition-watcher` 會自動觸發請求. 但是你可以設定 `manual` 為 `true` 來關閉這個功能。接著可以使用 `execute()` 在你想要的時機觸發請求。 270 | 271 | ```js 272 | const { execute } = useConditionWatcher({ 273 | fetcher, 274 | conditions, 275 | manual: true, 276 | }) 277 | 278 | execute() 279 | ``` 280 | 281 | ### 攔截請求 282 | 283 | `beforeFetch` 可以讓你在請求之前再次修改 `conditions`。 284 | - 第一個參數回傳一個深拷貝的 `conditions`,你可以任意的修改它且不會影響原本 `conditions`,你可以在這邊調整要給後端的 API 格式。 285 | - 第二個參數回傳一個 function,執行它將會終止這次請求。這在某些情況會很有用的。 286 | - `beforeFetch` 可以處理同步與非同步行為。 287 | - 必須返回修改後的 `conditions` 288 | 289 | ```js 290 | useConditionWatcher({ 291 | fetcher, 292 | conditions: { 293 | date: ['2022/01/01', '2022/01/02'] 294 | }, 295 | initialData: [], 296 | async beforeFetch(conditions, cancel) { 297 | // 請求之前先檢查 token 298 | await checkToken () 299 | 300 | // conditions 是一個深拷貝 `config.conditions` 的物件 301 | const {date, ...baseConditions} = conditions 302 | const [after, before] = date 303 | baseConditions.created_at_after = after 304 | baseConditions.created_at_before = before 305 | 306 | // 返回修改後的 `conditions` 307 | return baseConditions 308 | } 309 | }) 310 | ``` 311 | 312 | `afterFetch` 可以在更新 `data` 前攔截請求,這時候的 `loading` 狀態還是 `true`。 313 | - 你可以在這邊做依賴請求 🎭,或是處理其他同步與非同步行為 314 | - 可以在這邊最後修改 `data`,返回的值將會是 `data` 的值 315 | 316 | ```js 317 | const { data } = useConditionWatcher({ 318 | fetcher, 319 | conditions, 320 | async afterFetch(response) { 321 | //response.data = {id: 1, name: 'runkids'} 322 | if(response.data === null) { 323 | return [] 324 | } 325 | // 依賴其他請求 326 | // `loading` 還是 `true` 直到 `onFetchFinally` 327 | const finalResponse = await otherAPIById(response.data.id) 328 | 329 | return finalResponse // [{message: 'Hello', sender: 'runkids'}] 330 | } 331 | }) 332 | 333 | console.log(data) //[{message: 'Hello', sender: 'runkids'}] 334 | ``` 335 | 336 | `onFetchError` 可以攔截錯誤,可以在 `data` 和 `error` 更新前調整 `error` & `data`,這時候的 `loading` 狀態還是 `true`。 337 | - `onFetchError` 可以處理同步與非同步行為。 338 | - 最後返回格式必須為 339 | 340 | ```js 341 | { 342 | data: ... , 343 | error: ... 344 | } 345 | ``` 346 | 347 | ```js 348 | const { data, error } = useConditionWatcher({ 349 | fetcher, 350 | conditions, 351 | async onFetchError({data, error}) { 352 | if(error.code === 401) { 353 | await doSomething() 354 | } 355 | 356 | return { 357 | data: [], 358 | error: 'Error Message' 359 | } 360 | } 361 | }) 362 | 363 | console.log(data) //[] 364 | console.log(error) //'Error Message' 365 | ``` 366 | 367 | ### 變異資料 368 | 369 | 在一些情況下, mutations `data` 是提升用戶體驗的好方法,因為不需要等待 API 回傳結果。 370 | 371 | 使用 `mutate` function, 你可以修改 `data`。 當 `onFetchSuccess` 觸發時會再改變 `data`。 372 | 373 | 有兩種方式使用 `mutate` function: 374 | 375 | - 第一種:完整修改 data. 376 | 377 | ```js 378 | mutate(newData) 379 | ``` 380 | 381 | - 第二種:使用 callback function,會接受一個深拷貝的 `data` 資料,修改完後再返回結果 382 | 383 | ```js 384 | const finalData = mutate((draft) => { 385 | draft[0].name = 'runkids' 386 | return draft 387 | }) 388 | 389 | console.log(finalData[0]name === data.value[0].name) //true 390 | ``` 391 | 392 | #### 🏄‍♂️ 範例:依據目前的資料來修改部分資料 393 | 394 | POST API 會返回更新後的結果,我們不需要重新執行 `execute` 更新結果。我們可以用 `mutate` 的第二種方式來修改部分改動。 395 | 396 | ```js 397 | const { conditions, data, mutate } = useConditionWatcher({ 398 | fetcher: api.userInfo, 399 | conditions, 400 | initialData: [] 401 | }) 402 | 403 | async function updateUserName (userId, newName, rowIndex = 0) { 404 | console.log(data.value) //before: [{ id: 1, name: 'runkids' }, { id: 2, name: 'vuejs' }] 405 | 406 | const response = await api.updateUer(userId, newName) 407 | 408 | // 🚫 `data.value[0] = response.data` 409 | // 沒作用! 因為 `data` 是唯讀不可修改的. 410 | 411 | // Easy to use function will receive deep clone data, and return updated data. 412 | mutate(draft => { 413 | draft[rowIndex] = response.data 414 | return draft 415 | }) 416 | 417 | console.log(data.value) //after: [{ id: 1, name: 'mutate name' }, { id: 2, name: 'vuejs' }] 418 | } 419 | ``` 420 | 421 | ### Conditions 改變事件 422 | 423 | `onConditionsChange` 可以幫助你處理 `conditions` 的變化。會回傳新值和舊值 424 | 425 | ```js 426 | const { conditions, onConditionsChange } = useConditionWatcher({ 427 | fetcher, 428 | conditions: { 429 | page: 0 430 | }, 431 | }) 432 | 433 | conditions.page = 1 434 | 435 | onConditionsChange((conditions, preConditions)=> { 436 | console.log(conditions) // { page: 1} 437 | console.log(preConditions) // { page: 0} 438 | }) 439 | ``` 440 | 441 | ### 請求事件 442 | 443 | `onFetchResponse`, `onFetchError` 和 `onFetchFinally` 會在請求期間觸發。 444 | 445 | ```ts 446 | const { onFetchResponse, onFetchError, onFetchFinally } = useConditionWatcher(config) 447 | 448 | onFetchResponse((response) => { 449 | console.log(response) 450 | }) 451 | 452 | onFetchError((error) => { 453 | console.error(error) 454 | }) 455 | 456 | onFetchFinally(() => { 457 | //todo 458 | }) 459 | ``` 460 | 461 | ## 輪詢 462 | 463 | 你可以透過設定 `pollingInterval` 啟用輪詢功能(當為 0 時會關閉此功能) 464 | 465 | ```js 466 | useConditionWatcher({ 467 | fetcher, 468 | conditions, 469 | pollingInterval: 1000 470 | }) 471 | ``` 472 | 473 | 你還可以使用 `ref` 動態響應輪詢週期。 474 | 475 | ```js 476 | const pollingInterval = ref(0) 477 | 478 | useConditionWatcher({ 479 | fetcher, 480 | conditions, 481 | pollingInterval: pollingInterval 482 | }) 483 | 484 | function startPolling () { 485 | pollingInterval.value = 1000 486 | } 487 | 488 | onMounted(startPolling) 489 | ``` 490 | 491 | `vue-condition-watcher` 預設會在你離開畫面聚焦或是網路斷線時停用輪詢,直到畫面重新聚焦或是網路連線上了才會啟用輪詢。 492 | 493 | 你可以透過設定關閉預設行為: 494 | 495 | - `pollingWhenHidden=true` 離開聚焦後繼續輪詢 496 | - `pollingWhenOffline=true` 網路斷線還是會繼續輪詢 497 | 498 | 你也可以啟用聚焦畫面後重打請求,確保資料是最新狀態。 499 | 500 | - `revalidateOnFocus=true` 501 | 502 | ```js 503 | useConditionWatcher({ 504 | fetcher, 505 | conditions, 506 | pollingInterval: 1000, 507 | pollingWhenHidden: true, // pollingWhenHidden default is false 508 | pollingWhenOffline: true, // pollingWhenOffline default is false 509 | revalidateOnFocus: true // revalidateOnFocus default is false 510 | }) 511 | ``` 512 | 513 | ## 緩存 514 | 515 | `vue-condition-watcher` 預設會在當前組件緩存你的第一次數據。接著後面的請求會先使用緩存數據,背後默默請求新資料,等待最新回傳結果並比對緩存資料是否相同,達到類似預加載的效果。 516 | 517 | 你也可以設定 `cacheProvider` 全局共用或是緩存資料在 `localStorage`,搭配輪詢可以達到分頁同步資料的效果。 518 | 519 | ###### Global Based 520 | 521 | ```js 522 | // App.vue 523 | 540 | ``` 541 | 542 | ###### [LocalStorage Based](https://swr.vercel.app/docs/advanced/cache#localstorage-based-persistent-cache) 543 | 544 | ```js 545 | function localStorageProvider() { 546 | const map = new Map(JSON.parse(localStorage.getItem('your-cache-key') || '[]')) 547 | window.addEventListener('beforeunload', () => { 548 | const appCache = JSON.stringify(Array.from(map.entries())) 549 | localStorage.setItem('your-cache-key', appCache) 550 | }) 551 | return map 552 | } 553 | 554 | useConditionWatcher({ 555 | fetcher, 556 | conditions, 557 | cacheProvider: localStorageProvider 558 | }) 559 | ``` 560 | 561 | ## History 模式 562 | 563 | 你可以設定 `config.history` 啟用 History 模式,是基於 vue-router 的,支援 v3 和 v4 版本 564 | 565 | ```js 566 | const router = useRouter() 567 | 568 | useConditionWatcher({ 569 | fetcher, 570 | conditions, 571 | history: { 572 | sync: router 573 | } 574 | }) 575 | ``` 576 | 577 | 你還可以設定 `history.ignore` 排除 `conditions` 部分的 `key&value` 不要同步到 URL query string. 578 | 579 | ```js 580 | const router = useRouter() 581 | 582 | useConditionWatcher({ 583 | fetcher, 584 | conditions: { 585 | users: ['runkids', 'hello'] 586 | limit: 20, 587 | offset: 0 588 | }, 589 | history: { 590 | sync: router, 591 | ignore: ['limit'] 592 | } 593 | }) 594 | 595 | // the query string will be ?offset=0&users=runkids,hello 596 | ``` 597 | 598 | History mode 會轉換 `conditions`預設值的對應型別到 query string 而且會過濾掉 `undefined`, `null`, `''`, `[]` 這些類型的值. 599 | 600 | ```js 601 | conditions: { 602 | users: ['runkids', 'hello'] 603 | company: '' 604 | limit: 20, 605 | offset: 0 606 | } 607 | // the query string will be ?offset=0&limit=20&users=runkids,hello 608 | ``` 609 | 610 | 每當你重新整理網頁還會自動同步 query string 到 `conditions` 611 | 612 | ``` 613 | URL query string: ?offset=0&limit=10&users=runkids,hello&company=vue 614 | ``` 615 | 616 | `conditions` 將變成 617 | 618 | ```js 619 | { 620 | users: ['runkids', 'hello'] 621 | company: 'vue' 622 | limit: 10, 623 | offset: 0 624 | } 625 | ``` 626 | 627 | 使用 `navigation` 可以 push 或是 replace 當前的位置. 預設值為 'push' 628 | ```js 629 | useConditionWatcher({ 630 | fetcher, 631 | conditions: { 632 | limit: 20, 633 | offset: 0 634 | }, 635 | history: { 636 | sync: router, 637 | navigation: 'replace' 638 | } 639 | }) 640 | ``` 641 | 642 | ## 生命週期 643 | 644 | 645 | 646 | - ##### `onConditionsChange` 647 | 648 | `conditions` 變更時觸發,會返回新舊值。 649 | 650 | ```js 651 | onConditionsChange((cond, preCond)=> { 652 | console.log(cond) 653 | console.log(preCond) 654 | }) 655 | ``` 656 | 657 | - ##### `beforeFetch` 658 | 659 | 可以讓你在請求之前再次修改 `conditions`,也可以在這個階段終止請求。 660 | 661 | ```js 662 | const { conditions } = useConditionWatcher({ 663 | fetcher, 664 | conditions, 665 | beforeFetch 666 | }) 667 | 668 | async function beforeFetch(cond, cancel){ 669 | if(!cond.token) { 670 | // stop fetch 671 | cancel() 672 | // will fire onConditionsChange again 673 | conditions.token = await fetchToken() 674 | } 675 | return cond 676 | }) 677 | ``` 678 | 679 | - ##### `afterFetch` & `onFetchSuccess` 680 | 681 | `afterFetch` 會在 `onFetchSuccess` 前觸發
682 | `afterFetch` 可以在`data` 更新前修改 `data` 683 | ||Type|Modify data before update| Dependent request | 684 | |-----|--------|------|------| 685 | |afterFetch| config | ⭕️ | ⭕️ | 686 | |onFetchSuccess | event | ❌ | ❌ | 687 | 688 | ```html 689 | 692 | ``` 693 | 694 | ```js 695 | const { data, onFetchSuccess } = useConditionWatcher({ 696 | fetcher, 697 | conditions, 698 | async afterFetch(response){ 699 | //response = { id: 1 } 700 | const detail = await fetchDataById(response.id) 701 | return detail // { id: 1, detail: 'xxx' } 702 | }) 703 | }) 704 | 705 | onFetchSuccess((response)=> { 706 | console.log(response) // { id: 1, detail: 'xxx' } 707 | }) 708 | ``` 709 | 710 | - ##### `onFetchError(config)` & `onFetchError(event)` 711 | 712 | `config.onFetchError` 會在 `event.onFetchError` 前觸發
713 | `config.onFetchError` 可以攔截錯誤,可以在 `data` 和 `error` 更新前調整 `error` & `data`。 714 | ||Type|Modify data before update|Modify error before update| 715 | |-----|--------|------|------| 716 | |onFetchError| config | ⭕️ | ⭕️ | 717 | |onFetchError | event | ❌ | ❌ | 718 | 719 | ```js 720 | const { onFetchError } = useConditionWatcher({ 721 | fetcher, 722 | conditions, 723 | onFetchError(ctx){ 724 | return { 725 | data: [], 726 | error: 'Error message.' 727 | } 728 | }) 729 | }) 730 | 731 | onFetchError((error)=> { 732 | console.log(error) // origin error data 733 | }) 734 | ``` 735 | 736 | - ##### `onFetchFinally` 737 | 738 | 請求結束時觸發 739 | 740 | ```js 741 | onFetchFinally(async ()=> { 742 | //do something 743 | }) 744 | ``` 745 | 746 | ## 重複使用 747 | 748 | 建立 `vue-condition-watcher` 的可重用的 hook 非常容易。 749 | 750 | ```js 751 | function useUserExpensesHistory (id) { 752 | const { conditions, data, error, loading } = useConditionWatcher({ 753 | fetcher: params => api.user(id, { params }), 754 | defaultParams: { 755 | opt_expand: 'amount,place' 756 | }, 757 | conditions: { 758 | daterange: [] 759 | } 760 | immediate: false, 761 | initialData: [], 762 | beforeFetch(cond, cancel) { 763 | if(!id) { 764 | cancel() 765 | } 766 | const { daterange, ...baseCond } = cond 767 | if(daterange.length) { 768 | [baseCond.created_at_after, baseCond.created_at_before] = [ 769 | daterange[0], 770 | daterange[1] 771 | ] 772 | } 773 | return baseCond 774 | } 775 | }) 776 | 777 | return { 778 | histories: data, 779 | isFetching: loading, 780 | isError: error, 781 | daterange: conditions.daterange 782 | } 783 | } 784 | ``` 785 | 786 | 接著在 components 使用: 787 | 788 | ```js 789 | 802 | ``` 803 | 804 | ```html 805 | 815 | ``` 816 | 817 | 恭喜你! 🥳 你已經學會再次包裝 `vue-condition-watcher`. 818 | 819 | 現在我們來用 `vue-condition-watcher` 做分頁的處理. 820 | 821 | ## 分頁處理 822 | 823 | 這個範例適用 Django the limit and offset functions 和 Element UI. 824 | 825 | 建立 `usePagination` 826 | 827 | ```js 828 | function usePagination () { 829 | let cancelFlag = false // check this to cancel fetch 830 | 831 | const { startLoading, stopLoading } = useLoading() 832 | 833 | const { conditions, data, execute, resetConditions, onConditionsChange, onFetchFinally } = useConditionWatcher( 834 | { 835 | fetcher: api.list, 836 | conditions: { 837 | daterange: [], 838 | limit: 20, 839 | offset: 0 840 | } 841 | immediate: true, 842 | initialData: [], 843 | history: { 844 | sync: 'router', 845 | // You can ignore the key of URL query string, prevent users from entering unreasonable numbers by themselves. 846 | // The URL will look like ?offset=0 not show `limit` 847 | ignore: ['limit'] 848 | }, 849 | beforeFetch 850 | }, 851 | ) 852 | 853 | // use on pagination component 854 | const currentPage = computed({ 855 | get: () => conditions.offset / conditions.limit + 1, 856 | set: (page) => { 857 | conditions.offset = (page - 1) * conditions.limit 858 | } 859 | }) 860 | 861 | // onConditionsChange -> beforeFetch -> onFetchFinally 862 | onConditionsChange((newCond, oldCond) => { 863 | // When conditions changed, reset offset to 0 and then will fire beforeEach again. 864 | if (newCond.offset !== 0 && newCond.offset === oldCond.offset) { 865 | cancelFlag = true 866 | conditions.offset = 0 867 | } 868 | }) 869 | 870 | async function beforeFetch(cond, cancel) { 871 | if (cancelFlag) { 872 | // cancel fetch when cancelFlag be true 873 | cancel() 874 | cancelFlag = false // reset cancelFlag 875 | return cond 876 | } 877 | // start loading 878 | await nextTick() 879 | startLoading() 880 | const { daterange, ...baseCond } = cond 881 | if(daterange.length) { 882 | [baseCond.created_at_after, baseCond.created_at_before] = [ 883 | daterange[0], 884 | daterange[1] 885 | ] 886 | } 887 | return baseCond 888 | } 889 | 890 | onFetchFinally(async () => { 891 | await nextTick() 892 | // stop loading 893 | stopLoading() 894 | window.scrollTo(0, 0) 895 | }) 896 | 897 | return { 898 | data, 899 | conditions, 900 | currentPage, 901 | resetConditions, 902 | refetch: execute 903 | } 904 | } 905 | ``` 906 | 907 | 接著在 components 使用: 908 | 909 | ```js 910 | 913 | ``` 914 | 915 | ```html 916 | 935 | ``` 936 | 937 | 當 daterange or limit 改變時, 會將 offset 設置為 0,接著才會重新觸發請求。 938 | 939 | ## TDOD List 940 | 941 | - [ ] Error Retry 942 | - [ ] Nuxt SSR SSG Support 943 | 944 | ## Thanks 945 | 946 | This project is heavily inspired by the following awesome projects. 947 | 948 | - [vercel/swr](https://github.com/vercel/swr) 949 | 950 | ## 📄 License 951 | 952 | [MIT License](https://github.com/runkids/vue-condition-watcher/blob/master/LICENSE) © 2020-PRESENT [Runkids](https://github.com/runkids) 953 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [中文](./README-zh_TW.md) 2 | 3 | # vue-condition-watcher 4 | 5 | [![CircleCI](https://circleci.com/gh/runkids/vue-condition-watcher.svg?style=svg)](https://circleci.com/gh/runkids/vue-condition-watcher) [![vue3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org/) [![vue3](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://composition-api.vuejs.org/) [![npm](https://img.shields.io/npm/v/vue-condition-watcher.svg)](https://www.npmjs.com/package/vue-condition-watcher) [![npm](https://img.shields.io/npm/dt/vue-condition-watcher.svg)](https://www.npmjs.com/package/vue-condition-watcher) [![bundle size](https://badgen.net/bundlephobia/minzip/vue-condition-watcher)](https://bundlephobia.com/result?p=vue-condition-watcher) [![npm](https://img.shields.io/npm/l/vue-condition-watcher.svg)](https://github.com/runkids/vue-condition-watcher/blob/master/LICENSE) 6 | 7 | ## Introduction 8 | 9 | `vue-condition-watcher` is a data fetching library using the Vue Composition API. It allows you to easily control and sync data fetching to the URL query string using conditions. 10 | > requires Node.js 12.0.0 or higher. 11 | 12 | ## Features 13 | 14 | ✔ Automatically fetches data when conditions change. 15 | ✔ Filters out null, undefined, [], and '' before sending requests. 16 | ✔ Initializes conditions based on URL query strings and syncs them accordingly. 17 | ✔ Synchronizes URL query strings with condition changes, maintaining normal navigation. 18 | ✔ Ensures requests are first in, first out, and avoids repeats. 19 | ✔ Handles dependent requests before updating data. 20 | ✔ Customizable paging logic. 21 | ✔ Refetches data when the page is refocused or network resumes. 22 | ✔ Supports polling with adjustable periods. 23 | Caches data for faster rendering. 24 | ✔ Allows manual data modifications to improve user experience. 25 | ✔ TypeScript support. 26 | ✔ Compatible with Vue 2 & 3 via [vue-demi](https://github.com/vueuse/vue-demi). 27 | 28 | 29 | 30 | ## Navigation 31 | 32 | - [Installation](#installation) 33 | - [Quick Start](#quick-start) 34 | - [Configs](#configs) 35 | - [Return Values](#return-values) 36 | - [Execute Fetch](#execute-fetch) 37 | - [Prevent Request](#prevent-request) 38 | - [Manually Trigger](#manually-trigger-request) 39 | - [Intercepting Request](#intercepting-request) 40 | - [Mutations data](#mutations-data) 41 | - [Conditions Change Event](#conditions-change-event) 42 | - [Fetch Event](#fetch-event) 43 | - [Polling](#polling) 44 | - [Cache](#cache) 45 | - [History Mode](#history-mode) 46 | - [Lifecycle](#lifecycle) 47 | - [Pagination](#pagination) 48 | - [Changelog](https://github.com/runkids/vue-condition-watcher/blob/master/CHANGELOG.md) 49 | 50 | ## Demo 51 | 52 | [👉 Download Vue3 example](https://github.com/runkids/vue-condition-watcher/tree/master/examples/vue3) 53 | 54 | ```bash 55 | cd examples/vue3 56 | yarn 57 | yarn serve 58 | ``` 59 | 60 | [👉 Download Vue2 example](https://github.com/runkids/vue-condition-watcher/tree/master/examples/vue2) 61 | 62 | ```bash 63 | cd examples/vue2 64 | yarn 65 | yarn serve 66 | ``` 67 | 68 | [👉 Online demo with Vue 3](https://stackblitz.com/edit/vitejs-vite-tsvfqu?devtoolsheight=33&embed=1&file=src/views/Home.vue) 69 | 70 | ### Installation 71 | 72 | ```bash 73 | yarn add vue-condition-watcher 74 | ``` 75 | 76 | or 77 | 78 | ```bash 79 | npm install vue-condition-watcher 80 | ``` 81 | 82 | or via CDN 83 | 84 | ```javascript 85 | 86 | ``` 87 | 88 | ### Quick Start 89 | 90 | --- 91 | 92 | Example using `axios` and `vue-router`: 93 | 94 | ```html 95 | 109 | 110 | 120 | ``` 121 | 122 | ### Configs 123 | 124 | --- 125 | 126 | - `fetcher` (required): Function for data fetching. 127 | - `conditions` (required): Default conditions. 128 | - `defaultParams`: Parameters preset with each request. 129 | - `initialData`: Initial data returned. 130 | - `immediate`: If false, data will not be fetched initially. 131 | - `manual`: If true, fetch requests are manual. 132 | - `history`: Syncs conditions with URL query strings using vue-router. 133 | - `pollingInterval`: Enables polling with adjustable intervals. 134 | - `pollingWhenHidden`: Continues polling when the page loses focus. 135 | - `pollingWhenOffline`: Continues polling when offline. 136 | - `revalidateOnFocus`: Refetches data when the page regains focus. 137 | - `cacheProvider`: Customizable cache provider. 138 | - `beforeFetch`: Modify conditions before fetching. 139 | - `afterFetch`: Adjust data before updating. 140 | - `onFetchError`: Handle fetch errors. 141 | 142 | ### Return Values 143 | 144 | --- 145 | 146 | - `conditions`: Reactive object for conditions. 147 | - `data`: Readonly data returned by fetcher. 148 | - `error`: Readonly fetch error. 149 | - `isFetching`: Readonly fetch status. 150 | - `loading`: true when data and error are null. 151 | - `execute`: Function to trigger a fetch request. 152 | - `mutate`: Function to modify data. 153 | - `resetConditions`: Resets conditions to initial values. 154 | - `onConditionsChange`: Event triggered on condition changes. 155 | - `onFetchSuccess`: Event triggered on successful fetch. 156 | - `onFetchError`: Event triggered on fetch error. 157 | - `onFetchFinally`: Event triggered when fetch ends. 158 | 159 | ### Execute Fetch 160 | 161 | --- 162 | 163 | Fetch data when `conditions` change: 164 | 165 | ```js 166 | const { conditions } = useConditionWatcher({ 167 | fetcher, 168 | conditions: { page: 0 }, 169 | defaultParams: { opt_expand: 'date' } 170 | }) 171 | 172 | conditions.page = 1 173 | conditions.page = 2 174 | ``` 175 | 176 | Manually trigger a fetch: 177 | 178 | ```js 179 | const { conditions, execute: refetch } = useConditionWatcher({ 180 | fetcher, 181 | conditions: { page: 0 }, 182 | defaultParams: { opt_expand: 'date' } 183 | }) 184 | 185 | refetch() 186 | ``` 187 | 188 | Force reset conditions: 189 | 190 | ```js 191 | const { conditions, resetConditions } = useConditionWatcher({ 192 | const { conditions, resetConditions } = useConditionWatcher({ 193 | fetcher, 194 | immediate: false, 195 | conditions: { page: 0, name: '', date: [] }, 196 | }) 197 | 198 | resetConditions({ name: 'runkids', date: ['2022-01-01', '2022-01-02'] }) 199 | ``` 200 | 201 | ### Prevent Request 202 | 203 | --- 204 | 205 | Prevent requests until execute is called: 206 | 207 | ```js 208 | const { execute } = useConditionWatcher({ 209 | fetcher, 210 | conditions, 211 | immediate: false, 212 | }) 213 | 214 | execute() 215 | ``` 216 | 217 | ### Manually Trigger Request 218 | 219 | --- 220 | 221 | Disable automatic fetch and use execute() to trigger: 222 | 223 | ```js 224 | const { execute } = useConditionWatcher({ 225 | fetcher, 226 | conditions, 227 | manual: true, 228 | }) 229 | 230 | execute() 231 | ``` 232 | 233 | ### Intercepting Request 234 | 235 | --- 236 | 237 | Modify conditions before fetch: 238 | 239 | ```js 240 | useConditionWatcher({ 241 | fetcher, 242 | conditions: { date: ['2022/01/01', '2022/01/02'] }, 243 | initialData: [], 244 | async beforeFetch(conds, cancel) { 245 | await checkToken() 246 | const { date, ...baseConditions } = conds 247 | const [after, before] = date 248 | baseConditions.created_at_after = after 249 | baseConditions.created_at_before = before 250 | return baseConditions 251 | } 252 | }) 253 | ``` 254 | 255 | Modify data after fetch: 256 | 257 | ```js 258 | const { data } = useConditionWatcher({ 259 | fetcher, 260 | conditions, 261 | async afterFetch(response) { 262 | if(response.data === null) { 263 | return [] 264 | } 265 | const finalResponse = await otherAPIById(response.data.id) 266 | return finalResponse 267 | } 268 | }) 269 | ``` 270 | 271 | Handle fetch errors: 272 | 273 | ```js 274 | const { data, error } = useConditionWatcher({ 275 | fetcher, 276 | conditions, 277 | async onFetchError({data, error}) { 278 | if(error.code === 401) { 279 | await doSomething() 280 | } 281 | return { data: [], error: 'Error Message' } 282 | } 283 | }) 284 | ``` 285 | 286 | ### Mutations data 287 | 288 | --- 289 | 290 | Update data using mutate function: 291 | 292 | ```js 293 | mutate(newData) 294 | ``` 295 | 296 | Update part of data: 297 | 298 | ```js 299 | const finalData = mutate(draft => { 300 | draft[0].name = 'runkids' 301 | return draft 302 | }) 303 | ``` 304 | 305 | #### 🏄‍♂️ Example for updating part of data 306 | 307 | 308 | ```js 309 | const { conditions, data, mutate } = useConditionWatcher({ 310 | fetcher: api.userInfo, 311 | conditions, 312 | initialData: [] 313 | }) 314 | 315 | async function updateUserName (userId, newName, rowIndex = 0) { 316 | const response = await api.updateUer(userId, newName) 317 | mutate(draft => { 318 | draft[rowIndex] = response.data 319 | return draft 320 | }) 321 | } 322 | ``` 323 | 324 | ### Conditions Change Event 325 | 326 | --- 327 | 328 | Handle condition changes: 329 | 330 | ```js 331 | const { conditions, onConditionsChange } = useConditionWatcher({ 332 | fetcher, 333 | conditions: { page: 0 }, 334 | }) 335 | 336 | conditions.page = 1 337 | 338 | onConditionsChange((conditions, preConditions) => { 339 | console.log(conditions) 340 | console.log(preConditions) 341 | }) 342 | ``` 343 | 344 | ### Fetch Event 345 | 346 | --- 347 | 348 | Handle fetch events: 349 | 350 | ```ts 351 | const { onFetchResponse, onFetchError, onFetchFinally } = useConditionWatcher(config) 352 | 353 | onFetchResponse(response => console.log(response)) 354 | onFetchError(error => console.error(error)) 355 | onFetchFinally(() => { 356 | //todo 357 | }) 358 | ``` 359 | 360 | ## Polling 361 | 362 | --- 363 | 364 | Enable polling: 365 | ```js 366 | useConditionWatcher({ 367 | fetcher, 368 | conditions, 369 | pollingInterval: 1000 370 | }) 371 | ``` 372 | 373 | Use ref for reactivity: 374 | 375 | ```js 376 | const pollingInterval = ref(0) 377 | useConditionWatcher({ 378 | fetcher, 379 | conditions, 380 | pollingInterval: pollingInterval 381 | }) 382 | onMounted(() => pollingInterval.value = 1000) 383 | ``` 384 | 385 | Continue polling when hidden or offline: 386 | 387 | ```js 388 | useConditionWatcher({ 389 | fetcher, 390 | conditions, 391 | pollingInterval: 1000, 392 | pollingWhenHidden: true, // pollingWhenHidden default is false 393 | pollingWhenOffline: true, // pollingWhenOffline default is false 394 | revalidateOnFocus: true // revalidateOnFocus default is false 395 | }) 396 | ``` 397 | 398 | ## Cache 399 | 400 | --- 401 | 402 | Cache data globally: 403 | 404 | ```js 405 | // App.vue 406 | const cache = new Map() 407 | export default { 408 | name: 'App', 409 | provide: { cacheProvider: () => cache } 410 | } 411 | 412 | useConditionWatcher({ 413 | fetcher, 414 | conditions, 415 | cacheProvider: inject('cacheProvider') 416 | }) 417 | ``` 418 | 419 | Cache data in `localStorage`: 420 | 421 | ```js 422 | function localStorageProvider() { 423 | const map = new Map(JSON.parse(localStorage.getItem('your-cache-key') || '[]')) 424 | window.addEventListener('beforeunload', () => { 425 | const appCache = JSON.stringify(Array.from(map.entries())) 426 | localStorage.setItem('your-cache-key', appCache) 427 | }) 428 | return map 429 | } 430 | 431 | useConditionWatcher({ 432 | fetcher, 433 | conditions, 434 | cacheProvider: localStorageProvider 435 | }) 436 | ``` 437 | 438 | ## History Mode 439 | 440 | --- 441 | 442 | Enable history mode using `vue-router`: 443 | 444 | ````js 445 | const router = useRouter() 446 | useConditionWatcher({ 447 | fetcher, 448 | conditions, 449 | history: { sync: router } 450 | }) 451 | ```` 452 | 453 | Exclude keys from URL query string: 454 | 455 | ```js 456 | const router = useRouter() 457 | useConditionWatcher({ 458 | fetcher, 459 | conditions: { users: ['runkids', 'hello'], limit: 20, offset: 0 }, 460 | history: { sync: router, ignore: ['limit'] } 461 | }) 462 | // the query string will be ?offset=0&users=runkids,hello 463 | ``` 464 | 465 | Convert conditions to query strings: 466 | 467 | ```js 468 | conditions: { 469 | users: ['runkids', 'hello'] 470 | company: '' 471 | limit: 20, 472 | offset: 0 473 | } 474 | // the query string will be ?offset=0&limit=20&users=runkids,hello 475 | ``` 476 | 477 | Sync query strings to conditions on page refresh: 478 | 479 | ``` 480 | URL query string: ?offset=0&limit=10&users=runkids,hello&compay=vue 481 | ``` 482 | 483 | `conditions` will become 484 | 485 | ```js 486 | { 487 | users: ['runkids', 'hello'], 488 | company: 'vue', 489 | limit: 10, 490 | offset: 0 491 | } 492 | ``` 493 | 494 | Use navigation to replace or push current location: 495 | 496 | ```js 497 | useConditionWatcher({ 498 | fetcher, 499 | conditions: { 500 | limit: 20, 501 | offset: 0 502 | }, 503 | history: { 504 | sync: router, 505 | navigation: 'replace' 506 | } 507 | }) 508 | ``` 509 | 510 | ## Lifecycle 511 | 512 | --- 513 | 514 | 515 | 516 | - ##### `onConditionsChange` 517 | 518 | Fires new and old condition values. 519 | 520 | ```js 521 | onConditionsChange((cond, preCond)=> { 522 | console.log(cond) 523 | console.log(preCond) 524 | }) 525 | ``` 526 | 527 | - ##### `beforeFetch` 528 | 529 | Modify conditions before fetch or stop fetch. 530 | 531 | ```js 532 | const { conditions } = useConditionWatcher({ 533 | fetcher, 534 | conditions, 535 | beforeFetch 536 | }) 537 | 538 | async function beforeFetch(cond, cancel){ 539 | if(!cond.token) { 540 | // stop fetch 541 | cancel() 542 | // will fire onConditionsChange again 543 | conditions.token = await fetchToken() 544 | } 545 | return cond 546 | }) 547 | ``` 548 | 549 | - ##### `afterFetch` & `onFetchSuccess` 550 | 551 | `afterFetch` fire before `onFetchSuccess`
552 | `afterFetch` can modify data before update. 553 | ||Type|Modify data before update| Dependent request | 554 | |-----|--------|------|------| 555 | |afterFetch| config | ⭕️ | ⭕️ | 556 | |onFetchSuccess | event | ❌ | ❌ | 557 | 558 | ```html 559 | 562 | ``` 563 | 564 | ```js 565 | const { data, onFetchSuccess } = useConditionWatcher({ 566 | fetcher, 567 | conditions, 568 | async afterFetch(response){ 569 | //response = { id: 1 } 570 | const detail = await fetchDataById(response.id) 571 | return detail // { id: 1, detail: 'xxx' } 572 | }) 573 | }) 574 | 575 | onFetchSuccess((response)=> { 576 | console.log(response) // { id: 1, detail: 'xxx' } 577 | }) 578 | ``` 579 | 580 | - ##### `onFetchError(config)` & `onFetchError(event)` 581 | 582 | `config.onFetchError` fire before `event.onFetchError`
583 | `config.onFetchError` can modify data and error before update. 584 | ||Type|Modify data before update|Modify error before update| 585 | |-----|--------|------|------| 586 | |onFetchError| config | ⭕️ | ⭕️ | 587 | |onFetchError | event | ❌ | ❌ | 588 | 589 | ```js 590 | const { onFetchError } = useConditionWatcher({ 591 | fetcher, 592 | conditions, 593 | onFetchError(ctx){ 594 | return { 595 | data: [], 596 | error: 'Error message.' 597 | } 598 | }) 599 | }) 600 | 601 | onFetchError((error)=> { 602 | console.log(error) // origin error data 603 | }) 604 | ``` 605 | 606 | - ##### `onFetchFinally` 607 | 608 | Will fire on fetch finished. 609 | 610 | ```js 611 | onFetchFinally(async ()=> { 612 | //do something 613 | }) 614 | ``` 615 | 616 | ## Make It Reusable 617 | 618 | --- 619 | 620 | You might need to reuse the data in many places. It is incredibly easy to create reusable hooks of `vue-condition-watcher` : 621 | 622 | ```js 623 | function useUserExpensesHistory (id) { 624 | const { conditions, data, error, loading } = useConditionWatcher({ 625 | fetcher: params => api.user(id, { params }), 626 | defaultParams: { 627 | opt_expand: 'amount,place' 628 | }, 629 | conditions: { 630 | daterange: [] 631 | } 632 | immediate: false, 633 | initialData: [], 634 | beforeFetch(cond, cancel) { 635 | if(!id) { 636 | cancel() 637 | } 638 | const { daterange, ...baseCond } = cond 639 | if(daterange.length) { 640 | [baseCond.created_at_after, baseCond.created_at_before] = [ 641 | daterange[0], 642 | daterange[1] 643 | ] 644 | } 645 | return baseCond 646 | } 647 | }) 648 | 649 | return { 650 | histories: data, 651 | isFetching: loading, 652 | isError: error, 653 | daterange: conditions.daterange 654 | } 655 | } 656 | ``` 657 | 658 | Use in components: 659 | 660 | ```js 661 | 674 | ``` 675 | 676 | ```html 677 | 687 | ``` 688 | 689 | Congratulations! 🥳 You have learned how to use composition-api with `vue-condition-watcher`. 690 | 691 | Now we can manage the paging information use `vue-condition-watcher` . 692 | 693 | ## Pagination 694 | 695 | --- 696 | 697 | Here is an example use Django the limit and offset functions and Element UI. 698 | 699 | Create `usePagination` hook: 700 | 701 | ```js 702 | function usePagination () { 703 | let cancelFlag = false 704 | const { startLoading, stopLoading } = useLoading() 705 | const router = useRouter() 706 | 707 | const { conditions, data, execute, resetConditions, onConditionsChange, onFetchFinally } = useConditionWatcher({ 708 | fetcher: api.list, 709 | conditions: { daterange: [], limit: 20, offset: 0 }, 710 | immediate: true, 711 | initialData: [], 712 | history: { sync: router, ignore: ['limit'] }, 713 | beforeFetch 714 | }) 715 | 716 | const currentPage = computed({ 717 | get: () => conditions.offset / conditions.limit + 1, 718 | set: (page) => conditions.offset = (page - 1) * conditions.limit 719 | }) 720 | 721 | onConditionsChange((newCond, oldCond) => { 722 | if (newCond.offset !== 0 && newCond.offset === oldCond.offset) { 723 | cancelFlag = true 724 | conditions.offset = 0 725 | } 726 | }) 727 | 728 | async function beforeFetch(cond, cancel) { 729 | if (cancelFlag) { 730 | cancel() 731 | cancelFlag = false 732 | return cond 733 | } 734 | await nextTick() 735 | startLoading() 736 | const { daterange, ...baseCond } = cond 737 | if(daterange.length) { 738 | [baseCond.created_at_after, baseCond.created_at_before] = [daterange[0], daterange[1]] 739 | } 740 | return baseCond 741 | } 742 | 743 | onFetchFinally(async () => { 744 | await nextTick() 745 | stopLoading() 746 | window.scrollTo(0, 0) 747 | }) 748 | 749 | return { 750 | data, 751 | conditions, 752 | currentPage, 753 | resetConditions, 754 | refetch: execute 755 | } 756 | } 757 | ``` 758 | 759 | Use in components: 760 | 761 | ```js 762 | 765 | ``` 766 | 767 | ```html 768 | 787 | ``` 788 | 789 | Reset offset when daterange or limit changes. 790 | 791 | ## TODO List 792 | 793 | --- 794 | 795 | - [ ] Error Retry 796 | - [ ] Nuxt SSR SSG Support 797 | 798 | ## Thanks 799 | 800 | --- 801 | 802 | Inspired by [vercel/swr](https://github.com/vercel/swr) 803 | 804 | ## 📄 License 805 | 806 | --- 807 | 808 | [MIT License](https://github.com/runkids/vue-condition-watcher/blob/master/LICENSE) © 2020-PRESENT [Runkids](https://github.com/runkids) 809 | -------------------------------------------------------------------------------- /_internal/composable/useCache.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '../types' 2 | import { serializeFunc, sortObject, stringifyQuery } from '../utils/common' 3 | 4 | export function useCache(fetcher: (params: object) => Promise, provider: Cache) { 5 | const baseKey = serializeFunc(fetcher) 6 | function formatString(key) { 7 | return `${baseKey}@${stringifyQuery(sortObject(key))}` 8 | } 9 | return { 10 | set: (key, value) => provider.set(formatString(key), value), 11 | get: (key) => (provider.get(formatString(key)) ? provider.get(formatString(key)) : undefined), 12 | delete: (key) => provider.delete(formatString(key)), 13 | cached: (key) => provider.get(formatString(key)) !== undefined, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /_internal/composable/useHistory.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, watch, Ref } from 'vue-demi' 2 | import { HistoryOptions } from '../types' 3 | import { stringifyQuery } from '../utils/common' 4 | 5 | export interface HistoryConfig extends HistoryOptions { 6 | listener: (query: any) => void 7 | } 8 | 9 | function decode(text: string | number): string { 10 | try { 11 | return decodeURIComponent('' + text) 12 | } catch (err) { 13 | console.error(`Error decoding "${text}".`) 14 | } 15 | return '' + text 16 | } 17 | 18 | function parseQuery(search: string): object { 19 | const query = {} 20 | if (search === '' || search === '?') return query 21 | const hasLeadingIM = search[0] === '?' 22 | const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&') 23 | for (let i = 0; i < searchParams.length; ++i) { 24 | let [key, rawValue] = searchParams[i].split('=') as [string, string | undefined] 25 | key = decode(key) 26 | let value = rawValue == null ? null : decode(rawValue) 27 | if (key in query) { 28 | let currentValue = query[key] 29 | if (!Array.isArray(currentValue)) { 30 | currentValue = query[key] = [currentValue] 31 | } 32 | currentValue.push(value) 33 | } else { 34 | query[key] = value 35 | } 36 | } 37 | return query 38 | } 39 | 40 | export function useHistory(query: Ref, config: HistoryConfig) { 41 | function createQuery() { 42 | const href = window.location.href.split('?') 43 | const search = href.length === 1 ? '' : '?' + href[1] 44 | config.listener(parseQuery(search)) 45 | } 46 | 47 | watch( 48 | query, 49 | async () => { 50 | const path: string = config.sync.currentRoute.value 51 | ? config.sync.currentRoute.value.path 52 | : config.sync.currentRoute.path 53 | const queryString = stringifyQuery(query.value, config.ignore) 54 | const routeLocation = path + '?' + queryString 55 | try { 56 | config.navigation === 'replace' ? config.sync.replace(routeLocation) : config.sync.push(routeLocation) 57 | } catch (e) { 58 | throw new Error(`[vue-condition-watcher]: history.sync is not instance of vue-router. Please check.`) 59 | } 60 | }, 61 | { deep: true } 62 | ) 63 | 64 | // initial conditions by location.search. just do once when created. 65 | createQuery() 66 | 67 | onMounted(() => { 68 | window.addEventListener('popstate', createQuery) 69 | }) 70 | 71 | onUnmounted(() => { 72 | window.removeEventListener('popstate', createQuery) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /_internal/composable/usePromiseQueue.ts: -------------------------------------------------------------------------------- 1 | export function usePromiseQueue() { 2 | let queue = [] 3 | let workingOnPromise = false 4 | 5 | function enqueue(promise) { 6 | return new Promise((resolve, reject) => { 7 | queue.push({ 8 | promise, 9 | resolve, 10 | reject, 11 | }) 12 | dequeue() 13 | }) 14 | } 15 | 16 | function dequeue() { 17 | if (workingOnPromise) { 18 | return false 19 | } 20 | const item = queue.shift() 21 | if (!item) { 22 | return false 23 | } 24 | try { 25 | workingOnPromise = true 26 | item 27 | .promise() 28 | .then((value) => { 29 | workingOnPromise = false 30 | item.resolve(value) 31 | dequeue() 32 | }) 33 | .catch((err) => { 34 | workingOnPromise = false 35 | item.reject(err) 36 | dequeue() 37 | }) 38 | } catch (err) { 39 | workingOnPromise = false 40 | item.reject(err) 41 | dequeue() 42 | } 43 | return true 44 | } 45 | 46 | return { 47 | enqueue, 48 | dequeue, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /_internal/index.ts: -------------------------------------------------------------------------------- 1 | import { usePromiseQueue } from './composable/usePromiseQueue' 2 | import { useHistory } from './composable/useHistory' 3 | import { useCache } from './composable/useCache' 4 | import { createEvents } from './utils/createEvents' 5 | 6 | export { usePromiseQueue, useHistory, useCache, createEvents } 7 | 8 | export * from './utils/common' 9 | export * from './utils/helper' 10 | 11 | export * from './types' 12 | -------------------------------------------------------------------------------- /_internal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-condition-watcher-internal", 3 | "version": "0.0.1", 4 | "main": "./dist/index.js", 5 | "module": "./dist/index.esm.js", 6 | "types": "./dist/_internal", 7 | "exports": "./dist/index.mjs", 8 | "private": true, 9 | "scripts": { 10 | "watch": "bunchee index.ts --no-sourcemap -w", 11 | "build": "bunchee index.ts --no-sourcemap", 12 | "types:check": "tsc --noEmit", 13 | "clean": "rimraf dist" 14 | }, 15 | "peerDependencies": { 16 | "vue-demi": "*", 17 | "@vue/composition-api": "^1.1.0", 18 | "vue": "^2.0.0 || >=3.0.0" 19 | }, 20 | "peerDependenciesMeta": { 21 | "@vue/composition-api": { 22 | "optional": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /_internal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "..", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } -------------------------------------------------------------------------------- /_internal/types.ts: -------------------------------------------------------------------------------- 1 | type ArgumentsTuple = [any, ...unknown[]] | readonly [any, ...unknown[]] 2 | export type Arguments = string | ArgumentsTuple | Record | null | undefined | false 3 | export type Key = Arguments | (() => Arguments) 4 | export interface Cache { 5 | get(key: Key): Data | null | undefined 6 | set(key: Key, value: Data): void 7 | delete(key: Key): void 8 | } 9 | 10 | export interface HistoryOptions { 11 | sync: { 12 | currentRoute: any 13 | replace: (string) => any 14 | push: (string) => any 15 | } 16 | navigation?: 'push' | 'replace' 17 | ignore?: Array 18 | } 19 | -------------------------------------------------------------------------------- /_internal/utils/common.ts: -------------------------------------------------------------------------------- 1 | type ConditionsType = Record 2 | 3 | declare global { 4 | interface ObjectConstructor { 5 | fromEntries(xs: [string | number | symbol, any][]): Record 6 | } 7 | } 8 | 9 | const fromEntries = (xs: [string | number | symbol, any][]) => 10 | Object.fromEntries ? Object.fromEntries(xs) : xs.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) 11 | 12 | export function createParams(conditions: ConditionsType, defaultParams?: ConditionsType): ConditionsType { 13 | const _conditions = { 14 | ...conditions, 15 | ...defaultParams, 16 | } 17 | Object.entries(_conditions).forEach(([key, value]) => { 18 | if (Array.isArray(value)) { 19 | _conditions[key] = value.join(',') 20 | } 21 | }) 22 | return _conditions 23 | } 24 | 25 | export function filterNoneValueObject(object: ConditionsType): ConditionsType { 26 | return fromEntries( 27 | Object.entries(object).filter((item) => { 28 | const value: any = item[1] 29 | return typeof value !== 'undefined' && value !== null && value !== '' && value.length !== 0 30 | }) 31 | ) 32 | } 33 | 34 | export function stringifyQuery(params: ConditionsType, ignoreKeys?: any[]): string { 35 | const esc = encodeURIComponent 36 | return Object.entries(params) 37 | .filter( 38 | ([key, value]: [string, unknown]): boolean => 39 | typeof value !== 'undefined' && 40 | value !== null && 41 | value !== '' && 42 | (Array.isArray(value) ? value.length !== 0 : true) && 43 | (ignoreKeys && ignoreKeys.length ? !ignoreKeys.includes(key) : true) 44 | ) 45 | .map(([key, value]) => { 46 | return esc(key) + (value != null ? '=' + esc(value) : '') 47 | }) 48 | .join('&') 49 | } 50 | 51 | export function typeOf(obj) { 52 | return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase() 53 | } 54 | 55 | export function syncQuery2Conditions( 56 | conditions: ConditionsType, 57 | query: ConditionsType, 58 | backupIntiConditions: ConditionsType 59 | ): void { 60 | const conditions2Object = { ...conditions } 61 | const noQuery = Object.keys(query).length === 0 62 | Object.keys(conditions2Object).forEach((key) => { 63 | if (key in query || noQuery) { 64 | const conditionType = typeOf(conditions2Object[key]) 65 | switch (conditionType) { 66 | case 'date': 67 | conditions[key] = noQuery ? '' : new Date(query[key]) 68 | break 69 | case 'array': 70 | conditions[key] = 71 | noQuery || !query[key].length ? [] : typeOf(query[key]) === 'string' ? query[key].split(',') : query[key] 72 | 73 | if (backupIntiConditions[key].length && conditions[key].length) { 74 | let originArrayValueType = typeOf(backupIntiConditions[key][0]) 75 | conditions[key] = conditions[key].map((v: any) => { 76 | switch (originArrayValueType) { 77 | case 'number': 78 | return Number(v) 79 | case 'date': 80 | return new Date(v) 81 | case 'boolean': 82 | return v === 'true' 83 | default: 84 | return String(v) 85 | } 86 | }) 87 | } 88 | break 89 | case 'string': 90 | conditions[key] = noQuery ? '' : String(query[key]) 91 | break 92 | case 'number': 93 | conditions[key] = noQuery ? 0 : Number(query[key]) 94 | break 95 | case 'boolean': 96 | conditions[key] = noQuery ? '' : Boolean(query[key]) 97 | break 98 | default: 99 | conditions[key] = noQuery ? '' : query[key] 100 | break 101 | } 102 | } 103 | }) 104 | } 105 | 106 | export function isEquivalentString(a: ConditionsType, b: ConditionsType, ignore: string[]) { 107 | return stringifyQuery(a, ignore) === stringifyQuery(b, ignore) 108 | } 109 | 110 | export function isEquivalent(x: any, y: any) { 111 | if (x === null || x === undefined || y === null || y === undefined) { 112 | return x === y 113 | } 114 | if (x.constructor !== y.constructor) { 115 | return false 116 | } 117 | if (x instanceof Function) { 118 | return x === y 119 | } 120 | if (x instanceof RegExp) { 121 | return x === y 122 | } 123 | if (x === y || x.valueOf() === y.valueOf()) { 124 | return true 125 | } 126 | if (Array.isArray(x) && x.length !== y.length) { 127 | return false 128 | } 129 | if (x instanceof Date) { 130 | return false 131 | } 132 | if (!(x instanceof Object)) { 133 | return false 134 | } 135 | if (!(y instanceof Object)) { 136 | return false 137 | } 138 | let p = Object.keys(x) 139 | return ( 140 | Object.keys(y).every((i) => { 141 | return p.indexOf(i) !== -1 142 | }) && 143 | p.every((i) => { 144 | return isEquivalent(x[i], y[i]) 145 | }) 146 | ) 147 | } 148 | 149 | export function deepClone(obj): any { 150 | if (obj === null) return null 151 | let clone = Object.assign({}, obj) 152 | Object.keys(clone).forEach((key) => { 153 | if (obj[key] instanceof Date) { 154 | const original = obj[key] 155 | clone[key] = new Date(original) 156 | return 157 | } 158 | clone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key] 159 | }) 160 | return Array.isArray(obj) && obj.length 161 | ? (clone.length = obj.length) && Array.from(clone) 162 | : Array.isArray(obj) 163 | ? Array.from(obj) 164 | : clone 165 | } 166 | 167 | export function sortObject(unordered) { 168 | return Object.keys(unordered) 169 | .sort() 170 | .reduce((obj, key) => { 171 | obj[key] = unordered[key] 172 | return obj 173 | }, {}) 174 | } 175 | 176 | export function pick(obj: Record, keys: string[]) { 177 | const res = {} 178 | keys.forEach((key) => { 179 | if (key in obj) { 180 | res[key] = obj[key] 181 | } 182 | }) 183 | return res 184 | } 185 | 186 | export function serializeFunc(fn) { 187 | //the source code from https://github.com/yahoo/serialize-javascript/blob/main/index.js 188 | const serializedFn = fn.toString() 189 | if (/function.*?\(/.test(serializedFn)) { 190 | return serializedFn 191 | } 192 | if (/.*?=>.*?/.test(serializedFn)) { 193 | return serializedFn 194 | } 195 | const argsStartsAt = serializedFn.indexOf('(') 196 | const def = serializedFn 197 | .substr(0, argsStartsAt) 198 | .trim() 199 | .split(' ') 200 | .filter((val) => val.length > 0) 201 | const nonReservedSymbols = def.filter((val) => ['*', 'async'].indexOf(val) === -1) 202 | if (nonReservedSymbols.length > 0) { 203 | return ( 204 | (def.indexOf('async') > -1 ? 'async ' : '') + 205 | 'function' + 206 | (def.join('').indexOf('*') > -1 ? '*' : '') + 207 | serializedFn.substr(argsStartsAt) 208 | ) 209 | } 210 | return serializedFn 211 | } 212 | -------------------------------------------------------------------------------- /_internal/utils/createEvents.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { hasDocument, hasWindow, isDocumentVisibility } from './helper' 3 | 4 | // These function inspired from VueUse's `createEventHook` 5 | function useSubscribe() { 6 | const fns: Array<(...args) => void> = [] 7 | 8 | const off = (fn: (...args) => void) => { 9 | const index = fns.indexOf(fn) 10 | if (index !== -1) fns.splice(index, 1) 11 | } 12 | 13 | const on = (fn: (...args) => void) => { 14 | fns.push(fn) 15 | return { 16 | off: () => off(fn), 17 | } 18 | } 19 | 20 | const trigger = (...args) => { 21 | fns.forEach((fn) => fn(...args)) 22 | } 23 | 24 | return { 25 | on, 26 | trigger, 27 | } 28 | } 29 | 30 | let online = true 31 | const hasWin = hasWindow() 32 | const hasDoc = hasDocument() 33 | 34 | function createFocusEvent(eventHook) { 35 | if (hasWin && window.addEventListener) { 36 | window.addEventListener('focus', eventHook.trigger) 37 | } 38 | return () => { 39 | window.removeEventListener('focus', eventHook.trigger) 40 | } 41 | } 42 | 43 | function createVisibilityEvent(eventHook) { 44 | if (hasDoc && document.addEventListener) { 45 | document.addEventListener('visibilitychange', () => eventHook.trigger(isDocumentVisibility())) 46 | } 47 | return () => { 48 | document.removeEventListener('visibilitychange', () => eventHook.trigger(isDocumentVisibility())) 49 | } 50 | } 51 | 52 | function createReconnectEvent(eventHook) { 53 | const onOnline = () => { 54 | online = true 55 | eventHook.trigger(online) 56 | } 57 | // nothing to revalidate, just update the status 58 | const onOffline = () => { 59 | online = false 60 | eventHook.trigger(online) 61 | } 62 | if (hasWin && window.addEventListener) { 63 | window.addEventListener('online', onOnline) 64 | window.addEventListener('offline', onOffline) 65 | } 66 | return () => { 67 | window.removeEventListener('online', onOnline) 68 | window.removeEventListener('offline', onOffline) 69 | } 70 | } 71 | 72 | export function createEvents() { 73 | const conditionEvent = useSubscribe() 74 | const responseEvent = useSubscribe() 75 | const errorEvent = useSubscribe() 76 | const finallyEvent = useSubscribe() 77 | const focusEvent = useSubscribe() 78 | const visibilityEvent = useSubscribe() 79 | const reconnectEvent = useSubscribe() 80 | 81 | const stopFocusEvent = createFocusEvent(focusEvent) 82 | const stopVisibilityEvent = createVisibilityEvent(visibilityEvent) 83 | const stopReconnectEvent = createReconnectEvent(reconnectEvent) 84 | 85 | return { 86 | conditionEvent, 87 | responseEvent, 88 | errorEvent, 89 | finallyEvent, 90 | focusEvent, 91 | reconnectEvent, 92 | visibilityEvent, 93 | stopFocusEvent, 94 | stopReconnectEvent, 95 | stopVisibilityEvent, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /_internal/utils/helper.ts: -------------------------------------------------------------------------------- 1 | export function containsProp(obj: any, ...props: string[]) { 2 | if (!isObject(obj)) return false 3 | return props.some((k) => k in obj) 4 | } 5 | 6 | const STR_UNDEFINED = 'undefined' 7 | 8 | export const hasWindow = () => typeof window != STR_UNDEFINED 9 | export const hasDocument = () => typeof document != STR_UNDEFINED 10 | export const isDocumentVisibility = () => hasDocument() && document.visibilityState === 'visible' 11 | export const hasRequestAnimationFrame = () => hasWindow() && typeof window['requestAnimationFrame'] != STR_UNDEFINED 12 | export const isNil = (val: unknown) => val === null || val === undefined 13 | export const isObject = (val: unknown): val is Record => val !== null && typeof val === 'object' 14 | 15 | export const isServer = !hasWindow() 16 | export const rAF = (f: (...args: any[]) => void) => 17 | hasRequestAnimationFrame() ? window['requestAnimationFrame'](f) : setTimeout(f, 1) 18 | 19 | export const isNoData = (data: unknown) => { 20 | if (typeof data === 'string' || Array.isArray(data)) { 21 | return data.length === 0 22 | } 23 | if ([null, undefined].includes(data)) { 24 | return false 25 | } 26 | if (isObject(data)) { 27 | return Object.keys.length === 0 28 | } 29 | return !data 30 | } 31 | -------------------------------------------------------------------------------- /core/index.ts: -------------------------------------------------------------------------------- 1 | import useConditionWatcher from './use-condition-watcher' 2 | export default useConditionWatcher 3 | 4 | export type { 5 | VoidFn, 6 | HistoryOptions, 7 | Conditions, 8 | OnFetchErrorContext, 9 | Config, 10 | OnConditionsChangeReturnValue, 11 | OnConditionsChangeContext, 12 | UseConditionWatcherReturn, 13 | } from './types' 14 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-condition-watcher-core", 3 | "version": "0.0.2", 4 | "main": "./dist/index.js", 5 | "module": "./dist/index.esm.js", 6 | "types": "./dist/index.d.ts", 7 | "exports": "./dist/index.mjs", 8 | "private": true, 9 | "scripts": { 10 | "watch": "bunchee index.ts --no-sourcemap -w", 11 | "build": "bunchee index.ts --no-sourcemap", 12 | "types:check": "tsc --noEmit", 13 | "clean": "rimraf dist" 14 | }, 15 | "peerDependencies": { 16 | "vue-condition-watcher": "*", 17 | "vue-demi": "*", 18 | "@vue/composition-api": "^1.1.0", 19 | "vue": "^2.0.0 || >=3.0.0" 20 | }, 21 | "peerDependenciesMeta": { 22 | "@vue/composition-api": { 23 | "optional": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "..", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /core/types.ts: -------------------------------------------------------------------------------- 1 | import { Cache, HistoryOptions } from 'vue-condition-watcher/_internal' 2 | import { Ref, UnwrapNestedRefs } from 'vue-demi' 3 | 4 | export type { HistoryOptions } 5 | 6 | export type VoidFn = () => void 7 | export type Conditions = { 8 | [K in keyof T]: T[K] 9 | } 10 | export type FinalResult = 11 | | Promise 12 | | AfterFetchResult extends Result 13 | ? Result 14 | : AfterFetchResult 15 | 16 | export type OnConditionsChangeReturnValue = Partial> 17 | 18 | export type OnConditionsChangeContext = ( 19 | newConditions: OnConditionsChangeReturnValue, 20 | oldConditions: OnConditionsChangeReturnValue 21 | ) => void 22 | 23 | export interface OnFetchErrorContext { 24 | error: E 25 | data: T | null 26 | } 27 | 28 | type MutateFunction = (arg: (oldData: T) => any) => void 29 | type MutateData = (newData: any) => void 30 | export interface Mutate extends MutateFunction, MutateData {} 31 | 32 | export interface Config, Result = unknown, AfterFetchResult = Result> { 33 | fetcher: (...args: any) => Promise 34 | conditions?: Cond 35 | defaultParams?: Record 36 | immediate?: boolean 37 | manual?: boolean 38 | initialData?: any 39 | history?: HistoryOptions 40 | pollingInterval?: number | Ref 41 | pollingWhenHidden?: boolean 42 | pollingWhenOffline?: boolean 43 | revalidateOnFocus?: boolean 44 | cacheProvider?: () => Cache 45 | beforeFetch?: ( 46 | conditions: Partial & Record, 47 | cancel: VoidFn 48 | ) => Promise> | Record 49 | afterFetch?: (data: Result) => FinalResult 50 | onFetchError?: (ctx: OnFetchErrorContext) => Promise> | Partial 51 | } 52 | 53 | export interface UseConditionWatcherReturn { 54 | conditions: UnwrapNestedRefs 55 | readonly isFetching: Ref 56 | readonly isLoading: Ref 57 | readonly data: Readonly> 58 | readonly error: Ref 59 | execute: (throwOnFailed?: boolean) => void 60 | mutate: Mutate 61 | resetConditions: (conditions?: object) => void 62 | onConditionsChange: (fn: OnConditionsChangeContext) => void 63 | onFetchSuccess: (fn: (response: any) => void) => void 64 | onFetchError: (fn: (error: any) => void) => void 65 | onFetchFinally: (fn: (error: any) => void) => void 66 | } 67 | -------------------------------------------------------------------------------- /core/use-condition-watcher.ts: -------------------------------------------------------------------------------- 1 | import { Conditions, Config, Mutate, UseConditionWatcherReturn } from './types' 2 | import { 3 | UnwrapNestedRefs, 4 | computed, 5 | getCurrentInstance, 6 | isRef, 7 | onUnmounted, 8 | reactive, 9 | readonly, 10 | ref, 11 | shallowRef, 12 | unref, 13 | watch, 14 | watchEffect, 15 | } from 'vue-demi' 16 | import { containsProp, isNoData as isDataEmpty, isObject, isServer, rAF } from 'vue-condition-watcher/_internal' 17 | import { createEvents, useCache, useHistory, usePromiseQueue } from 'vue-condition-watcher/_internal' 18 | import { 19 | createParams, 20 | deepClone, 21 | filterNoneValueObject, 22 | isEquivalent, 23 | pick, 24 | syncQuery2Conditions, 25 | } from 'vue-condition-watcher/_internal' 26 | 27 | export default function useConditionWatcher, Result, AfterFetchResult = Result>( 28 | config: Config 29 | ): UseConditionWatcherReturn { 30 | function isFetchConfig(obj: Record): obj is typeof config { 31 | return containsProp( 32 | obj, 33 | 'fetcher', 34 | 'conditions', 35 | 'defaultParams', 36 | 'initialData', 37 | 'manual', 38 | 'immediate', 39 | 'history', 40 | 'pollingInterval', 41 | 'pollingWhenHidden', 42 | 'pollingWhenOffline', 43 | 'revalidateOnFocus', 44 | 'cacheProvider', 45 | 'beforeFetch', 46 | 'afterFetch', 47 | 'onFetchError' 48 | ) 49 | } 50 | 51 | function isHistoryOption() { 52 | if (!config.history || !config.history.sync) return false 53 | return containsProp(config.history, 'navigation', 'ignore', 'sync') 54 | } 55 | 56 | // default config 57 | let watcherConfig: typeof config = { 58 | fetcher: config.fetcher, 59 | conditions: config.conditions, 60 | immediate: true, 61 | manual: false, 62 | initialData: undefined, 63 | pollingInterval: isRef(config.pollingInterval) ? config.pollingInterval : ref(config.pollingInterval || 0), 64 | pollingWhenHidden: false, 65 | pollingWhenOffline: false, 66 | revalidateOnFocus: false, 67 | cacheProvider: () => new Map(), 68 | } 69 | 70 | // update config 71 | if (isFetchConfig(config)) { 72 | watcherConfig = { ...watcherConfig, ...config } 73 | } 74 | const cache = useCache(watcherConfig.fetcher, watcherConfig.cacheProvider()) 75 | 76 | const backupIntiConditions = deepClone(watcherConfig.conditions) 77 | const _conditions = reactive(watcherConfig.conditions) 78 | 79 | const isFetching = ref(false) 80 | const isOnline = ref(true) 81 | const isActive = ref(true) 82 | 83 | const data = shallowRef( 84 | cache.cached(backupIntiConditions) ? cache.get(backupIntiConditions) : watcherConfig.initialData || undefined 85 | ) 86 | const error = ref(undefined) 87 | const query = ref({}) 88 | 89 | const pollingTimer = ref() 90 | 91 | const { enqueue } = usePromiseQueue() 92 | // - create fetch event & condition event & web event 93 | const { 94 | conditionEvent, 95 | responseEvent, 96 | errorEvent, 97 | finallyEvent, 98 | reconnectEvent, 99 | focusEvent, 100 | visibilityEvent, 101 | stopFocusEvent, 102 | stopReconnectEvent, 103 | stopVisibilityEvent, 104 | } = createEvents() 105 | 106 | const resetConditions = (cond?: Record): void => { 107 | const conditionKeys = Object.keys(_conditions) 108 | Object.assign(_conditions, isObject(cond) ? pick(cond, conditionKeys) : backupIntiConditions) 109 | } 110 | 111 | const isLoading = computed(() => !error.value && !data.value) 112 | 113 | const conditionsChangeHandler = async (conditions, throwOnFailed = false) => { 114 | const checkThrowOnFailed = typeof throwOnFailed === 'boolean' ? throwOnFailed : false 115 | if (isFetching.value) return 116 | isFetching.value = true 117 | error.value = undefined 118 | const conditions2Object: Conditions = conditions 119 | let customConditions: Record = {} 120 | const deepCopyCondition: Conditions = deepClone(conditions2Object) 121 | 122 | if (typeof watcherConfig.beforeFetch === 'function') { 123 | let isCanceled = false 124 | customConditions = await watcherConfig.beforeFetch(deepCopyCondition, () => { 125 | isCanceled = true 126 | }) 127 | if (isCanceled) { 128 | isFetching.value = false 129 | return Promise.resolve(undefined) 130 | } 131 | if (!customConditions || typeof customConditions !== 'object' || customConditions.constructor !== Object) { 132 | isFetching.value = false 133 | throw new Error(`[vue-condition-watcher]: beforeFetch should return an object`) 134 | } 135 | } 136 | 137 | const validateCustomConditions: boolean = Object.keys(customConditions).length !== 0 138 | 139 | /* 140 | * if custom conditions has value, just use custom conditions 141 | * filterNoneValueObject will filter no value like [] , '', null, undefined 142 | * example. {name: '', items: [], age: 0, tags: null} 143 | * return result will be {age: 0} 144 | */ 145 | query.value = filterNoneValueObject(validateCustomConditions ? customConditions : conditions2Object) 146 | const finalConditions: Record = createParams(query.value, watcherConfig.defaultParams) 147 | 148 | let responseData: any = undefined 149 | 150 | data.value = cache.cached(query.value) ? cache.get(query.value) : watcherConfig.initialData || undefined 151 | 152 | return new Promise((resolve, reject) => { 153 | config 154 | .fetcher(finalConditions) 155 | .then(async (fetchResponse) => { 156 | responseData = fetchResponse 157 | if (typeof watcherConfig.afterFetch === 'function') { 158 | responseData = await watcherConfig.afterFetch(fetchResponse) 159 | } 160 | if (responseData === undefined) { 161 | console.warn(`[vue-condition-watcher]: "afterFetch" return value is ${responseData}. Please check it.`) 162 | } 163 | if (!isEquivalent(data.value, responseData)) { 164 | data.value = responseData 165 | } 166 | if (!isEquivalent(cache.get(query.value), responseData)) { 167 | cache.set(query.value, responseData) 168 | } 169 | responseEvent.trigger(responseData) 170 | return resolve(fetchResponse) 171 | }) 172 | .catch(async (fetchError) => { 173 | if (typeof watcherConfig.onFetchError === 'function') { 174 | ;({ data: responseData, error: fetchError } = await watcherConfig.onFetchError({ 175 | data: undefined, 176 | error: fetchError, 177 | })) 178 | data.value = responseData || watcherConfig.initialData 179 | error.value = fetchError 180 | } 181 | errorEvent.trigger(fetchError) 182 | if (checkThrowOnFailed) { 183 | return reject(fetchError) 184 | } 185 | return resolve(undefined) 186 | }) 187 | .finally(() => { 188 | isFetching.value = false 189 | finallyEvent.trigger() 190 | }) 191 | }) 192 | } 193 | 194 | const revalidate = (throwOnFailed = false) => 195 | enqueue(() => conditionsChangeHandler({ ..._conditions }, throwOnFailed)) 196 | 197 | function execute(throwOnFailed = false) { 198 | if (isDataEmpty(data.value) || isServer) { 199 | revalidate(throwOnFailed) 200 | } else { 201 | rAF(() => revalidate(throwOnFailed)) 202 | } 203 | } 204 | 205 | // - Start polling with out setting to manual 206 | if (!watcherConfig.manual) { 207 | watchEffect((onCleanup) => { 208 | const pollingInterval = unref(watcherConfig.pollingInterval) 209 | if (pollingInterval) { 210 | pollingTimer.value = (() => { 211 | let timerId = null 212 | function next() { 213 | const interval = pollingInterval 214 | if (interval && timerId !== -1) { 215 | timerId = setTimeout(nun, interval) 216 | } 217 | } 218 | function nun() { 219 | // Only run when the page is visible, online and not errored. 220 | if ( 221 | !error.value && 222 | (watcherConfig.pollingWhenHidden || isActive.value) && 223 | (watcherConfig.pollingWhenOffline || isOnline.value) 224 | ) { 225 | revalidate().then(next) 226 | } else { 227 | next() 228 | } 229 | } 230 | next() 231 | return () => timerId && clearTimeout(timerId) 232 | })() 233 | } 234 | 235 | onCleanup(() => { 236 | pollingTimer.value && pollingTimer.value() 237 | }) 238 | }) 239 | } 240 | 241 | // - mutate: Modify `data` directly 242 | // - `data` is read only by default, recommend modify `data` at `afterFetch` 243 | // - When you need to modify `data`, you can use mutate() to directly modify data 244 | /* 245 | * Two way to use mutate 246 | * - 1. 247 | * mutate(newData) 248 | * - 2. 249 | * mutate((draft) => { 250 | * draft[0].name = 'runkids' 251 | * return draft 252 | * }) 253 | */ 254 | const mutate = (...args): Mutate => { 255 | const arg = args[0] 256 | if (arg === undefined) { 257 | return data.value 258 | } 259 | if (typeof arg === 'function') { 260 | data.value = arg(deepClone(data.value)) 261 | } else { 262 | data.value = arg 263 | } 264 | cache.set({ ..._conditions }, data.value) 265 | return data.value 266 | } 267 | 268 | // - History mode base on vue-router 269 | if (isHistoryOption()) { 270 | const historyOption = { 271 | sync: config.history.sync, 272 | ignore: config.history.ignore || [], 273 | navigation: config.history.navigation || 'push', 274 | listener(parsedQuery: Record) { 275 | const queryObject = Object.keys(parsedQuery).length ? parsedQuery : backupIntiConditions 276 | syncQuery2Conditions(_conditions, queryObject, backupIntiConditions) 277 | }, 278 | } 279 | useHistory(query, historyOption) 280 | } 281 | 282 | // - Automatic data fetching by default 283 | if (!watcherConfig.manual && watcherConfig.immediate) { 284 | execute() 285 | } 286 | 287 | watch( 288 | () => ({ ..._conditions }), 289 | (nc, oc) => { 290 | // - Deep check object if be true do nothing 291 | if (isEquivalent(nc, oc)) return 292 | conditionEvent.trigger(deepClone(nc), deepClone(oc)) 293 | // - Automatic data fetching until manual to be false 294 | !watcherConfig.manual && enqueue(() => conditionsChangeHandler(nc)) 295 | } 296 | ) 297 | 298 | reconnectEvent.on((status: boolean) => { 299 | isOnline.value = status 300 | }) 301 | 302 | visibilityEvent.on((status: boolean) => { 303 | isActive.value = status 304 | }) 305 | 306 | const stopSubscribeFocus = focusEvent.on(() => { 307 | if (!isActive.value) return 308 | execute() 309 | // if (isHistoryOption() && cache.cached({ ..._conditions })) { 310 | //todo sync to query 311 | // } 312 | }) 313 | 314 | if (!watcherConfig.revalidateOnFocus) { 315 | stopFocusEvent() 316 | stopSubscribeFocus.off() 317 | } 318 | 319 | if (getCurrentInstance()) { 320 | onUnmounted(() => { 321 | pollingTimer.value && pollingTimer.value() 322 | stopFocusEvent() 323 | stopReconnectEvent() 324 | stopVisibilityEvent() 325 | }) 326 | } 327 | 328 | return { 329 | conditions: _conditions as UnwrapNestedRefs, 330 | data: readonly(data), 331 | error: readonly(error), 332 | isFetching: readonly(isFetching), 333 | isLoading, 334 | execute, 335 | mutate, 336 | resetConditions, 337 | onConditionsChange: conditionEvent.on, 338 | onFetchSuccess: responseEvent.on, 339 | onFetchError: errorEvent.on, 340 | onFetchFinally: finallyEvent.on, 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /examples/vue2/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /examples/vue2/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /examples/vue2/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /examples/vue2/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /examples/vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@vue/composition-api": "^1.4.9", 12 | "core-js": "^3.6.5", 13 | "vue": "^2.6.14", 14 | "vue-condition-watcher": "1.4.7", 15 | "vue-infinite-scroll": "^2.0.2", 16 | "vue-router": "^3.3.4" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "~4.4.0", 20 | "@vue/cli-plugin-eslint": "~4.4.0", 21 | "@vue/cli-plugin-router": "~4.4.0", 22 | "@vue/cli-service": "~4.4.0", 23 | "@vue/eslint-config-prettier": "^6.0.0", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^6.7.2", 26 | "eslint-plugin-prettier": "^3.1.3", 27 | "eslint-plugin-vue": "^6.2.2", 28 | "prettier": "^1.19.1", 29 | "vue-template-compiler": "^2.6.14" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/vue2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runkids/vue-condition-watcher/20e06980aa3d4b3a15ec13eb637298edf8959d53/examples/vue2/public/favicon.ico -------------------------------------------------------------------------------- /examples/vue2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/vue2/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 53 | -------------------------------------------------------------------------------- /examples/vue2/src/api.js: -------------------------------------------------------------------------------- 1 | const users = params => 2 | fetch('https://randomuser.me/api/?' + query(params), { 3 | method: 'GET' 4 | }).then(res => res.json()) 5 | 6 | const addBox = params => { 7 | return new Promise(resolve => { 8 | setTimeout(() => { 9 | resolve( 10 | Array.from(Array(params.limit), (_, index) => { 11 | return { 12 | id: params.offset + index + 1, 13 | color: '#' + ('00000' + ((Math.random() * 0xffffff) << 0).toString(16)).slice(-6) 14 | } 15 | }) 16 | ) 17 | }, 1000) 18 | }) 19 | } 20 | 21 | function query(params) { 22 | const esc = encodeURIComponent 23 | return Object.keys(params) 24 | .map(k => esc(k) + '=' + esc(params[k])) 25 | .join('&') 26 | } 27 | 28 | export default { 29 | users, 30 | addBox 31 | } 32 | -------------------------------------------------------------------------------- /examples/vue2/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | import VueCompositionAPI from '@vue/composition-api' 6 | 7 | Vue.config.productionTip = false 8 | Vue.use(VueCompositionAPI) 9 | 10 | new Vue({ 11 | router, 12 | provide: { 13 | router 14 | }, 15 | render: h => h(App) 16 | }).$mount('#app') 17 | -------------------------------------------------------------------------------- /examples/vue2/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'Home', 10 | component: () => import('../views/Home.vue') 11 | }, 12 | { 13 | path: '/infinite', 14 | name: 'InfiniteScrolling', 15 | component: () => import('../views/InfiniteScrolling.vue') 16 | } 17 | ] 18 | 19 | const router = new VueRouter({ 20 | routes 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /examples/vue2/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /examples/vue2/src/views/InfiniteScrolling.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 54 | 55 | 86 | -------------------------------------------------------------------------------- /examples/vue3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Condition Watcher 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "serve": "vite", 6 | "build": "vite build" 7 | }, 8 | "dependencies": { 9 | "element-plus": "^1.3.0-beta.1", 10 | "vue": "^3.2.45", 11 | "vue-condition-watcher": "1.4.7", 12 | "vue-router": "^4.1.6" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^3.2.0", 16 | "@vue/compiler-sfc": "^3.2.45", 17 | "typescript": "^4.9.3", 18 | "vite": "^3.2.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/vue3/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /examples/vue3/src/api.ts: -------------------------------------------------------------------------------- 1 | export type Users = { 2 | cell: string 3 | bob: { 4 | date: string 5 | age: number 6 | } 7 | email: string 8 | gender: string 9 | id: { 10 | name: string 11 | value: string 12 | } 13 | name: { 14 | title: string 15 | first: string 16 | last: string 17 | } 18 | phone: string 19 | picture: { 20 | large: string 21 | medium: string 22 | thumbnail: string 23 | } 24 | }[] 25 | export interface Result { 26 | info: { 27 | page: number 28 | results: number 29 | seed: string 30 | version: string 31 | } 32 | results: Users 33 | } 34 | 35 | const users = (params: Record) => 36 | fetch('https://randomuser.me/api/?' + query(params), { method: 'GET' }).then((res) => res.json() as Promise) 37 | 38 | const photos = () => fetch('https://jsonplaceholder.typicode.com/photos', { method: 'GET' }).then((res) => res.json()) 39 | 40 | function query(params: Record) { 41 | const esc = encodeURIComponent 42 | return Object.keys(params) 43 | .map((k) => esc(k) + '=' + esc(params[k])) 44 | .join('&') 45 | } 46 | 47 | export default { 48 | users, 49 | photos, 50 | } 51 | -------------------------------------------------------------------------------- /examples/vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './styles/index.css' 4 | import { router } from './router' 5 | import ElementPlus from 'element-plus' 6 | import 'element-plus/dist/index.css' 7 | 8 | createApp(App).use(ElementPlus).use(router).mount('#app') 9 | -------------------------------------------------------------------------------- /examples/vue3/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Home from './views/Home.vue' 3 | 4 | export const routerHistory = createWebHistory() 5 | export const router = createRouter({ 6 | history: routerHistory, 7 | strict: true, 8 | routes: [ 9 | { 10 | path: '/', 11 | components: { default: Home }, 12 | }, 13 | ], 14 | }) 15 | -------------------------------------------------------------------------------- /examples/vue3/src/styles/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | color: #2c3e50; 6 | padding: 1rem; 7 | } 8 | 9 | .footer { 10 | display: flex; 11 | justify-content: end; 12 | margin-top: 20px; 13 | } 14 | 15 | .status { 16 | color: #67C23A; 17 | } -------------------------------------------------------------------------------- /examples/vue3/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 176 | -------------------------------------------------------------------------------- /examples/vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["esnext", "dom"], 12 | "types": ["vite/client", "node", "vite-svg-loader", "element-plus/global"] 13 | }, 14 | "include": ["views/**.ts", "./*.ts", "./*.d.ts", "src/main.ts", "src/router.ts"] 15 | } -------------------------------------------------------------------------------- /examples/vue3/vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | 3 | export default { 4 | plugins: [vue()], 5 | optimizeDeps: { 6 | exclude: ['vue-demi'] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-condition-watcher", 3 | "version": "2.0.0-beta.3", 4 | "description": "Data fetching with Vue Composition API. Power of conditions to easily control and sync to the URL query string.", 5 | "main": "./core/dist/index.js", 6 | "module": "./core/dist/index.esm.js", 7 | "types": "./core/dist/core/index.d.ts", 8 | "packageManager": "pnpm@7.5.0", 9 | "files": [ 10 | "core/dist/**", 11 | "core/package.json", 12 | "package.json", 13 | "_internal/dist/**", 14 | "_internal/package.json" 15 | ], 16 | "exports": { 17 | "./package.json": "./package.json", 18 | ".": { 19 | "import": "./core/dist/index.mjs", 20 | "module": "./core/dist/index.esm.js", 21 | "require": "./core/dist/index.js", 22 | "types": "./core/dist/core/index.d.ts" 23 | }, 24 | "./_internal": { 25 | "import": "./_internal/dist/index.mjs", 26 | "module": "./_internal/dist/index.esm.js", 27 | "require": "./_internal/dist/index.js", 28 | "types": "./_internal/dist/_internal/index.d.ts" 29 | } 30 | }, 31 | "scripts": { 32 | "clean": "turbo run clean", 33 | "build": "turbo run build", 34 | "watch": "turbo run watch --parallel", 35 | "types:check": "turbo run types:check", 36 | "format": "prettier --write ./**/*.ts", 37 | "lint": "eslint . --ext .ts --cache", 38 | "lint:fix": "pnpm lint --fix", 39 | "test": "pnpm build && pnpm test:3", 40 | "test:2": "vue-demi-switch 2 vue2 && vitest run", 41 | "test:3": "vue-demi-switch 3 && vitest run", 42 | "test:all": "pnpm format && pnpm build && pnpm test:3 && pnpm test:2 && vue-demi-switch 3", 43 | "prepublishOnly": "pnpm format && pnpm clean && pnpm build", 44 | "publish-beta": "pnpm publish --tag beta" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/runkids/vue-condition-watcher.git" 49 | }, 50 | "keywords": [ 51 | "vue", 52 | "conditions", 53 | "watcher", 54 | "vue-hooks", 55 | "composition-api", 56 | "vue-composable", 57 | "composable", 58 | "fetch-data" 59 | ], 60 | "author": "Willy Hong", 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/runkids/vue-condition-watcher/issues" 64 | }, 65 | "homepage": "https://github.com/runkids/vue-condition-watcher#readme", 66 | "husky": { 67 | "hooks": { 68 | "pre-commit": "pnpm types:check && lint-staged" 69 | } 70 | }, 71 | "lint-staged": { 72 | "*.{ts, js}": [ 73 | "pnpm lint:fix", 74 | "pnpm format", 75 | "git add" 76 | ] 77 | }, 78 | "engines": { 79 | "node": ">=14", 80 | "pnpm": "7" 81 | }, 82 | "devDependencies": { 83 | "@types/node": "^17.0.34", 84 | "@typescript-eslint/eslint-plugin": "^5.25.0", 85 | "@typescript-eslint/parser": "^5.25.0", 86 | "@vue/composition-api": "^1.7.0", 87 | "@vue/test-utils": "^2.0.2", 88 | "bunchee": "^1.9.0", 89 | "eslint": "8.15.0", 90 | "eslint-config-prettier": "8.5.0", 91 | "husky": "2.4.1", 92 | "jsdom": "^20.0.0", 93 | "lint-staged": "8.2.1", 94 | "prettier": "^2.0.5", 95 | "rimraf": "^3.0.2", 96 | "turbo": "^1.3.1", 97 | "typescript": "^4.6.4", 98 | "vite": "^3.0.0", 99 | "vitest": "^0.18.0", 100 | "vue": "^3.2.45", 101 | "vue-condition-watcher": "workspace:*", 102 | "vue2": "npm:vue@2.6.14" 103 | }, 104 | "dependencies": { 105 | "vue-demi": "latest" 106 | }, 107 | "peerDependencies": { 108 | "@vue/composition-api": "^1.1.0", 109 | "vue": "^2.0.0 || >=3.0.0" 110 | }, 111 | "peerDependenciesMeta": { 112 | "@vue/composition-api": { 113 | "optional": true 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '_internal' 3 | - "core" 4 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { Vue2, install, isVue2 } from 'vue-demi' 2 | import { beforeAll, beforeEach } from 'vitest' 3 | 4 | const setupVueSwitch = () => { 5 | if (isVue2) { 6 | Vue2.config.productionTip = false 7 | Vue2.config.devtools = false 8 | install(Vue2) 9 | } 10 | } 11 | 12 | setupVueSwitch() 13 | 14 | beforeAll(() => { 15 | setupVueSwitch() 16 | }) 17 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "..", 5 | "outDir": ".", 6 | "strict": false 7 | }, 8 | "include": ["."] 9 | } 10 | -------------------------------------------------------------------------------- /test/use-condition-watcher.test.ts: -------------------------------------------------------------------------------- 1 | import useConditionWatcher from 'vue-condition-watcher' 2 | import { isRef, isReactive, isReadonly, defineComponent, isVue3 } from 'vue-demi' 3 | import type { VueWrapper } from '@vue/test-utils' 4 | import { flushPromises, mount } from '@vue/test-utils' 5 | import { describe, expect, beforeEach } from 'vitest' 6 | 7 | describe('Basic test of vue-condition-watcher', () => { 8 | let basicTestConfig: any = {} 9 | beforeEach(() => { 10 | basicTestConfig = { 11 | fetcher: (params) => Promise.resolve(params), 12 | conditions: { 13 | gender: ['male'], 14 | results: 9, 15 | }, 16 | defaultParams: { 17 | name: 'runkids', 18 | }, 19 | } 20 | }) 21 | 22 | it(`Check return value type`, () => { 23 | const { conditions, data, error, isLoading, execute, isFetching } = useConditionWatcher(basicTestConfig) 24 | 25 | expect(isReactive(conditions)).toBeTruthy() 26 | expect(isRef(data)).toBeTruthy() 27 | expect(isRef(error)).toBeTruthy() 28 | expect(isRef(isLoading)).toBeTruthy() 29 | expect(isRef(isFetching)).toBeTruthy() 30 | expect(isLoading.value).toBeTypeOf('boolean') 31 | expect(isLoading.value).toBe(true) 32 | expect(isFetching.value).toBe(false) 33 | expect(execute).toBeTypeOf('function') 34 | }) 35 | 36 | it(`Check data, error, isLoading, isFetching is readonly`, () => { 37 | const { data, error, isLoading, isFetching } = useConditionWatcher(basicTestConfig) 38 | 39 | expect(isReadonly(data)).toBeTruthy() 40 | expect(isReadonly(error)).toBeTruthy() 41 | expect(isReadonly(isLoading)).toBeTruthy() 42 | expect(isReadonly(isFetching)).toBeTruthy() 43 | }) 44 | 45 | it(`Condition should be change`, () => { 46 | const { conditions } = useConditionWatcher(basicTestConfig) 47 | 48 | expect(conditions).toMatchObject({ 49 | gender: ['male'], 50 | results: 9, 51 | }) 52 | conditions.results = 10 53 | conditions.gender = ['male', 'female'] 54 | 55 | expect(conditions).toMatchObject({ 56 | gender: ['male', 'female'], 57 | results: 10, 58 | }) 59 | }) 60 | 61 | it('Reset conditions to initial value', () => { 62 | const { conditions, resetConditions } = useConditionWatcher(basicTestConfig) 63 | 64 | conditions.results = 10 65 | conditions.gender = ['male', 'female'] 66 | 67 | resetConditions() 68 | 69 | expect(conditions).toMatchObject({ 70 | gender: ['male'], 71 | results: 9, 72 | }) 73 | }) 74 | 75 | it('Reset conditions to custom value and only assign if property exists', () => { 76 | const { conditions, resetConditions } = useConditionWatcher(basicTestConfig) 77 | 78 | conditions.results = 10 79 | conditions.gender = ['male', 'female'] 80 | 81 | resetConditions({ 82 | gender: ['female'], 83 | results: 19, 84 | type: '2', 85 | useless: '12312321', 86 | }) 87 | 88 | expect(conditions).toMatchObject({ 89 | gender: ['female'], 90 | results: 19, 91 | }) 92 | }) 93 | }) 94 | 95 | if (isVue3) { 96 | describe('useConditionWatcher', () => { 97 | const App = defineComponent({ 98 | template: `
{{ data }}
`, 99 | setup() { 100 | const { data, conditions } = useConditionWatcher({ 101 | fetcher: (params) => Promise.resolve(`Hello, world! ${params.name}!`), 102 | conditions: { 103 | name: '', 104 | }, 105 | }) 106 | return { 107 | data, 108 | conditions, 109 | } 110 | }, 111 | }) 112 | 113 | let wrapper: VueWrapper 114 | 115 | beforeEach(() => { 116 | wrapper = mount(App) 117 | }) 118 | 119 | afterEach(() => { 120 | wrapper.unmount() 121 | }) 122 | 123 | it('Should be defined', async () => { 124 | expect(wrapper).toBeDefined() 125 | }) 126 | 127 | it('Refetch after conditions value changed.', async () => { 128 | expect(wrapper.text()).toContain('') 129 | wrapper.vm.conditions.name = 'RUNKIDS' 130 | await flushPromises() 131 | expect(wrapper.text()).toContain(`Hello, world! RUNKIDS!`) 132 | }) 133 | }) 134 | } 135 | 136 | // const tick = async (times: number) => { 137 | // for (let _ in [...Array(times).keys()]) { 138 | // await nextTick() 139 | // } 140 | // } 141 | 142 | // function doAsync(c) { 143 | // setTimeout(() => { 144 | // c() 145 | // }, 0) 146 | // } 147 | 148 | // describe('useConditionWatcher', () => { 149 | // const root = document.createElement('div') 150 | // const consoleWarnSpy = vi.spyOn(console, 'warn') 151 | 152 | // beforeEach(() => { 153 | // consoleWarnSpy.mockClear() 154 | // }) 155 | 156 | // /** 157 | // * @jest-environment jsdom 158 | // */ 159 | 160 | // test('use jsdom in this test file', () => { 161 | // expect(root).not.toBeNull() 162 | // }) 163 | 164 | // it(`Should return data from a promise`, () => { 165 | // const vm = createApp({ 166 | // template: `
{{data}}
`, 167 | // setup() { 168 | // const config = { 169 | // fetcher: () => new Promise((resolve) => resolve('ConditionWatcher')), 170 | // immediate: true, 171 | // conditions: { 172 | // gender: ['male'], 173 | // results: 9, 174 | // }, 175 | // } 176 | // return useConditionWatcher(config) 177 | // }, 178 | // }).mount(root) 179 | 180 | // doAsync(async () => { 181 | // expect(vm.$el.textContent).toBe('') 182 | // await tick(1) 183 | // expect(vm.$el.textContent).toBe('ConditionWatcher') 184 | // }) 185 | // }) 186 | 187 | // it(`isLoading state should return true until promise resolve`, () => { 188 | // const vm = createApp({ 189 | // template: `
isLoading:{{isLoading}}, result:{{data}}
`, 190 | // setup() { 191 | // const config = { 192 | // fetcher: () => new Promise((resolve) => setTimeout(() => resolve('ConditionWatcher'), 200)), 193 | // conditions: { 194 | // gender: ['male'], 195 | // results: 9, 196 | // }, 197 | // } 198 | // return useConditionWatcher(config) 199 | // }, 200 | // }).mount(root) 201 | 202 | // doAsync(async () => { 203 | // await tick(1) 204 | // expect(vm.$el.textContent).toBe('isLoading:true, result:') 205 | // await tick(1) 206 | // expect(vm.$el.textContent).toBe('isLoading:false, result:ConditionWatcher') 207 | // }) 208 | // }) 209 | 210 | // it(`Fetcher's params should same by condition and defaultParams`, () => { 211 | // const vm = createApp({ 212 | // template: `
{{data}}
`, 213 | // setup() { 214 | // const config = { 215 | // fetcher: (params) => new Promise((resolve) => resolve(JSON.stringify(params))), 216 | // conditions: { 217 | // results: 9, 218 | // name: 'runkids', 219 | // }, 220 | // defaultParams: { 221 | // limit: 10, 222 | // offset: 1, 223 | // }, 224 | // } 225 | // return useConditionWatcher(config) 226 | // }, 227 | // }).mount(root) 228 | 229 | // doAsync(async () => { 230 | // await tick(1) 231 | // expect(JSON.parse(vm.$el.textContent)).toMatchObject({ 232 | // results: 9, 233 | // name: 'runkids', 234 | // limit: 10, 235 | // offset: 1, 236 | // }) 237 | // }) 238 | // }) 239 | 240 | // it(`Fetcher's params should same with beforeFetch return object`, () => { 241 | // const vm = createApp({ 242 | // template: `
{{data}}
`, 243 | // setup() { 244 | // const config = { 245 | // fetcher: (params) => new Promise((resolve) => resolve(JSON.stringify(params))), 246 | // conditions: { 247 | // results: 9, 248 | // date: new Date('2020-05-22'), 249 | // }, 250 | // beforeFetch(conditions) { 251 | // const d = conditions.date 252 | // conditions.date = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}` 253 | // return conditions 254 | // }, 255 | // } 256 | // return useConditionWatcher(config) 257 | // }, 258 | // }).mount(root) 259 | 260 | // doAsync(async () => { 261 | // await tick(1) 262 | // expect(JSON.parse(vm.$el.textContent)).toMatchObject({ 263 | // results: 9, 264 | // date: '2020-5-22', 265 | // }) 266 | // }) 267 | // }) 268 | 269 | // it(`If conditions attribute's type is array, fetcher's param should be string`, () => { 270 | // const vm = createApp({ 271 | // template: `
{{data}}
`, 272 | // setup() { 273 | // const config = { 274 | // fetcher: (params) => new Promise((resolve) => resolve(params.gender)), 275 | // conditions: { 276 | // gender: ['male', 'female'], 277 | // }, 278 | // } 279 | // return useConditionWatcher(config) 280 | // }, 281 | // }).mount(root) 282 | 283 | // doAsync(async () => { 284 | // await tick(1) 285 | // expect(vm.$el.textContent).toBe('male,female') 286 | // }) 287 | // }) 288 | 289 | // it(`Initial values of conditions by config`, () => { 290 | // const vm = createApp({ 291 | // template: `
{{conditions.count}}
`, 292 | // setup() { 293 | // const { conditions, resetConditions } = useConditionWatcher({ 294 | // fetcher: (params) => new Promise((resolve) => resolve(params)), 295 | // conditions: { 296 | // count: 0, 297 | // }, 298 | // }) 299 | 300 | // conditions.count = 10 301 | 302 | // resetConditions() 303 | 304 | // return { 305 | // conditions, 306 | // } 307 | // }, 308 | // }).mount(root) 309 | 310 | // doAsync(async () => { 311 | // await tick(1) 312 | // expect(vm.$el.textContent).toBe('0') 313 | // }) 314 | // }) 315 | // }) 316 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | filterNoneValueObject, 3 | createParams, 4 | stringifyQuery, 5 | syncQuery2Conditions, 6 | isEquivalent, 7 | deepClone, 8 | typeOf, 9 | pick, 10 | } from 'vue-condition-watcher/_internal' 11 | import { describe, expect, test } from 'vitest' 12 | 13 | describe('utils: isEquivalent', () => { 14 | it(`Check Object Equality`, () => { 15 | const current = { 16 | name: '', 17 | tags: [], 18 | phone: undefined, 19 | address: null, 20 | data: new Date(), 21 | } 22 | const old = { 23 | name: '', 24 | tags: [], 25 | phone: undefined, 26 | address: null, 27 | data: new Date(), 28 | } 29 | expect(isEquivalent(current, old)).toBeTruthy() 30 | }) 31 | }) 32 | 33 | describe('utils: typeof', () => { 34 | it(`Is array`, () => { 35 | expect(typeOf([])).toEqual('array') 36 | }) 37 | it(`Is date`, () => { 38 | expect(typeOf(new Date())).toEqual('date') 39 | }) 40 | it(`Is object`, () => { 41 | expect(typeOf({})).toEqual('object') 42 | }) 43 | it(`Is number`, () => { 44 | expect(typeOf(1)).toEqual('number') 45 | }) 46 | it(`Is string`, () => { 47 | expect(typeOf('hi')).toEqual('string') 48 | }) 49 | it(`Is null`, () => { 50 | expect(typeOf(null)).toEqual('null') 51 | }) 52 | it(`Is undefined`, () => { 53 | expect(typeOf(undefined)).toEqual('undefined') 54 | }) 55 | it(`Is function`, () => { 56 | expect( 57 | typeOf(() => { 58 | // 59 | }) 60 | ).toEqual('function') 61 | }) 62 | }) 63 | 64 | describe('utils: deepClone', () => { 65 | it(`Check Object deepClone`, () => { 66 | const current = { 67 | name: '', 68 | tags: [], 69 | phone: undefined, 70 | address: null, 71 | data: new Date(), 72 | } 73 | const newObj = deepClone(current) 74 | expect(newObj !== current).toBeTruthy() 75 | expect(newObj.data !== current.data).toBeTruthy() 76 | }) 77 | }) 78 | 79 | describe('utils: filterNoneValueObject', () => { 80 | it(`Should be return empty object`, () => { 81 | const conditions = { 82 | name: '', 83 | tags: [], 84 | phone: undefined, 85 | address: null, 86 | } 87 | const result = filterNoneValueObject(conditions) 88 | 89 | expect(Object.keys(result).length === 0).toBeTruthy() 90 | }) 91 | 92 | it(`Should be return an object with key: [age]`, () => { 93 | const conditions = { 94 | name: '', 95 | tags: [], 96 | phone: undefined, 97 | address: null, 98 | age: 20, 99 | } 100 | const result = filterNoneValueObject(conditions) 101 | 102 | expect(Object.keys(result).length === 1).toBeTruthy() 103 | expect(result).toMatchObject({ age: 20 }) 104 | }) 105 | }) 106 | 107 | describe('utils: stringifyQuery', () => { 108 | it('should return query string', () => { 109 | const conditions = { 110 | age: 20, 111 | tags: ['react', 'vue'], 112 | } 113 | const params = createParams(conditions) 114 | const query = stringifyQuery(params) 115 | expect(query).toBe('age=20&tags=react%2Cvue') 116 | }) 117 | 118 | it('should return query string and filter keys', () => { 119 | const conditions = { 120 | age: 20, 121 | tags: ['react', 'vue'], 122 | } 123 | const params = createParams(conditions) 124 | const query = stringifyQuery(params, ['age']) 125 | expect(query).toBe('tags=react%2Cvue') 126 | }) 127 | }) 128 | 129 | describe('utils: syncQuery2Conditions', () => { 130 | it('should sync query object to conditions', () => { 131 | const query = { 132 | age: 50, 133 | tags: 'react,vue', 134 | } 135 | const conditions = { 136 | age: 20, 137 | tags: ['react', 'vue'], 138 | } 139 | syncQuery2Conditions(conditions, query, conditions) 140 | expect(conditions).toMatchObject({ 141 | age: 50, 142 | tags: ['react', 'vue'], 143 | }) 144 | }) 145 | 146 | it('should sync query object to conditions with date', () => { 147 | const query = { 148 | date: '2020-01-02', 149 | } 150 | const conditions = { 151 | date: new Date(), 152 | } 153 | syncQuery2Conditions(conditions, query, conditions) 154 | expect(Object.prototype.toString.call(conditions.date) === '[object Date]').toBeTruthy() 155 | }) 156 | 157 | it('should sync query object to conditions with Array', () => { 158 | const query = { 159 | daterange: ['2020-01-02', '2020-01-03'], 160 | } 161 | const conditions = { 162 | daterange: [], 163 | } 164 | syncQuery2Conditions(conditions, query, conditions) 165 | expect(conditions.daterange).toMatchObject(['2020-01-02', '2020-01-03']) 166 | }) 167 | 168 | it('should sync query object to conditions with Array', () => { 169 | const query = { 170 | daterange: [1, 2], 171 | } 172 | const conditions = { 173 | daterange: [], 174 | } 175 | syncQuery2Conditions(conditions, query, conditions) 176 | expect(conditions.daterange).toMatchObject([1, 2]) 177 | }) 178 | 179 | it('if query is empty conditions should set init value', () => { 180 | const query = {} 181 | const conditions = { 182 | date: new Date(), 183 | string: 'runkids', 184 | array: ['react', 'vue'], 185 | boolean: false, 186 | undefined: undefined, 187 | null: null, 188 | } 189 | syncQuery2Conditions(conditions, query, conditions) 190 | 191 | expect(conditions).toMatchObject({ 192 | date: '', 193 | string: '', 194 | array: [], 195 | boolean: '', 196 | undefined: '', 197 | null: '', 198 | }) 199 | }) 200 | }) 201 | 202 | describe('utils: pick', () => { 203 | it('Assign only if property exists in target object', () => { 204 | const targetObj = { name: 'Runkids', age: 10 } 205 | const resObj = pick({ age: 20, name: 'runkids', nickname: 'egg' }, Object.keys(targetObj)) 206 | 207 | expect(Object.assign(targetObj, resObj)).toMatchObject({ 208 | name: 'runkids', 209 | age: 20, 210 | }) 211 | }) 212 | }) 213 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": [ 6 | "esnext", "dom" 7 | ], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "downlevelIteration": true, 16 | "outDir": "./dist", 17 | "rootDir": "core", 18 | "baseUrl": ".", 19 | "types": [ 20 | "node", 21 | "vitest", 22 | "vitest/globals" 23 | ], 24 | "target": "es2017", 25 | "paths": { 26 | "vue-condition-watcher": ["./core/index.ts"] 27 | }, 28 | "typeRoots": ["./node_modules/@types"], 29 | "incremental": true 30 | }, 31 | "exclude": [ 32 | "node_modules", 33 | "./**/dist" 34 | ], 35 | "include": [ 36 | "core", 37 | "_internal", 38 | "vitest.config.ts" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "baseBranch": "origin/master", 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"] 7 | }, 8 | "types:check": {}, 9 | "watch": { 10 | "cache": false 11 | }, 12 | "clean": { 13 | "cache": false 14 | } 15 | }, 16 | "globalDependencies": [ 17 | "tsconfig.json", 18 | "_internal/composable/**", 19 | "_internal/utils/**", 20 | "_internal/index.ts", 21 | "_internal/types.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | environment: 'jsdom', 8 | reporters: 'dot', 9 | setupFiles: [resolve(__dirname, './test/setup.ts')], 10 | deps: { 11 | inline: ['vue2', '@vue/composition-api', 'vue-demi'], 12 | }, 13 | }, 14 | }) 15 | --------------------------------------------------------------------------------