├── .circleci └── config.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── examples-ts ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── components │ │ ├── UseAsync.vue │ │ ├── UseAsyncWithAbort.vue │ │ ├── UseAsyncWithRefParam.vue │ │ ├── UseFetch.vue │ │ └── UseFetchWithRefParam.vue │ ├── main.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts ├── tsconfig.json └── vue.config.js ├── examples ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── components │ │ ├── UseAsync.vue │ │ ├── UseAsyncWithAbort.vue │ │ ├── UseAsyncWithRefParam.vue │ │ ├── UseFetch.vue │ │ └── UseFetchWithRefParam.vue │ └── main.js └── vue.config.js └── lib ├── .eslintrc.js ├── .npmignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── img └── vue-async-function.png ├── package-lock.json ├── package.json ├── src └── index.ts ├── tests ├── useAsync.spec.js └── useFetch.spec.js └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.16 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | - restore_cache: 13 | keys: 14 | - v1-dependencies-{{ checksum "lib/package.json" }} 15 | - v1-dependencies- 16 | 17 | - run: 18 | name: npm install 19 | command: npm install 20 | working_directory: lib 21 | 22 | - save_cache: 23 | paths: 24 | - lib/node_modules 25 | key: v1-dependencies-{{ checksum "lib/package.json" }} 26 | 27 | - run: 28 | name: npm test:lint 29 | command: npm run test:lint 30 | working_directory: lib 31 | 32 | - run: 33 | name: npm test 34 | command: npm test 35 | working_directory: lib 36 | 37 | - run: 38 | name: npm run build 39 | command: npm run build 40 | working_directory: lib 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.prettierPath": "examples/node_modules/prettier" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Vue Async Function 3 |

4 |
5 | 6 |

7 | 8 | npm version 9 | 10 | 11 | npm downloads 12 | 13 | 14 | minified size 15 | 16 | 17 | license 18 | 19 | 20 | issues 21 | 22 | 23 | pull requests 24 | 25 |

26 | 27 | Vue Async Function delivers a compositional API for promise resolution and data fetching. It is inspired by the hooks 28 | functions of [React Async](https://github.com/ghengeveld/react-async) and builds upon the 29 | [Vue Composition API](https://vue-composition-api-rfc.netlify.com) that is coming with Vue 3.0. 30 | 31 | Read more about the library in the [README](lib/README.md). 32 | -------------------------------------------------------------------------------- /examples-ts/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /examples-ts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | "plugin:vue/essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint", 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples-ts/.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 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /examples-ts/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // changed in prettier 2.x, vscode plugin is not updated yet 3 | arrowParens: "always", 4 | trailingComma: "es5", 5 | }; 6 | -------------------------------------------------------------------------------- /examples-ts/README.md: -------------------------------------------------------------------------------- 1 | # examples-ts 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /examples-ts/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-ts", 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": "0.5.0", 12 | "core-js": "^3.6.4", 13 | "vue": "^2.6.11", 14 | "vue-async-function": "file:../lib" 15 | }, 16 | "devDependencies": { 17 | "@typescript-eslint/eslint-plugin": "2.25.0", 18 | "@typescript-eslint/parser": "2.25.0", 19 | "@vue/cli-plugin-babel": "^4.2.3", 20 | "@vue/cli-plugin-eslint": "^4.2.3", 21 | "@vue/cli-plugin-typescript": "^4.2.3", 22 | "@vue/cli-service": "^4.2.3", 23 | "@vue/eslint-config-prettier": "^6.0.0", 24 | "@vue/eslint-config-typescript": "^5.0.2", 25 | "eslint": "^6.8.0", 26 | "eslint-plugin-prettier": "^3.1.2", 27 | "eslint-plugin-vue": "^6.2.2", 28 | "prettier": "^2.0.2", 29 | "source-map-loader": "0.2.4", 30 | "typescript": "~3.8.3", 31 | "vue-template-compiler": "^2.6.11" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples-ts/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples-ts/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertBrand/vue-async-function/7aa2312cda5e6201199db1688266e947ac7aee47/examples-ts/public/favicon.ico -------------------------------------------------------------------------------- /examples-ts/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | examples-ts 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples-ts/src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | 50 | -------------------------------------------------------------------------------- /examples-ts/src/components/UseAsync.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /examples-ts/src/components/UseAsyncWithAbort.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /examples-ts/src/components/UseAsyncWithRefParam.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 48 | 49 | 54 | -------------------------------------------------------------------------------- /examples-ts/src/components/UseFetch.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /examples-ts/src/components/UseFetchWithRefParam.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /examples-ts/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueCompositionApi from "@vue/composition-api"; 3 | Vue.use(VueCompositionApi); 4 | 5 | import App from "./App.vue"; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | render: (h) => h(App), 11 | }).$mount("#app"); 12 | -------------------------------------------------------------------------------- /examples-ts/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples-ts/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /examples-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "types": ["webpack-env"], 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 18 | }, 19 | "include": [ 20 | "src/**/*.ts", 21 | "src/**/*.tsx", 22 | "src/**/*.vue", 23 | "tests/**/*.ts", 24 | "tests/**/*.tsx" 25 | ], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples-ts/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); // eslint-disable-line @typescript-eslint/no-var-requires 2 | 3 | // use vue-async-function sourcemap when generating source maps 4 | function addSourceMapLoader(config) { 5 | config.module 6 | .rule("source-map-loader") 7 | .test(/\.js$/) 8 | .enforce("pre") 9 | .include.add(path.join(__dirname, "node_modules/vue-async-function")) 10 | .end() 11 | .use("source-map-loader") 12 | .loader("source-map-loader") 13 | .end(); 14 | } 15 | 16 | module.exports = { 17 | configureWebpack: { 18 | devtool: "source-map", 19 | // enable symlinked resolving of vue-async-function, make sure to load one Vue and plugin module 20 | resolve: { 21 | symlinks: false, 22 | alias: { 23 | vue: path.resolve(__dirname, "node_modules/vue"), 24 | "@vue/composition-api": path.resolve( 25 | __dirname, 26 | "node_modules/@vue/composition-api" 27 | ), 28 | }, 29 | }, 30 | }, 31 | chainWebpack: (config) => { 32 | addSourceMapLoader(config); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /examples/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /examples/.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" ? "error" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/.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 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /examples/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // changed in prettier 2.x, vscode plugin is not updated yet 3 | arrowParens: "always", 4 | trailingComma: "es5", 5 | }; 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # vue-async-function example 2 | 3 | Showcase of some `vue-async-function` examples. 4 | 5 | ## Install and run 6 | 7 | Make sure to install dependencies first: 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | Then check out the examples in development mode: 14 | 15 | ``` 16 | npm run serve -- --open 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-async-function-example", 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": "^0.5.0", 12 | "core-js": "^3.6.4", 13 | "vue": "^2.6.11", 14 | "vue-async-function": "file:../lib" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "^4.2.3", 18 | "@vue/cli-plugin-eslint": "^4.2.3", 19 | "@vue/cli-service": "4.2.3", 20 | "@vue/eslint-config-prettier": "^6.0.0", 21 | "babel-eslint": "^10.1.0", 22 | "eslint": "^6.8.0", 23 | "eslint-plugin-prettier": "^3.1.2", 24 | "eslint-plugin-vue": "^6.2.2", 25 | "prettier": "^2.0.2", 26 | "source-map-loader": "0.2.4", 27 | "vue-template-compiler": "^2.6.11" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertBrand/vue-async-function/7aa2312cda5e6201199db1688266e947ac7aee47/examples/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-async-function-example 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 42 | 43 | 49 | -------------------------------------------------------------------------------- /examples/src/components/UseAsync.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /examples/src/components/UseAsyncWithAbort.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /examples/src/components/UseAsyncWithRefParam.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /examples/src/components/UseFetch.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /examples/src/components/UseFetchWithRefParam.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /examples/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueCompositionApi from "@vue/composition-api"; 3 | Vue.use(VueCompositionApi); 4 | 5 | import App from "./App.vue"; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | render: (h) => h(App), 11 | }).$mount("#app"); 12 | -------------------------------------------------------------------------------- /examples/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | // use vue-async-function sourcemap when generating source maps 4 | function addSourceMapLoader(config) { 5 | config.module 6 | .rule("source-map-loader") 7 | .test(/\.js$/) 8 | .enforce("pre") 9 | .include.add(path.join(__dirname, "node_modules/vue-async-function")) 10 | .end() 11 | .use("source-map-loader") 12 | .loader("source-map-loader") 13 | .end(); 14 | } 15 | 16 | module.exports = { 17 | configureWebpack: { 18 | devtool: "source-map", 19 | // enable symlinked resolving of vue-async-function, make sure to load one Vue and plugin module 20 | resolve: { 21 | symlinks: false, 22 | alias: { 23 | vue: path.resolve(__dirname, "node_modules/vue"), 24 | "@vue/composition-api": path.resolve( 25 | __dirname, 26 | "node_modules/@vue/composition-api" 27 | ), 28 | }, 29 | }, 30 | }, 31 | chainWebpack: (config) => { 32 | addSourceMapLoader(config); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /lib/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2020: true, 6 | node: true, 7 | jest: true, 8 | }, 9 | extends: ["eslint:recommended", "plugin:prettier/recommended"], 10 | parser: "babel-eslint", 11 | rules: { 12 | "prettier/prettier": "warn", 13 | }, 14 | globals: { 15 | RequestInfo: "readonly", 16 | RequestInit: "readonly", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/.npmignore: -------------------------------------------------------------------------------- 1 | /.circleci 2 | /coverage 3 | /examples 4 | /img 5 | /tests 6 | .DS_Store 7 | .eslintrc.js 8 | .prettierrc.js 9 | babel.config.js 10 | tsconfig.json -------------------------------------------------------------------------------- /lib/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // changed in prettier 2.x, vscode plugin is not updated yet 3 | arrowParens: "always", 4 | trailingComma: "es5", 5 | }; 6 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 |

2 | Vue Async Function 3 |

4 |
5 | 6 |

7 | 8 | npm version 9 | 10 | 11 | npm downloads 12 | 13 | 14 | minified size 15 | 16 | 17 | license 18 | 19 | 20 | issues 21 | 22 | 23 | pull requests 24 | 25 |

26 | 27 | Vue Async Function delivers a compositional API for promise resolution and data fetching. It is inspired by the hooks 28 | functions of [React Async](https://github.com/ghengeveld/react-async) and builds upon the 29 | [Vue Composition API](https://vue-composition-api-rfc.netlify.com) that is coming with Vue 3.0. Luckily, thanks to 30 | [the official plugin](https://github.com/vuejs/composition-api) you can build Vue apps with the new Composition API 31 | today with Vue 2.5+. 32 | 33 | - Works with promises, async/await and the Fetch API 34 | - Provides `abort` and `retry` functions 35 | - Supports abortable fetch by providing an AbortController signal 36 | - Reactive retry when arguments are `ref`-wrapped 37 | - Written in TypeScript, ships with type definitions 38 | 39 | ## Installation 40 | 41 | The current version expects your project to be built with [vue-cli 4.0+](https://cli.vuejs.org). There's no support for 42 | previous verions. If your project is still built with 3.x, consider upgrading it. There's a detailed 43 | [upgrading guide](https://cli.vuejs.org/migrating-from-v3) available. 44 | 45 | In your Vue project, you need to install `@vue/composition-api` together with `vue-async-function`: 46 | 47 | ```bash 48 | npm install --save @vue/composition-api vue-async-function 49 | ``` 50 | 51 | Or with Yarn: 52 | 53 | ```bash 54 | yarn add @vue/composition-api vue-async-function 55 | ``` 56 | 57 | Then modify your entrypoint (often `main.js` or `main.ts`), as stated in the 58 | [Vue Composition API docs](https://github.com/vuejs/composition-api#installation): 59 | 60 | ```javascript 61 | import Vue from "vue"; 62 | import VueCompositionApi from "@vue/composition-api"; 63 | 64 | Vue.use(VueCompositionApi); 65 | ``` 66 | 67 | After that, you can import `useAsync` or `useFetch`: 68 | 69 | ```javascript 70 | import { useAsync, useFetch } from "vue-async-function"; 71 | ``` 72 | 73 | ## useAsync usage 74 | 75 | Inside your `setup()` function you can call `useAsync` and provide it a function that returns a Promise as its first 76 | argument. `useAsync` returns three ref-wrapped properties: `isLoading`, `error` and `data`. These are reactively updated 77 | to match the state of the asynchronous process while resolution takes place. You also get two functions, `retry` and 78 | `abort` that respectively retry the original asynchronous function or abort the current running function. 79 | 80 | You can choose to return any of these values to use them in the component template or pass them to other functions to 81 | 'compose' your component. A simple example: 82 | 83 | ```javascript 84 | export default { 85 | setup() { 86 | const { data, error, isLoading, retry, abort } = useAsync(someAsyncFunc); 87 | // ... 88 | return { data, error, isLoading, retry, abort }; 89 | } 90 | }; 91 | ``` 92 | 93 | The second argument of `useAsync` is optional. If provided, it is passed as first argument to the Promise returning 94 | function. 95 | 96 | ```javascript 97 | export default { 98 | setup() { 99 | return useAsync(someAsyncFunc, { id: 9000 }); 100 | } 101 | }; 102 | ``` 103 | 104 | ### AbortController 105 | 106 | `useAsync` calls the asynchronous function for you with the optional first argument. Its second argument is an instance 107 | of an [AbortController signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal). Your function 108 | should listen to the 'abort' event on the signal and cancel its behavior when triggered. 109 | 110 | In the following example, the `wait` function simply waits for a configurable period and then resolves to a string. If 111 | the `abort` function returned from the `useAsync` call is triggered, the timeout is cleared and the promise won't 112 | resolve. It is up to you to decide if the promise needs to be rejected as well. 113 | 114 | ```javascript 115 | async function wait({ millis }, signal) { 116 | return new Promise(resolve => { 117 | const timeout = setTimeout( 118 | () => resolve(`Done waiting ${millis} milliseconds!`), 119 | millis 120 | ); 121 | signal.addEventListener("abort", () => clearTimeout(timeout)); 122 | }); 123 | } 124 | 125 | export default { 126 | setup() { 127 | return useAsync(wait, { millis: 10000 }); 128 | } 129 | }; 130 | ``` 131 | 132 | Note: calling `retry` while the asynchronous function is not resolved calls `abort` as well. 133 | 134 | ### Reactive arguments 135 | 136 | If you want your application to reactively respond to changing input values for `useAsync`, you can pass in a 137 | `ref`-wrapped value as well as any parameter. 138 | 139 | An example: 140 | 141 | ```javascript 142 | import { ref } from "@vue/composition-api"; 143 | // ... 144 | 145 | export default { 146 | setup() { 147 | const wrappedAsyncFunc = ref(someAsyncFunc); 148 | const wrappedParams = ref({ id: 9000 }); 149 | const { data, error, isLoading, retry, abort } = useAsync(someAsyncFunc); 150 | // ... 151 | watch(someVal, () => { 152 | wrappedAsyncFunc.value = someOtherAsyncFunc; // triggers retry 153 | // or 154 | wrappedParams.value = { id: 10000 }; // triggers retry 155 | }); 156 | // ... 157 | return { data, error, isLoading, retry, abort }; 158 | } 159 | }; 160 | ``` 161 | 162 | Note that the triggered retry is the same as when you call `retry` explicitly, so it will also call `abort` for 163 | unresolved functions. 164 | 165 | ## useFetch usage 166 | 167 | With `useAsync` you could wrap the Fetch API easily. An example: 168 | 169 | ```javascript 170 | async function loadStarship({ id }, signal) { 171 | const headers = { Accept: "application/json" }; 172 | const res = await fetch(`https://swapi.co/api/starships/${id}/`, { 173 | headers, 174 | signal 175 | }); 176 | if (!res.ok) throw res; 177 | return res.json(); 178 | } 179 | 180 | export default { 181 | setup() { 182 | return useAsync(loadStarship, { id: 2 }); 183 | } 184 | }; 185 | ``` 186 | 187 | This is such a common pattern that there is a special function for it: `useFetch`. We can implement above example as 188 | follows: 189 | 190 | ```javascript 191 | import { useFetch } from "vue-async-function"; 192 | 193 | export default { 194 | setup() { 195 | const id = 9; 196 | const url = `https://swapi.co/api/starships/${id}/`; 197 | const headers = { Accept: "application/json" }; 198 | return useFetch(url, { headers }); 199 | } 200 | }; 201 | ``` 202 | 203 | `useFetch` accepts the same arguments as the browser Fetch API. It will hook up the `AbortController` signal for you and 204 | based on the `Accept` header it will choose between returning `text()` or `json()` results. 205 | 206 | ### Reactive arguments 207 | 208 | Above example shines even more with reactive arguments: 209 | 210 | ```javascript 211 | import { useFetch } from "vue-async-function"; 212 | import { ref, computed } from "@vue/composition-api"; 213 | 214 | export default { 215 | setup() { 216 | const id = ref(2); 217 | const computedUrl = computed( 218 | () => `https://swapi.co/api/starships/${id.value}/` 219 | ); 220 | const headers = { Accept: "application/json" }; 221 | return { 222 | id, 223 | ...useFetch(computedUrl, { headers }) 224 | }; 225 | } 226 | }; 227 | ``` 228 | 229 | Here, the `id` is made reactive. The `url` is also reactive, using the `computed` function that recomputes whenever any 230 | of the reactive values is changed. We return both the `id` and all of the results of `useFetch`. Now we can for instance 231 | bind the reactive `id` with `v-model` to an input field. Whenever the input field changes, it will cause the fetch to be 232 | retried, aborting the current fetch if unresolved. 233 | 234 | ## Full examples 235 | 236 | ### `useAsync` and `Promise` example 237 | 238 | ```html 239 | 247 | 248 | 263 | ``` 264 | 265 | ### `useAsync` and `fetch` example 266 | 267 | ```html 268 | 275 | 276 | 295 | ``` 296 | 297 | ### `useFetch` example 298 | 299 | ```html 300 | 307 | 308 | 320 | ``` 321 | 322 | ### `useAsync` example with wrapped values 323 | 324 | ```html 325 | 335 | 336 | 367 | ``` 368 | 369 | ### `useFetch` example with wrapped values 370 | 371 | ```html 372 | 386 | 387 | 405 | ``` 406 | 407 | See the [examples](../examples) folder for a demo project with all examples in JavaScript. 408 | See the [examples-ts](../examples-ts) folder for a demo project with all examples in TypeScript. 409 | -------------------------------------------------------------------------------- /lib/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | modules: false, 7 | corejs: 3, 8 | useBuiltIns: "usage", 9 | }, 10 | ], 11 | "@babel/typescript", 12 | ], 13 | env: { 14 | test: { 15 | presets: [ 16 | [ 17 | "@babel/preset-env", 18 | { 19 | targets: { 20 | node: "current", 21 | }, 22 | }, 23 | ], 24 | "@babel/typescript", 25 | ], 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /lib/img/vue-async-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertBrand/vue-async-function/7aa2312cda5e6201199db1688266e947ac7aee47/lib/img/vue-async-function.png -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-async-function", 3 | "version": "3.1.2", 4 | "description": "Vue.js async function helper", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/AlbertBrand/vue-async-function.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/AlbertBrand/vue-async-function/issues" 12 | }, 13 | "homepage": "https://github.com/AlbertBrand/vue-async-function#readme", 14 | "author": "Albert Brand ", 15 | "keywords": [ 16 | "vuejs", 17 | "vue", 18 | "async", 19 | "function", 20 | "javascript" 21 | ], 22 | "main": "dist/index.js", 23 | "scripts": { 24 | "type-check": "tsc --noEmit", 25 | "build": "npm run build:types && npm run build:js", 26 | "build:types": "tsc --emitDeclarationOnly", 27 | "build:js": "babel src --out-dir dist --extensions \".ts\" --source-maps", 28 | "check-git": "[[ -z $(git status --porcelain) ]] || (echo 'Repo not clean'; false)", 29 | "preversion": "npm run check-git && npm run test:lint && npm test", 30 | "version": "npm run build && npm publish", 31 | "postversion": "VERSION=$(node -p \"require('./package.json').version\") && git add -A && git commit -m $VERSION && git tag v$VERSION -m '' && git push --follow-tags", 32 | "lint": "eslint --fix src/** tests/** && prettier .*.js package.json tsconfig.json --write --loglevel warn", 33 | "test": "jest", 34 | "test:lint": "eslint --max-warnings 0 src/** tests/** && prettier .*.js package.json tsconfig.json --check", 35 | "test:cov": "jest --coverage" 36 | }, 37 | "peerDependencies": { 38 | "core-js": "^3.6.4", 39 | "vue": "^2.6.11", 40 | "@vue/composition-api": "^0.5.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "^7.8.4", 44 | "@babel/core": "^7.9.0", 45 | "@babel/preset-env": "^7.9.0", 46 | "@babel/preset-typescript": "^7.9.0", 47 | "@babel/runtime-corejs3": "^7.9.2", 48 | "@types/jest": "^25.1.4", 49 | "@vue/composition-api": "^0.5.0", 50 | "@vue/test-utils": "^1.0.0-beta.32", 51 | "babel-eslint": "^10.1.0", 52 | "babel-jest": "^25.2.4", 53 | "eslint": "^6.8.0", 54 | "eslint-config-prettier": "^6.10.1", 55 | "eslint-plugin-prettier": "^3.1.2", 56 | "flush-promises": "^1.0.2", 57 | "jest": "^25.2.4", 58 | "prettier": "^2.0.2", 59 | "typescript": "^3.8.3", 60 | "vue": "^2.6.11", 61 | "vue-template-compiler": "^2.6.11" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, isRef, watch, onBeforeUnmount } from "@vue/composition-api"; 2 | 3 | type AsyncFunction = (params: P, signal: AbortSignal) => Promise; 4 | 5 | interface AsyncFunctionReturn { 6 | isLoading: Ref; 7 | error: Ref; 8 | data: Ref; 9 | abort: () => void; 10 | retry: () => void; 11 | } 12 | 13 | /** 14 | * Async helper function that returns three reactive values: 15 | * * `isLoading`, a boolean that is true during pending state; 16 | * * `data`, contains the resolved value in the fulfilled state; and 17 | * * `error`, contains the exception in the rejected state. 18 | * 19 | * It returns the following functions as well: 20 | * * `abort`, that aborts the current promise 21 | * * `retry`, that retries the original promise 22 | * 23 | * @param promiseFn (optionally ref to) function that returns a Promise. 24 | * @param params (optionally ref to) parameters passed as first argument to the promise function. 25 | * @returns Object literal containing `isLoading`, `error` and `data` value wrappers and `abort` and `retry` 26 | * functions. 27 | */ 28 | export function useAsync( 29 | promiseFn: AsyncFunction | Ref>, 30 | params?: P | Ref

31 | ): AsyncFunctionReturn { 32 | // always wrap arguments 33 | const wrapPromiseFn = isRef>(promiseFn) 34 | ? promiseFn 35 | : ref>(promiseFn); 36 | const wrapParams: Ref

= isRef

(params) ? params : ref(params); 37 | 38 | // create empty return values 39 | const isLoading = ref(false); 40 | const error = ref(); 41 | const data = ref(); 42 | 43 | // abort controller 44 | let controller: AbortController | undefined; 45 | 46 | function abort() { 47 | isLoading.value = false; 48 | if (controller !== undefined) { 49 | controller.abort(); 50 | controller = undefined; 51 | } 52 | } 53 | 54 | function retry() { 55 | // unwrap the original promise as it is optionally wrapped 56 | const origPromiseFn = wrapPromiseFn.value; 57 | // create a new promise and trigger watch 58 | wrapPromiseFn.value = async (params, signal) => 59 | origPromiseFn(params, signal); 60 | } 61 | 62 | // watch for change in arguments, which triggers immediately initially 63 | const watched: [typeof wrapPromiseFn, typeof wrapParams] = [ 64 | wrapPromiseFn, 65 | wrapParams, 66 | ]; 67 | watch(watched, async ([newPromiseFn, newParams]) => { 68 | try { 69 | abort(); 70 | isLoading.value = true; 71 | controller = new AbortController(); 72 | const result = await newPromiseFn(newParams, controller.signal); 73 | error.value = undefined; 74 | data.value = result; 75 | } catch (e) { 76 | error.value = e; 77 | data.value = undefined; 78 | } finally { 79 | isLoading.value = false; 80 | } 81 | }); 82 | 83 | onBeforeUnmount(abort); 84 | 85 | return { 86 | isLoading, 87 | error, 88 | data, 89 | abort, 90 | retry, 91 | }; 92 | } 93 | 94 | /** 95 | * Fetch helper function that accepts the same arguments as `fetch` and returns the same values as `useAsync`. 96 | * If the `Accept` header is set to `application/json` in the `requestInit` object, the response will be parsed as JSON, 97 | * else text. 98 | * 99 | * @param requestInfo (optionally ref to) URL or request object. 100 | * @param requestInit (optionally ref to) init parameters for the request. 101 | * @returns Object literal containing same return values as `useAsync`. 102 | */ 103 | export function useFetch( 104 | requestInfo: RequestInfo | Ref, 105 | requestInit: RequestInit | Ref = {} 106 | ): AsyncFunctionReturn { 107 | // always wrap arguments 108 | const wrapReqInfo = isRef(requestInfo) 109 | ? requestInfo 110 | : ref(requestInfo); 111 | const wrapReqInit = isRef(requestInit) 112 | ? requestInit 113 | : ref(requestInit); 114 | 115 | async function doFetch(params: undefined, signal: AbortSignal) { 116 | const requestInit = wrapReqInit.value; 117 | const res = await fetch(wrapReqInfo.value, { 118 | ...requestInit, 119 | signal, 120 | }); 121 | if (!res.ok) { 122 | throw res; 123 | } 124 | 125 | // TODO figure out how to use typed headers 126 | const headers: any = requestInit.headers; 127 | if (headers && headers.Accept === "application/json") { 128 | return res.json(); 129 | } 130 | return res.text(); 131 | } 132 | 133 | // wrap original fetch function in value 134 | const wrapPromiseFn = ref>(doFetch); 135 | 136 | // watch for change in arguments, which triggers immediately initially 137 | watch([wrapReqInfo, wrapReqInit], async () => { 138 | // create a new promise and trigger watch 139 | wrapPromiseFn.value = async (params, signal) => doFetch(params, signal); 140 | }); 141 | 142 | return useAsync(wrapPromiseFn); 143 | } 144 | -------------------------------------------------------------------------------- /lib/tests/useAsync.spec.js: -------------------------------------------------------------------------------- 1 | import { useAsync } from "../src"; 2 | import { shallowMount } from "@vue/test-utils"; 3 | import flushPromises from "flush-promises"; 4 | 5 | // setup vue 6 | import Vue from "vue"; 7 | import VueCompositionApi, { ref } from "@vue/composition-api"; 8 | Vue.use(VueCompositionApi); 9 | 10 | // component helper 11 | function createComponentWithUseAsync(promiseFn, params) { 12 | return { 13 | setup() { 14 | return useAsync(promiseFn, params); 15 | }, 16 | render: (h) => h(), 17 | }; 18 | } 19 | 20 | describe("useAsync", () => { 21 | it("returns initial values", () => { 22 | const promiseFn = async () => {}; 23 | const Component = createComponentWithUseAsync(promiseFn); 24 | 25 | const wrapper = shallowMount(Component); 26 | 27 | expect(wrapper.vm.isLoading).toBe(true); 28 | expect(wrapper.vm.error).toBeUndefined(); 29 | expect(wrapper.vm.data).toBeUndefined(); 30 | expect(wrapper.vm.retry).toBeDefined(); 31 | expect(wrapper.vm.abort).toBeDefined(); 32 | }); 33 | 34 | it("updates reactive values when promise resolves", async () => { 35 | const promiseFn = () => Promise.resolve("done"); 36 | const Component = createComponentWithUseAsync(promiseFn); 37 | 38 | const wrapper = shallowMount(Component); 39 | await wrapper.vm.$nextTick(); 40 | 41 | expect(wrapper.vm.isLoading).toBe(false); 42 | expect(wrapper.vm.error).toBeUndefined(); 43 | expect(wrapper.vm.data).toBe("done"); 44 | }); 45 | 46 | it("updates reactive values when promise rejects", async () => { 47 | const promiseFn = () => Promise.reject("error"); 48 | const Component = createComponentWithUseAsync(promiseFn); 49 | 50 | const wrapper = shallowMount(Component); 51 | await wrapper.vm.$nextTick(); 52 | 53 | expect(wrapper.vm.isLoading).toBe(false); 54 | expect(wrapper.vm.error).toBe("error"); 55 | expect(wrapper.vm.data).toBeUndefined(); 56 | }); 57 | 58 | it("retries original promise when retry is called", async () => { 59 | let fail = true; 60 | const promiseFn = jest.fn(() => 61 | fail ? Promise.reject("error") : Promise.resolve("done") 62 | ); 63 | const Component = createComponentWithUseAsync(promiseFn); 64 | const wrapper = shallowMount(Component); 65 | await wrapper.vm.$nextTick(); 66 | expect(wrapper.vm.isLoading).toBe(false); 67 | expect(wrapper.vm.error).toBe("error"); 68 | expect(wrapper.vm.data).toBeUndefined(); 69 | expect(promiseFn).toBeCalledTimes(1); 70 | 71 | fail = false; 72 | wrapper.vm.retry(); 73 | await wrapper.vm.$nextTick(); 74 | 75 | expect(wrapper.vm.isLoading).toBe(true); 76 | expect(wrapper.vm.error).toBe("error"); 77 | expect(wrapper.vm.data).toBeUndefined(); 78 | expect(promiseFn).toBeCalledTimes(2); 79 | 80 | await flushPromises(); 81 | expect(wrapper.vm.isLoading).toBe(false); 82 | expect(wrapper.vm.error).toBeUndefined(); 83 | expect(wrapper.vm.data).toBe("done"); 84 | }); 85 | 86 | it("sends abort signal to promise when abort is called", () => { 87 | let aborted = false; 88 | const promiseFn = async (params, signal) => { 89 | signal.addEventListener("abort", () => { 90 | aborted = true; 91 | }); 92 | }; 93 | const Component = createComponentWithUseAsync(promiseFn); 94 | const wrapper = shallowMount(Component); 95 | expect(wrapper.vm.isLoading).toBe(true); 96 | 97 | wrapper.vm.abort(); 98 | 99 | expect(wrapper.vm.isLoading).toBe(false); 100 | expect(wrapper.vm.error).toBeUndefined(); 101 | expect(wrapper.vm.data).toBeUndefined(); 102 | expect(aborted).toBe(true); 103 | }); 104 | 105 | it("aborts promise when component is destroyed", async () => { 106 | let aborted = false; 107 | const promiseFn = async (params, signal) => { 108 | signal.addEventListener("abort", () => { 109 | aborted = true; 110 | }); 111 | }; 112 | const Component = createComponentWithUseAsync(promiseFn); 113 | const wrapper = shallowMount(Component); 114 | 115 | wrapper.destroy(); 116 | 117 | expect(aborted).toBe(true); 118 | }); 119 | 120 | it("calls promiseFn with provided params argument", () => { 121 | const promiseFn = jest.fn(async () => {}); 122 | const params = {}; 123 | const Component = createComponentWithUseAsync(promiseFn, params); 124 | 125 | shallowMount(Component); 126 | 127 | expect(promiseFn).toBeCalledWith(params, expect.any(Object)); 128 | }); 129 | 130 | it("accepts value wrapped arguments", async () => { 131 | const promiseFn = ref(async ({ msg }) => msg); 132 | const params = ref({ msg: "done" }); 133 | const Component = createComponentWithUseAsync(promiseFn, params); 134 | 135 | const wrapper = shallowMount(Component); 136 | await wrapper.vm.$nextTick(); 137 | 138 | expect(wrapper.vm.isLoading).toBe(false); 139 | expect(wrapper.vm.error).toBeUndefined(); 140 | expect(wrapper.vm.data).toBe("done"); 141 | }); 142 | 143 | it("retries original promise when value wrapped promiseFn is changed", async () => { 144 | const promiseFn = async () => "done"; 145 | const wrapPromiseFn = ref(promiseFn); 146 | const Component = createComponentWithUseAsync(wrapPromiseFn); 147 | const wrapper = shallowMount(Component); 148 | await wrapper.vm.$nextTick(); 149 | expect(wrapper.vm.isLoading).toBe(false); 150 | expect(wrapper.vm.error).toBeUndefined(); 151 | expect(wrapper.vm.data).toBe("done"); 152 | 153 | let resolvePromise = () => {}; 154 | const newPromiseFn = () => 155 | new Promise((resolve) => { 156 | resolvePromise = () => resolve("done again"); 157 | }); 158 | 159 | wrapPromiseFn.value = newPromiseFn; 160 | await wrapper.vm.$nextTick(); 161 | 162 | expect(wrapper.vm.isLoading).toBe(true); 163 | expect(wrapper.vm.error).toBeUndefined(); 164 | expect(wrapper.vm.data).toBe("done"); 165 | 166 | resolvePromise(); 167 | await wrapper.vm.$nextTick(); 168 | expect(wrapper.vm.isLoading).toBe(false); 169 | expect(wrapper.vm.error).toBeUndefined(); 170 | expect(wrapper.vm.data).toBe("done again"); 171 | }); 172 | 173 | it("retries original promise within wrapped value when retry is called", async () => { 174 | const promiseFn = jest.fn(async () => "done"); 175 | const wrapPromiseFn = jest.fn(promiseFn); 176 | const Component = createComponentWithUseAsync(wrapPromiseFn); 177 | const wrapper = shallowMount(Component); 178 | expect(promiseFn).toBeCalledTimes(1); 179 | 180 | wrapper.vm.retry(); 181 | await wrapper.vm.$nextTick(); 182 | 183 | expect(promiseFn).toBeCalledTimes(2); 184 | }); 185 | 186 | it("resets error state when resolve directly follows reject", async () => { 187 | let failReject, successResolve; 188 | const failPromiseFn = () => 189 | new Promise((resolve, reject) => { 190 | failReject = () => reject("error"); 191 | }); 192 | const successPromiseFn = () => 193 | new Promise((resolve) => { 194 | successResolve = () => resolve("success"); 195 | }); 196 | const wrapPromiseFn = ref(failPromiseFn); 197 | const Component = createComponentWithUseAsync(wrapPromiseFn); 198 | 199 | const wrapper = shallowMount(Component); 200 | wrapPromiseFn.value = successPromiseFn; 201 | await wrapper.vm.$nextTick(); 202 | failReject(); 203 | successResolve(); 204 | 205 | await wrapper.vm.$nextTick(); 206 | expect(wrapper.vm.isLoading).toBe(false); 207 | expect(wrapper.vm.error).toBeUndefined(); 208 | expect(wrapper.vm.data).toBe("success"); 209 | }); 210 | 211 | it("sets mutually exclusive data or error", async () => { 212 | const promiseFn = () => Promise.resolve("done"); 213 | const wrapPromiseFn = ref(promiseFn); 214 | const Component = createComponentWithUseAsync(wrapPromiseFn); 215 | 216 | const wrapper = shallowMount(Component); 217 | await wrapper.vm.$nextTick(); 218 | 219 | expect(wrapper.vm.isLoading).toBe(false); 220 | expect(wrapper.vm.error).toBeUndefined(); 221 | expect(wrapper.vm.data).toBe("done"); 222 | 223 | wrapPromiseFn.value = () => Promise.reject("error"); 224 | await flushPromises(); 225 | 226 | expect(wrapper.vm.isLoading).toBe(false); 227 | expect(wrapper.vm.error).toBe("error"); 228 | expect(wrapper.vm.data).toBeUndefined(); 229 | 230 | wrapPromiseFn.value = () => Promise.resolve("done"); 231 | await flushPromises(); 232 | 233 | expect(wrapper.vm.isLoading).toBe(false); 234 | expect(wrapper.vm.error).toBeUndefined(); 235 | expect(wrapper.vm.data).toBe("done"); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /lib/tests/useFetch.spec.js: -------------------------------------------------------------------------------- 1 | import { useFetch } from "../src"; 2 | import { shallowMount } from "@vue/test-utils"; 3 | import flushPromises from "flush-promises"; 4 | 5 | // setup vue 6 | import Vue from "vue"; 7 | import VueCompositionApi, { ref } from "@vue/composition-api"; 8 | Vue.use(VueCompositionApi); 9 | 10 | // component helper 11 | function createComponentWithUseFetch(requestInfo, requestInit) { 12 | return { 13 | setup() { 14 | return useFetch(requestInfo, requestInit); 15 | }, 16 | render: (h) => h(), 17 | }; 18 | } 19 | 20 | describe("useFetch", () => { 21 | const jsonResult = { success: true }; 22 | const textResult = "success"; 23 | const response = { 24 | ok: true, 25 | json: jest.fn(async () => jsonResult), 26 | text: jest.fn(async () => textResult), 27 | }; 28 | const mockFetch = jest.fn(async () => response); 29 | 30 | beforeEach(() => { 31 | global.fetch = mockFetch; 32 | }); 33 | 34 | afterEach(() => { 35 | global.fetch.mockClear(); 36 | delete global.fetch; 37 | }); 38 | 39 | it("calls fetch with requestInfo and requestInit arguments including signal, returns values", async () => { 40 | const requestInfo = "http://some-url.local"; 41 | const requestInit = { headers: { Accept: "application/json" } }; 42 | const Component = createComponentWithUseFetch(requestInfo, requestInit); 43 | 44 | const wrapper = shallowMount(Component); 45 | await wrapper.vm.$nextTick(); 46 | 47 | expect(wrapper.vm.isLoading).toBe(true); 48 | expect(wrapper.vm.error).toBeUndefined(); 49 | expect(wrapper.vm.data).toBeUndefined(); 50 | expect(wrapper.vm.retry).toBeDefined(); 51 | expect(wrapper.vm.abort).toBeDefined(); 52 | expect(fetch).toBeCalledWith( 53 | requestInfo, 54 | expect.objectContaining(requestInit) 55 | ); 56 | expect(fetch.mock.calls[0][1].signal).toBeDefined(); 57 | 58 | await flushPromises(); 59 | expect(wrapper.vm.isLoading).toBe(false); 60 | expect(wrapper.vm.error).toBeUndefined(); 61 | expect(wrapper.vm.data).toBe(jsonResult); 62 | }); 63 | 64 | it("resolves to text response when no json header is set", async () => { 65 | const Component = createComponentWithUseFetch(""); 66 | 67 | const wrapper = shallowMount(Component); 68 | await wrapper.vm.$nextTick(); 69 | expect(wrapper.vm.isLoading).toBe(true); 70 | 71 | await flushPromises(); 72 | expect(wrapper.vm.isLoading).toBe(false); 73 | expect(wrapper.vm.error).toBeUndefined(); 74 | expect(wrapper.vm.data).toBe(textResult); 75 | }); 76 | 77 | it("rejects with bad response when response is not ok", async () => { 78 | const Component = createComponentWithUseFetch(""); 79 | const failedResponse = { 80 | ok: false, 81 | }; 82 | mockFetch.mockResolvedValueOnce(failedResponse); 83 | 84 | const wrapper = shallowMount(Component); 85 | expect(wrapper.vm.isLoading).toBe(true); 86 | 87 | await flushPromises(); 88 | expect(wrapper.vm.isLoading).toBe(false); 89 | expect(wrapper.vm.error).toEqual(failedResponse); 90 | expect(wrapper.vm.data).toBeUndefined(); 91 | }); 92 | 93 | it("accepts value wrapped arguments", async () => { 94 | const requestInfo = "http://some-url.local"; 95 | const requestInit = { headers: { Accept: "application/json" } }; 96 | const Component = createComponentWithUseFetch( 97 | ref(requestInfo), 98 | ref(requestInit) 99 | ); 100 | 101 | const wrapper = shallowMount(Component); 102 | await wrapper.vm.$nextTick(); 103 | 104 | expect(fetch).toBeCalledWith( 105 | requestInfo, 106 | expect.objectContaining(requestInit) 107 | ); 108 | }); 109 | 110 | it("retries original promise when requestInfo argument changes", async () => { 111 | const requestInfo = "http://some-url.local"; 112 | const wrapRequestInfo = ref(requestInfo); 113 | const Component = createComponentWithUseFetch(wrapRequestInfo); 114 | const wrapper = shallowMount(Component); 115 | await flushPromises(); 116 | expect(wrapper.vm.isLoading).toBe(false); 117 | expect(wrapper.vm.error).toBeUndefined(); 118 | expect(wrapper.vm.data).toBe(textResult); 119 | expect(fetch).toBeCalledWith(requestInfo, expect.anything()); 120 | 121 | const newTextResult = "success 2"; 122 | response.text.mockResolvedValueOnce(newTextResult); 123 | const newRequestInfo = "http://some-other-url.local"; 124 | wrapRequestInfo.value = newRequestInfo; 125 | await wrapper.vm.$nextTick(); 126 | await wrapper.vm.$nextTick(); 127 | expect(wrapper.vm.isLoading).toBe(true); 128 | expect(wrapper.vm.error).toBeUndefined(); 129 | expect(wrapper.vm.data).toBe(textResult); 130 | expect(fetch).toBeCalledWith(newRequestInfo, expect.anything()); 131 | 132 | await flushPromises(); 133 | expect(wrapper.vm.isLoading).toBe(false); 134 | expect(wrapper.vm.error).toBeUndefined(); 135 | expect(wrapper.vm.data).toBe(newTextResult); 136 | }); 137 | 138 | it("retries original promise when requestInit argument changes", async () => { 139 | const requestInfo = "http://some-url.local"; 140 | const requestInit = { headers: { Accept: "application/json" } }; 141 | const wrapRequestInit = ref(requestInit); 142 | const Component = createComponentWithUseFetch(requestInfo, wrapRequestInit); 143 | const wrapper = shallowMount(Component); 144 | await flushPromises(); 145 | expect(wrapper.vm.isLoading).toBe(false); 146 | expect(wrapper.vm.error).toBeUndefined(); 147 | expect(wrapper.vm.data).toBe(jsonResult); 148 | 149 | const newRequestInit = { headers: { Accept: "text/plain" } }; 150 | wrapRequestInit.value = newRequestInit; 151 | await wrapper.vm.$nextTick(); 152 | await wrapper.vm.$nextTick(); 153 | expect(wrapper.vm.isLoading).toBe(true); 154 | expect(wrapper.vm.error).toBeUndefined(); 155 | expect(wrapper.vm.data).toBe(jsonResult); 156 | expect(fetch).toBeCalledWith( 157 | requestInfo, 158 | expect.objectContaining(requestInit) 159 | ); 160 | 161 | await flushPromises(); 162 | expect(wrapper.vm.isLoading).toBe(false); 163 | expect(wrapper.vm.error).toBeUndefined(); 164 | expect(wrapper.vm.data).toBe(textResult); 165 | expect(fetch).toBeCalledWith(requestInfo, expect.anything()); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "strict": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "outDir": "dist" 10 | }, 11 | "include": ["src/**/*.ts", "tests/**/*.ts"], 12 | "exclude": ["node_modules"] 13 | } 14 | --------------------------------------------------------------------------------