├── .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 | [](https://circleci.com/gh/runkids/vue-condition-watcher) [](https://vuejs.org/) [](https://composition-api.vuejs.org/) [](https://www.npmjs.com/package/vue-condition-watcher) [](https://www.npmjs.com/package/vue-condition-watcher) [](https://bundlephobia.com/result?p=vue-condition-watcher) [](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 |
690 | {{ data?.detail }}
691 |
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 |
806 |
811 |
812 | {{ `${history.created_at}: ${history.amount}` }}
813 |
814 |
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 |
917 | Refetch Data
918 | Reset Offset
919 |
920 |
924 |
925 |
926 | {{ info }}
927 |
928 |
929 |
934 |
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 | [](https://circleci.com/gh/runkids/vue-condition-watcher) [](https://vuejs.org/) [](https://composition-api.vuejs.org/) [](https://www.npmjs.com/package/vue-condition-watcher) [](https://www.npmjs.com/package/vue-condition-watcher) [](https://bundlephobia.com/result?p=vue-condition-watcher) [](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 |
111 |
112 |
113 |
114 |
115 |
116 | {{ !loading ? data : 'Loading...' }}
117 |
118 | {{ error }}
119 |
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 |
560 | {{ data?.detail }}
561 |
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 |
678 |
683 |
684 | {{ `${history.created_at}: ${history.amount}` }}
685 |
686 |
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 |
769 | Refetch Data
770 | Reset Offset
771 |
772 |
776 |
777 |
778 | {{ info }}
779 |
780 |
781 |
786 |
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 |
2 | ]
3 |
4 |
5 |
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 |
2 |
3 |
Demo Infinite Scrolling
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
![]()
19 |
20 |
{{ `${item.name.first} ${item.name.last}` }}
21 |
{{ item.email }}
22 |
{{ item.phone }}
23 |
24 |
25 |
Loading...
26 |
27 |
28 |
29 |
63 |
--------------------------------------------------------------------------------
/examples/vue2/src/views/InfiniteScrolling.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ i.id }}
6 |
7 |
8 |
9 | LOADING...
10 |
11 |
Counter: {{ items.length }}
12 |
13 |
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 |
2 |
3 |
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 |
100 |
101 |
102 |
103 |
104 | Male
105 |
106 |
107 | Female
108 |
109 |
110 |
111 |
112 |
113 |
114 | Stop Interval
115 |
116 |
117 | Set Interval 3s
118 |
119 |
120 | Set Interval 10s
121 |
122 |
123 |
124 |
125 | Refresh
126 | Reset Conditions
127 | Mutate First Row Data
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | pollingWhenHidden: true
140 |
141 |
142 | pollingWhenOffline: true
143 |
144 |
145 | revalidateOnFocus: true
146 |
147 |
148 | Results Size: {{ data && data.length }}
149 |
150 |
151 | Count of data fetching : {{ fetchCounts }}
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | {{ $index + 1}}
160 |
161 |
162 |
163 |
164 |
165 | {{`${row.name.first} ${row.name.last}`}}
166 |
167 |
168 |
169 |
170 |
171 |
172 |
175 |
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 |
--------------------------------------------------------------------------------