Skip to content

Commit 87f4bc0

Browse files
authored
Merge pull request #11 from microcipcip/feature/useBeforeUnload
Feature/use before unload
2 parents ad0a23d + ca1db28 commit 87f4bc0

File tree

23 files changed

+418
-309
lines changed

23 files changed

+418
-309
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ Vue.use(VueCompositionAPI);
102102
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-usetimeout--demo)
103103
- [`useTimeoutFn`](./src/components/useTimeoutFn/stories/useTimeoutFn.md) — calls function when timer is completed.
104104
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-usetimeoutfn--demo)
105+
- Side Effects
106+
- [`useBeforeUnload`](./src/components/useBeforeUnload/stories/useBeforeUnload.md) — shows browser alert when user try to reload or close the page.
107+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/ui-useclickaway--demo)
105108
- UI
106109
- [`useClickAway`](./src/components/useClickAway/stories/useClickAway.md) — triggers callback when user clicks outside target area.
107110
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/ui-useclickaway--demo)
@@ -113,7 +116,7 @@ Vue.use(VueCompositionAPI);
113116

114117
## Inspiration
115118

116-
- [react-use](https://github.com/streamich/react-use)
119+
- [react-use 👍](https://github.com/streamich/react-use)
117120
- [vue-hooks](https://github.com/u3u/vue-hooks)
118121
- [vue-use-web](https://github.com/logaretm/vue-use-web)
119122

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useBeforeUnload'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<template>
2+
<div class="form">
3+
<div class="field">
4+
<label class="label">Name</label>
5+
<div class="control">
6+
<input class="input" type="text" v-model="nameFld" />
7+
</div>
8+
</div>
9+
10+
<div class="field">
11+
<p>
12+
<a class="button is-primary" href="https://google.com">
13+
<span>Change page</span>
14+
<span class="icon is-small">
15+
<i class="fas fa-external-link-alt"></i>
16+
</span>
17+
</a>
18+
</p>
19+
</div>
20+
21+
<div class="field">
22+
<button class="button is-primary" @click="start" v-if="!isTracking">
23+
Start checking beforeUnload event
24+
</button>
25+
<button class="button is-danger" @click="stop" v-else>
26+
Stop checking beforeUnload event
27+
</button>
28+
</div>
29+
</div>
30+
</template>
31+
32+
<script lang="ts">
33+
import Vue from 'vue'
34+
import { ref, watch } from '@vue/composition-api'
35+
import { useBeforeUnload } from '@src/vue-use-kit'
36+
37+
export default Vue.extend({
38+
name: 'UseBeforeUnloadDemo',
39+
setup() {
40+
const isPageDirty = ref(false)
41+
const nameFld = ref('')
42+
43+
watch(nameFld, newVal => {
44+
isPageDirty.value = !!newVal
45+
})
46+
47+
const { isTracking, start, stop } = useBeforeUnload(isPageDirty)
48+
return { isTracking, start, stop, isPageDirty, nameFld }
49+
}
50+
})
51+
</script>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# useBeforeUnload
2+
3+
Vue function that shows browser alert when user try to reload or close the page.
4+
5+
## Reference
6+
7+
```typescript
8+
function useBeforeUnload(
9+
isPageDirty: Ref<boolean>,
10+
runOnMount?: boolean
11+
): {
12+
isTracking: Ref<boolean>
13+
start: () => void
14+
stop: () => void
15+
}
16+
```
17+
18+
### Parameters
19+
20+
- `isPageDirty: Ref<boolean>` when this value is `true` value, it will show the browser alert on page change
21+
- `runOnMount: boolean` whether to listen to the 'beforeunload' event on mount, `true` by default
22+
23+
### Returns
24+
25+
- `isTracking: Ref<boolean>` whether this function events are running or not
26+
- `start: Function` the function used to start tracking page change or reload
27+
- `stop: Function` the function used to stop tracking page change or reload
28+
29+
## Usage
30+
31+
```html
32+
<template>
33+
<div>
34+
<div>
35+
<a href="https://google.com">Change page</a>
36+
</div>
37+
<div>
38+
<button @click="start" v-if="!isTracking">
39+
Start tracking page change or reload
40+
</button>
41+
<button @click="stop" v-else>Stop tracking page change or reload</button>
42+
</div>
43+
</div>
44+
</template>
45+
46+
<script lang="ts">
47+
import Vue from 'vue'
48+
import { ref, watch } from '@vue/composition-api'
49+
import { useBeforeUnload } from 'vue-use-kit'
50+
51+
export default Vue.extend({
52+
name: 'UseBeforeUnloadDemo',
53+
setup() {
54+
const isPageDirty = ref(true)
55+
const { isTracking, start, stop } = useBeforeUnload(isPageDirty)
56+
return { isTracking, start, stop, isPageDirty }
57+
}
58+
})
59+
</script>
60+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import path from 'path'
3+
import StoryTitle from '@src/helpers/StoryTitle.vue'
4+
import UseBeforeUnloadDemo from './UseBeforeUnloadDemo.vue'
5+
6+
const functionName = 'useBeforeUnload'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseBeforeUnloadDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseBeforeUnloadDemo.vue"
18+
>
19+
<template v-slot:title></template>
20+
<template v-slot:intro>
21+
<p>
22+
<strong>Try typing some text in the field below</strong> then hit the 'Change page'
23+
button to see the alert message.
24+
</p>
25+
<p>
26+
You can also try to leave the field empty and hit again the 'Change page' button and you'll
27+
be able to change page without any confirmation message.
28+
</p>
29+
</template>
30+
</story-title>
31+
<demo />
32+
</div>`
33+
})
34+
35+
storiesOf('side effects|useBeforeUnload', module)
36+
.addParameters({ notes })
37+
.add('Demo', basicDemo)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
checkElementExistenceOnMount,
3+
checkOnMountAndUnmountEvents,
4+
checkOnStartEvents,
5+
checkOnStopEvents
6+
} from '@src/helpers/test'
7+
import { ref } from '@vue/composition-api'
8+
import { useBeforeUnload } from '@src/vue-use-kit'
9+
10+
afterEach(() => {
11+
jest.clearAllMocks()
12+
})
13+
14+
const testComponent = (onMount = true) => ({
15+
template: `
16+
<div>
17+
<div id="isTracking" v-if="isTracking"></div>
18+
<button id="start" @click="start"></button>
19+
<button id="stop" @click="stop"></button>
20+
</div>
21+
`,
22+
setup() {
23+
const isDirty = ref(false)
24+
const { isTracking, start, stop } = useBeforeUnload(isDirty, onMount)
25+
return { isTracking, start, stop }
26+
}
27+
})
28+
29+
describe('useBeforeUnload', () => {
30+
const events = ['beforeunload']
31+
32+
it('should add events on mounted and remove them on unmounted', async () => {
33+
await checkOnMountAndUnmountEvents(window, events, testComponent)
34+
})
35+
36+
it('should add events again when start is called', async () => {
37+
await checkOnStartEvents(window, events, testComponent)
38+
})
39+
40+
it('should remove events when stop is called', async () => {
41+
await checkOnStopEvents(window, events, testComponent)
42+
})
43+
44+
it('should show #isTracking when runOnMount is true', async () => {
45+
await checkElementExistenceOnMount(true, testComponent)
46+
})
47+
48+
it('should not show #isTracking when runOnMount is false', async () => {
49+
await checkElementExistenceOnMount(false, testComponent)
50+
})
51+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@vue/composition-api'
2+
3+
export function useBeforeUnload(isPageDirty: Ref<boolean>, runOnMount = true) {
4+
const isTracking = ref(false)
5+
6+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
7+
// Show alert message only when isPageDirty is true
8+
if (isPageDirty.value) e.preventDefault()
9+
}
10+
11+
const start = () => {
12+
if (isTracking.value) return
13+
window.addEventListener('beforeunload', handleBeforeUnload)
14+
isTracking.value = true
15+
}
16+
17+
const stop = () => {
18+
if (!isTracking.value) return
19+
window.removeEventListener('beforeunload', handleBeforeUnload)
20+
isTracking.value = false
21+
}
22+
23+
onMounted(() => runOnMount && start())
24+
onUnmounted(stop)
25+
26+
return { isTracking, start, stop }
27+
}

src/components/useClickAway/useClickAway.spec.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mount } from '@src/helpers/test'
1+
import { checkOnMountAndUnmountEvents, mount } from '@src/helpers/test'
22
import { ref } from '@vue/composition-api'
33
import { useClickAway } from '@src/vue-use-kit'
44

@@ -26,23 +26,9 @@ const testComponent = () => ({
2626
})
2727

2828
describe('useClickAway', () => {
29-
it('should call document.addEventListener', async () => {
30-
const addEventListenerSpy = jest.spyOn(document, 'addEventListener')
31-
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener')
32-
const wrapper = mount(testComponent())
33-
await wrapper.vm.$nextTick()
34-
expect(addEventListenerSpy).toHaveBeenCalled()
35-
expect(addEventListenerSpy).toBeCalledWith(
36-
'mousedown',
37-
expect.any(Function)
38-
)
29+
const events = ['mousedown', 'touchstart']
3930

40-
// Destroy instance to check if the remove event listener is being called
41-
wrapper.destroy()
42-
expect(removeEventListenerSpy).toHaveBeenCalled()
43-
expect(removeEventListenerSpy).toBeCalledWith(
44-
'mousedown',
45-
expect.any(Function)
46-
)
31+
it('should call addEventListener on mount and unmount', async () => {
32+
await checkOnMountAndUnmountEvents(document, events, testComponent)
4733
})
4834
})

src/components/useGeolocation/useGeolocation.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ const testComponent = () => ({
4444
})
4545

4646
describe('useGeolocation', () => {
47-
it('should call getCurrentPosition and watchPosition onMounted', () => {
47+
it('should call getCurrentPosition and watchPosition on mounted', () => {
4848
expect(getCurrentPosition).toHaveBeenCalledTimes(0)
4949
expect(watchPosition).toHaveBeenCalledTimes(0)
5050
mount(testComponent())
5151
expect(getCurrentPosition).toHaveBeenCalledTimes(1)
5252
expect(watchPosition).toHaveBeenCalledTimes(1)
5353
})
5454

55-
it('should call clearWatch onUnmounted', () => {
55+
it('should call clearWatch on unmounted', () => {
5656
expect(clearWatch).toHaveBeenCalledTimes(0)
5757
const wrapper = mount(testComponent())
5858
wrapper.vm.$destroy()

src/components/useHover/useHover.spec.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mount } from '@src/helpers/test'
1+
import { checkOnMountAndUnmountEvents, mount } from '@src/helpers/test'
22
import { ref } from '@vue/composition-api'
33
import { useHover } from '@src/vue-use-kit'
44

@@ -18,25 +18,10 @@ const testComponent = () => ({
1818
})
1919

2020
describe('useHover', () => {
21-
it('should call document.body hover events', () => {
22-
const addEventListenerSpy = jest.spyOn(document.body, 'addEventListener')
23-
const removeEventListenerSpy = jest.spyOn(
24-
document.body,
25-
'removeEventListener'
26-
)
27-
const wrapper = mount(testComponent())
28-
expect(addEventListenerSpy).toHaveBeenCalled()
29-
expect(addEventListenerSpy).toBeCalledWith(
30-
'mouseenter',
31-
expect.any(Function)
32-
)
33-
expect(removeEventListenerSpy).not.toHaveBeenCalled()
34-
wrapper.destroy()
35-
expect(removeEventListenerSpy).toHaveBeenCalled()
36-
expect(removeEventListenerSpy).toBeCalledWith(
37-
'mouseenter',
38-
expect.any(Function)
39-
)
21+
const events = ['mouseenter', 'mouseleave']
22+
23+
it('should add events on mounted and remove them on unmounted', async () => {
24+
await checkOnMountAndUnmountEvents(document.body, events, testComponent)
4025
})
4126

4227
it('should return isHovered false by default', () => {

0 commit comments

Comments
 (0)