Skip to content

Commit f6de30e

Browse files
authored
Merge pull request #21 from microcipcip/feature/useFetch
Feature/use fetch
2 parents 8f77a5a + e544384 commit f6de30e

12 files changed

+1404
-635
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ Vue.use(VueCompositionAPI)
119119
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usebeforeunload--demo)
120120
- [`useCookie`](./src/functions/useCookie/stories/useCookie.md) — provides way to read, update and delete a cookie.
121121
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usecookie--demo)
122+
- [`useFetch`](./src/functions/useFetch/stories/useFetch.md) — provides a way to fetch resources asynchronously across the network.
123+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usefetch--demo)
122124
- [`useLocalStorage`](./src/functions/useLocalStorage/stories/useLocalStorage.md) — provides way to read, update and delete a localStorage key.
123125
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-uselocalstorage--demo)
124126
- [`useSessionStorage`](./src/functions/useSessionStorage/stories/useSessionStorage.md) — provides way to read, update and delete a sessionStorage key.

Diff for: package-lock.json

+975-631
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@
8989
"@commitlint/cli": "^7.1.2",
9090
"@commitlint/config-conventional": "^7.1.2",
9191
"@fortawesome/fontawesome-free": "^5.12.0",
92-
"@storybook/addon-notes": "^5.3.13",
93-
"@storybook/addon-viewport": "^5.3.13",
94-
"@storybook/theming": "^5.3.13",
95-
"@storybook/vue": "^5.3.13",
92+
"@storybook/addon-notes": "^5.3.18",
93+
"@storybook/addon-viewport": "^5.3.18",
94+
"@storybook/theming": "^5.3.18",
95+
"@storybook/vue": "^5.3.18",
9696
"@types/jest": "^23.3.2",
9797
"@types/node": "^10.11.0",
9898
"@types/throttle-debounce": "^2.1.0",

Diff for: src/functions/useFetch/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useFetch'

Diff for: src/functions/useFetch/stories/UseFetchDemo.vue

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<template>
2+
<div>
3+
<div class="actions">
4+
<button
5+
class="button is-primary"
6+
@click="startWithSuccess"
7+
:disabled="isLoading"
8+
v-text="isInit ? 'Fetch again!' : 'Fetch'"
9+
/>
10+
<button
11+
class="button is-info"
12+
@click="startWithFailed"
13+
:disabled="isLoading"
14+
v-text="`Fetch with failure`"
15+
/>
16+
<button
17+
class="button is-danger"
18+
@click="stop"
19+
:disabled="!isLoading"
20+
v-text="`Abort fetch`"
21+
/>
22+
</div>
23+
24+
<!-- isAborted -->
25+
<use-fetch-demo-table status="Aborted" v-if="isAborted">
26+
Resource fetch aborted with status code
27+
<strong>{{ status }}</strong> and message
28+
<strong>{{ statusText }}</strong>
29+
</use-fetch-demo-table>
30+
31+
<!-- isFailed -->
32+
<use-fetch-demo-table status="Failed" v-else-if="isFailed">
33+
Resource fetch failed with status code
34+
<strong>{{ status }}</strong> and message
35+
<strong>{{ statusText }}</strong>
36+
</use-fetch-demo-table>
37+
38+
<!-- isSuccess -->
39+
<div v-else>
40+
<!-- !isInit -->
41+
<use-fetch-demo-table status="Not initialized" v-if="!isInit">
42+
Click "Fetch" to initialize the request.
43+
</use-fetch-demo-table>
44+
45+
<!-- isLoading -->
46+
<use-fetch-demo-table status="Loading" v-else-if="isLoading">
47+
Resource is being fetched...
48+
</use-fetch-demo-table>
49+
50+
<!-- Fetched -->
51+
<use-fetch-demo-table status="Success" v-else-if="data">
52+
<p>
53+
Resource fetched successfully with status code
54+
<strong>{{ status }}</strong> and message
55+
<strong>{{ statusText }}</strong>
56+
</p>
57+
<img class="img" :src="data.message" alt="" />
58+
</use-fetch-demo-table>
59+
</div>
60+
</div>
61+
</template>
62+
63+
<script lang="ts">
64+
import Vue from 'vue'
65+
import { ref } from '@src/api'
66+
import { useFetch } from '@src/vue-use-kit'
67+
import UseFetchDemoTable from './UseFetchDemoTable.vue'
68+
69+
export default Vue.extend({
70+
name: 'UseFetchDemo',
71+
components: { UseFetchDemoTable },
72+
setup() {
73+
const isInit = ref(false)
74+
const delayUrl = 'http://deelay.me/2000'
75+
const randomDogUrl = `${delayUrl}/https://dog.ceo/api/breeds/image/random`
76+
const url = ref(randomDogUrl)
77+
const {
78+
data,
79+
status,
80+
statusText,
81+
isLoading,
82+
isFailed,
83+
isAborted,
84+
start,
85+
stop
86+
} = useFetch(url, {}, false)
87+
88+
const startWithSuccess = () => {
89+
isInit.value = true
90+
url.value = randomDogUrl
91+
start()
92+
}
93+
94+
const startWithFailed = () => {
95+
isInit.value = true
96+
url.value = `${delayUrl}/https://dog.ceo`
97+
start()
98+
}
99+
100+
return {
101+
data,
102+
status,
103+
statusText,
104+
isInit,
105+
isLoading,
106+
isFailed,
107+
isAborted,
108+
startWithSuccess,
109+
startWithFailed,
110+
stop
111+
}
112+
}
113+
})
114+
</script>
115+
116+
<style scoped>
117+
.actions {
118+
padding-bottom: 20px;
119+
}
120+
121+
.img {
122+
display: block;
123+
margin: 20px 0 0;
124+
max-width: 300px;
125+
border-radius: 3px;
126+
}
127+
</style>

Diff for: src/functions/useFetch/stories/UseFetchDemoTable.vue

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<table class="table is-fullwidth">
3+
<thead>
4+
<tr>
5+
<th>Status</th>
6+
<th>Message</th>
7+
</tr>
8+
</thead>
9+
<tbody>
10+
<tr>
11+
<td v-text="status" />
12+
<td width="100%"><slot></slot></td>
13+
</tr>
14+
</tbody>
15+
</table>
16+
</template>
17+
18+
<script lang="ts">
19+
import Vue from 'vue'
20+
21+
export default Vue.extend({
22+
name: 'UseFetchDemoTable',
23+
props: {
24+
status: String
25+
}
26+
})
27+
</script>
28+
29+
<style scoped></style>

Diff for: src/functions/useFetch/stories/useFetch.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# useFetch
2+
3+
Vue function that fetch resources asynchronously across the network.
4+
5+
## Reference
6+
7+
```typescript
8+
type TUseFetchUrl = RequestInfo | Ref<RequestInfo>
9+
```
10+
11+
```typescript
12+
function useFetch(
13+
url: TUseFetchUrl,
14+
options?: RequestInit,
15+
runOnMount?: boolean
16+
): {
17+
data: Ref<any>
18+
status: Ref<number | null>
19+
statusText: Ref<string | null>
20+
isLoading: Ref<boolean>
21+
isFailed: Ref<boolean>
22+
isAborted: Ref<boolean>
23+
start: () => Promise<void>
24+
stop: () => void
25+
}
26+
```
27+
28+
### Parameters
29+
30+
- `url: TUseFetchUrl` the fetch url value, can be type string or type `RequestInfo`.
31+
- `options: RequestInit` the fetch url options.
32+
- `runOnMount: boolean` whether to fetch on mount, `true` by default.
33+
34+
### Returns
35+
36+
- `data: Ref<any>` the response data, has to be of JSON type otherwise will return an error
37+
- `status: Ref<number | null>` the status code of the response
38+
- `statusText: Ref<string | null>` the status text of the response
39+
- `isLoading: Ref<boolean>` whether fetch request is loading or not
40+
- `isFailed: Ref<boolean>` whether fetch request has failed or not
41+
- `isAborted: Ref<boolean>` whether fetch request has been aborted or not
42+
- `start: Function` the function used for starting fetch request
43+
- `stop: Function` the function used for aborting fetch request
44+
45+
## Usage
46+
47+
```html
48+
<template>
49+
<div>
50+
<div v-if="isFailed">Failed!</div>
51+
<div v-else-if="isLoading">Loading...</div>
52+
<div v-else-if="data">
53+
<img :src="data.message" alt="" />
54+
</div>
55+
</div>
56+
</template>
57+
58+
<script lang="ts">
59+
import Vue from 'vue'
60+
import { useFetch } from 'vue-use-kit'
61+
62+
export default Vue.extend({
63+
name: 'UseFetchDemo',
64+
setup() {
65+
const url = 'https://dog.ceo/api/breeds/image/random'
66+
const { data, isLoading, isFailed } = useFetch(url)
67+
return { data, isLoading, isFailed }
68+
}
69+
})
70+
</script>
71+
```

Diff for: src/functions/useFetch/stories/useFetch.story.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import path from 'path'
3+
import StoryTitle from '@src/helpers/StoryTitle.vue'
4+
import UseFetchDemo from './UseFetchDemo.vue'
5+
6+
const functionName = 'useFetch'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseFetchDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseFetchDemo.vue"
18+
>
19+
<template v-slot:title></template>
20+
<template v-slot:intro></template>
21+
</story-title>
22+
<demo />
23+
</div>`
24+
})
25+
26+
storiesOf('side effects|useFetch', module)
27+
.addParameters({ notes })
28+
.add('Demo', basicDemo)

Diff for: src/functions/useFetch/useFetch.spec.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { mount, flushPromises } from '@src/helpers/test'
2+
import { useFetch } from '@src/vue-use-kit'
3+
4+
afterEach(() => {
5+
jest.clearAllMocks()
6+
})
7+
8+
const abortError = 'AbortError'
9+
10+
const mockFetch = ({
11+
data = { text: 'Message here' },
12+
header = 'abcd;application/json',
13+
isAborted = false
14+
} = {}) => {
15+
;(window as any).fetch = () => {
16+
if (isAborted) {
17+
const err = new Error()
18+
err.name = abortError
19+
throw err
20+
}
21+
22+
return {
23+
headers: {
24+
get: () => header
25+
},
26+
json: () => data
27+
}
28+
}
29+
}
30+
31+
const testComponent = (url = '', opts: RequestInit = {}, onMount = true) => ({
32+
template: `
33+
<div>
34+
<div id="isLoading" v-if="isLoading"></div>
35+
<div id="isFailed" v-if="isFailed"></div>
36+
<div id="isAborted" v-if="isAborted"></div>
37+
<div id="data" v-if="data" v-text="data.text"></div>
38+
<button id="start" @click="start"></button>
39+
<button id="stop" @click="stop"></button>
40+
</div>
41+
`,
42+
setup() {
43+
const { data, isLoading, isFailed, isAborted, start, stop } = useFetch(
44+
url,
45+
opts,
46+
onMount
47+
)
48+
return { data, isLoading, isFailed, isAborted, start, stop }
49+
}
50+
})
51+
52+
describe('useFetch', () => {
53+
const url = 'http://test.com'
54+
55+
it('should show #isLoading in the correct order', async () => {
56+
mockFetch()
57+
const wrapper = mount(testComponent(url))
58+
expect(wrapper.find('#isLoading').exists()).toBe(false)
59+
await wrapper.vm.$nextTick()
60+
expect(wrapper.find('#isLoading').exists()).toBe(true)
61+
})
62+
63+
it('should show #data when provided', async () => {
64+
const data = { text: 'Here is some data' }
65+
mockFetch({ data })
66+
const wrapper = mount(testComponent(url))
67+
await flushPromises()
68+
expect(wrapper.find('#data').text()).toBe(data.text)
69+
})
70+
71+
it('should show #isFailed when the header is not of json type', async () => {
72+
mockFetch({ header: 'brokenHeader' })
73+
const wrapper = mount(testComponent(url))
74+
await flushPromises()
75+
expect(wrapper.find('#isFailed').exists()).toBe(true)
76+
})
77+
78+
it('should show #isAborted when aborted is triggered', async () => {
79+
mockFetch({ isAborted: true })
80+
const wrapper = mount(testComponent(url))
81+
await flushPromises()
82+
expect(wrapper.find('#isFailed').exists()).toBe(true)
83+
expect(wrapper.find('#isAborted').exists()).toBe(true)
84+
})
85+
})

0 commit comments

Comments
 (0)