Skip to content

Commit

Permalink
Merge pull request #21 from microcipcip/feature/useFetch
Browse files Browse the repository at this point in the history
Feature/use fetch
  • Loading branch information
microcipcip authored Apr 26, 2020
2 parents 8f77a5a + e544384 commit f6de30e
Show file tree
Hide file tree
Showing 12 changed files with 1,404 additions and 635 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ Vue.use(VueCompositionAPI)
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usebeforeunload--demo)
- [`useCookie`](./src/functions/useCookie/stories/useCookie.md) — provides way to read, update and delete a cookie.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usecookie--demo)
- [`useFetch`](./src/functions/useFetch/stories/useFetch.md) — provides a way to fetch resources asynchronously across the network.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-usefetch--demo)
- [`useLocalStorage`](./src/functions/useLocalStorage/stories/useLocalStorage.md) — provides way to read, update and delete a localStorage key.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/side-effects-uselocalstorage--demo)
- [`useSessionStorage`](./src/functions/useSessionStorage/stories/useSessionStorage.md) — provides way to read, update and delete a sessionStorage key.
Expand Down
1,606 changes: 975 additions & 631 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@
"@commitlint/cli": "^7.1.2",
"@commitlint/config-conventional": "^7.1.2",
"@fortawesome/fontawesome-free": "^5.12.0",
"@storybook/addon-notes": "^5.3.13",
"@storybook/addon-viewport": "^5.3.13",
"@storybook/theming": "^5.3.13",
"@storybook/vue": "^5.3.13",
"@storybook/addon-notes": "^5.3.18",
"@storybook/addon-viewport": "^5.3.18",
"@storybook/theming": "^5.3.18",
"@storybook/vue": "^5.3.18",
"@types/jest": "^23.3.2",
"@types/node": "^10.11.0",
"@types/throttle-debounce": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/functions/useFetch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useFetch'
127 changes: 127 additions & 0 deletions src/functions/useFetch/stories/UseFetchDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<template>
<div>
<div class="actions">
<button
class="button is-primary"
@click="startWithSuccess"
:disabled="isLoading"
v-text="isInit ? 'Fetch again!' : 'Fetch'"
/>
<button
class="button is-info"
@click="startWithFailed"
:disabled="isLoading"
v-text="`Fetch with failure`"
/>
<button
class="button is-danger"
@click="stop"
:disabled="!isLoading"
v-text="`Abort fetch`"
/>
</div>

<!-- isAborted -->
<use-fetch-demo-table status="Aborted" v-if="isAborted">
Resource fetch aborted with status code
<strong>{{ status }}</strong> and message
<strong>{{ statusText }}</strong>
</use-fetch-demo-table>

<!-- isFailed -->
<use-fetch-demo-table status="Failed" v-else-if="isFailed">
Resource fetch failed with status code
<strong>{{ status }}</strong> and message
<strong>{{ statusText }}</strong>
</use-fetch-demo-table>

<!-- isSuccess -->
<div v-else>
<!-- !isInit -->
<use-fetch-demo-table status="Not initialized" v-if="!isInit">
Click "Fetch" to initialize the request.
</use-fetch-demo-table>

<!-- isLoading -->
<use-fetch-demo-table status="Loading" v-else-if="isLoading">
Resource is being fetched...
</use-fetch-demo-table>

<!-- Fetched -->
<use-fetch-demo-table status="Success" v-else-if="data">
<p>
Resource fetched successfully with status code
<strong>{{ status }}</strong> and message
<strong>{{ statusText }}</strong>
</p>
<img class="img" :src="data.message" alt="" />
</use-fetch-demo-table>
</div>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import { ref } from '@src/api'
import { useFetch } from '@src/vue-use-kit'
import UseFetchDemoTable from './UseFetchDemoTable.vue'
export default Vue.extend({
name: 'UseFetchDemo',
components: { UseFetchDemoTable },
setup() {
const isInit = ref(false)
const delayUrl = 'http://deelay.me/2000'
const randomDogUrl = `${delayUrl}/https://dog.ceo/api/breeds/image/random`
const url = ref(randomDogUrl)
const {
data,
status,
statusText,
isLoading,
isFailed,
isAborted,
start,
stop
} = useFetch(url, {}, false)
const startWithSuccess = () => {
isInit.value = true
url.value = randomDogUrl
start()
}
const startWithFailed = () => {
isInit.value = true
url.value = `${delayUrl}/https://dog.ceo`
start()
}
return {
data,
status,
statusText,
isInit,
isLoading,
isFailed,
isAborted,
startWithSuccess,
startWithFailed,
stop
}
}
})
</script>

<style scoped>
.actions {
padding-bottom: 20px;
}
.img {
display: block;
margin: 20px 0 0;
max-width: 300px;
border-radius: 3px;
}
</style>
29 changes: 29 additions & 0 deletions src/functions/useFetch/stories/UseFetchDemoTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr>
<td v-text="status" />
<td width="100%"><slot></slot></td>
</tr>
</tbody>
</table>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'UseFetchDemoTable',
props: {
status: String
}
})
</script>

<style scoped></style>
71 changes: 71 additions & 0 deletions src/functions/useFetch/stories/useFetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# useFetch

Vue function that fetch resources asynchronously across the network.

## Reference

```typescript
type TUseFetchUrl = RequestInfo | Ref<RequestInfo>
```
```typescript
function useFetch(
url: TUseFetchUrl,
options?: RequestInit,
runOnMount?: boolean
): {
data: Ref<any>
status: Ref<number | null>
statusText: Ref<string | null>
isLoading: Ref<boolean>
isFailed: Ref<boolean>
isAborted: Ref<boolean>
start: () => Promise<void>
stop: () => void
}
```

### Parameters

- `url: TUseFetchUrl` the fetch url value, can be type string or type `RequestInfo`.
- `options: RequestInit` the fetch url options.
- `runOnMount: boolean` whether to fetch on mount, `true` by default.

### Returns

- `data: Ref<any>` the response data, has to be of JSON type otherwise will return an error
- `status: Ref<number | null>` the status code of the response
- `statusText: Ref<string | null>` the status text of the response
- `isLoading: Ref<boolean>` whether fetch request is loading or not
- `isFailed: Ref<boolean>` whether fetch request has failed or not
- `isAborted: Ref<boolean>` whether fetch request has been aborted or not
- `start: Function` the function used for starting fetch request
- `stop: Function` the function used for aborting fetch request

## Usage

```html
<template>
<div>
<div v-if="isFailed">Failed!</div>
<div v-else-if="isLoading">Loading...</div>
<div v-else-if="data">
<img :src="data.message" alt="" />
</div>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import { useFetch } from 'vue-use-kit'
export default Vue.extend({
name: 'UseFetchDemo',
setup() {
const url = 'https://dog.ceo/api/breeds/image/random'
const { data, isLoading, isFailed } = useFetch(url)
return { data, isLoading, isFailed }
}
})
</script>
```
28 changes: 28 additions & 0 deletions src/functions/useFetch/stories/useFetch.story.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { storiesOf } from '@storybook/vue'
import path from 'path'
import StoryTitle from '@src/helpers/StoryTitle.vue'
import UseFetchDemo from './UseFetchDemo.vue'

const functionName = 'useFetch'
const functionPath = path.resolve(__dirname, '..')
const notes = require(`./${functionName}.md`).default

const basicDemo = () => ({
components: { StoryTitle, demo: UseFetchDemo },
template: `
<div class="container">
<story-title
function-path="${functionPath}"
source-name="${functionName}"
demo-name="UseFetchDemo.vue"
>
<template v-slot:title></template>
<template v-slot:intro></template>
</story-title>
<demo />
</div>`
})

storiesOf('side effects|useFetch', module)
.addParameters({ notes })
.add('Demo', basicDemo)
85 changes: 85 additions & 0 deletions src/functions/useFetch/useFetch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mount, flushPromises } from '@src/helpers/test'
import { useFetch } from '@src/vue-use-kit'

afterEach(() => {
jest.clearAllMocks()
})

const abortError = 'AbortError'

const mockFetch = ({
data = { text: 'Message here' },
header = 'abcd;application/json',
isAborted = false
} = {}) => {
;(window as any).fetch = () => {
if (isAborted) {
const err = new Error()
err.name = abortError
throw err
}

return {
headers: {
get: () => header
},
json: () => data
}
}
}

const testComponent = (url = '', opts: RequestInit = {}, onMount = true) => ({
template: `
<div>
<div id="isLoading" v-if="isLoading"></div>
<div id="isFailed" v-if="isFailed"></div>
<div id="isAborted" v-if="isAborted"></div>
<div id="data" v-if="data" v-text="data.text"></div>
<button id="start" @click="start"></button>
<button id="stop" @click="stop"></button>
</div>
`,
setup() {
const { data, isLoading, isFailed, isAborted, start, stop } = useFetch(
url,
opts,
onMount
)
return { data, isLoading, isFailed, isAborted, start, stop }
}
})

describe('useFetch', () => {
const url = 'http://test.com'

it('should show #isLoading in the correct order', async () => {
mockFetch()
const wrapper = mount(testComponent(url))
expect(wrapper.find('#isLoading').exists()).toBe(false)
await wrapper.vm.$nextTick()
expect(wrapper.find('#isLoading').exists()).toBe(true)
})

it('should show #data when provided', async () => {
const data = { text: 'Here is some data' }
mockFetch({ data })
const wrapper = mount(testComponent(url))
await flushPromises()
expect(wrapper.find('#data').text()).toBe(data.text)
})

it('should show #isFailed when the header is not of json type', async () => {
mockFetch({ header: 'brokenHeader' })
const wrapper = mount(testComponent(url))
await flushPromises()
expect(wrapper.find('#isFailed').exists()).toBe(true)
})

it('should show #isAborted when aborted is triggered', async () => {
mockFetch({ isAborted: true })
const wrapper = mount(testComponent(url))
await flushPromises()
expect(wrapper.find('#isFailed').exists()).toBe(true)
expect(wrapper.find('#isAborted').exists()).toBe(true)
})
})
Loading

0 comments on commit f6de30e

Please sign in to comment.