Skip to content

Commit 7e20e02

Browse files
authored
Feat: Add support for initialization onBeforeMount ✨ (#37)
* feat: Add support for initialization onBeforeMount ✨ - If onBeforeMount is true, initialization occurs onBeforeMount. - Otherwise, the composable initializes onMounted * Minor refactor. Also replaces relative imports
1 parent a653231 commit 7e20e02

16 files changed

+193
-121
lines changed

README.md

+72-48
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
# Enhance User Engagement with Exit Intent Detection.
2+
23
### A composable to show your modal when a user is about to leave the page or another threshold reached.
4+
35
[Demo](https://vue-exit-intent.netlify.app/)
46

57
The very first version of this package created in favor of [this](https://dev.to/nickap/exit-intent-pop-up-how-to-publish-on-npm-vue-3-3bhm) old guide. [Here](https://dev.to/nickap/use-vue-exit-intent-a-vue-composable-to-power-your-exit-intent-content-4hlh) is an updated article regarding the latest version.
68

79
# Usage
10+
811
### Import the composable and show your content according to the value of `isShowing`.
12+
913
## Add the package
14+
1015
```
1116
npm i vue-exit-intent
1217
```
18+
1319
## Use the composable
14-
``` javascript
20+
21+
```javascript
1522
<sript setup lang="ts">
1623
import { useVueExitIntent } from 'vue-exit-intent'
1724

@@ -22,15 +29,18 @@ const options = {
2229

2330
const { isShowing, close } = useVueExitIntent(options);
2431
</script>
25-
```
26-
``` html
32+
```
33+
34+
```html
2735
<template>
2836
<yourModalPopUp v-if="isShowing" @close="close"></yourModalPopUp>
2937
</template>
30-
```
38+
```
3139

3240
## Available helpers
41+
3342
If you use all available helpers your code would look like this:
43+
3444
```javascript
3545
const {
3646
isShowing,
@@ -41,76 +51,87 @@ const {
4151
unsubscribe
4252
} = useVueExitIntent(options);
4353
```
54+
4455
`isShowing`: a reactive boolean ref that tracks whether the exit intent popup is currently visible.
4556
`isAllowedToGetTriggered`: a reactive boolean ref that tracks whether the exit intent popup is allowed to trigger.
4657
`isUnsubscribed`: a reactive boolean ref that tracks whether the user has unsubscribed from the exit intent popup.
4758
`close`: a function that closes the exit intent popup and resets any related states (e.g. the isShowing ref).
4859
`resetState`: a function that resets all state related to the exit intent popup (e.g. isShowing, isAllowedToGetTriggered, isUnsubscribed).
49-
`unsubscribe`: a function that unsubscribes the user from the exit intent popup.
60+
`unsubscribe`: a function that unsubscribes the user from the exit intent popup.
61+
5062
## Options
51-
| Key | Default Value | Type | Required |
52-
| --- | --- | --- | --- |
53-
| **repeatAfterDays** | 7 | Number | false |
54-
| **scrollPercentageToTrigger** | 0 | Number | false |
55-
| **delaySecondsAndTrigger** | 0 | Number | false |
56-
| **triggerOnExitIntent** | true | Boolean | false |
57-
| **touchDeviceSensitivity** | 15 | Number | false |
58-
| **scrollDebounceMillis** | 300 | Number | false |
59-
| **triggerOnPageLoad** | false | Boolean | false |
60-
| **handleScrollBars** | false | Boolean | false |
61-
| **LSItemKey** | 'vue-exit-intent' | String | false |
63+
64+
| Key | Default Value | Type | Required |
65+
| ----------------------------- | ----------------- | ------- | -------- |
66+
| **repeatAfterDays** | 7 | Number | false |
67+
| **scrollPercentageToTrigger** | 0 | Number | false |
68+
| **delaySecondsAndTrigger** | 0 | Number | false |
69+
| **triggerOnExitIntent** | true | Boolean | false |
70+
| **touchDeviceSensitivity** | 15 | Number | false |
71+
| **scrollDebounceMillis** | 300 | Number | false |
72+
| **triggerOnPageLoad** | false | Boolean | false |
73+
| **handleScrollBars** | false | Boolean | false |
74+
| **LSItemKey** | 'vue-exit-intent' | String | false |
75+
| **setupBeforeMount** | false | Boolean | false |
6276

6377
### Options Description
78+
6479
- **repeatAfterDays**
65-
After how many days you want the popup to get triggered again.
66-
When a user gets the popup that exact timestamp is stored in localstorage and its taken into account next time the user will visit your page.
67-
Giving a zero, the popup will be shown only once! Until the localstrage of the user gets cleared/resets.
68-
**Give 0 to disable.**
69-
**This one runs a CHECK before show.**
80+
After how many days you want the popup to get triggered again.
81+
When a user gets the popup that exact timestamp is stored in localstorage and its taken into account next time the user will visit your page.
82+
Giving a zero, the popup will be shown only once! Until the localstrage of the user gets cleared/resets.
83+
**Give 0 to disable.**
84+
**This one runs a CHECK before show.**
7085

7186
- **scrollPercentageToTrigger**
72-
A scroll percentage that if reached by the user, the pop-up will get triggered.
73-
**Give 0 to disable.**
74-
**This one TRIGGERS the popup.**
87+
A scroll percentage that if reached by the user, the pop-up will get triggered.
88+
**Give 0 to disable.**
89+
**This one TRIGGERS the popup.**
7590

7691
- **delaySecondsAndTrigger**
77-
Trigger the pop-up after a short delay in seconds, once the page has loaded.
78-
**Give 0 to disable.**
79-
**This one TRIGGERS the popup.**
92+
Trigger the pop-up after a short delay in seconds, once the page has loaded.
93+
**Give 0 to disable.**
94+
**This one TRIGGERS the popup.**
8095

8196
- **touchDeviceSensitivity**
82-
On touch devices where there is no mouseleave event, the popup will get triggered on fast (touch)scroll up.
83-
The larger the number you will give, the more sesitive will be the pop-up on touch devices.
84-
**Give 0 to disable.**
85-
**This one TRIGGERS the popup on touch devices.**
97+
On touch devices where there is no mouseleave event, the popup will get triggered on fast (touch)scroll up.
98+
The larger the number you will give, the more sesitive will be the pop-up on touch devices.
99+
**Give 0 to disable.**
100+
**This one TRIGGERS the popup on touch devices.**
86101

87102
- **triggerOnExitIntent**
88-
If false. Mouse out event, and scroll-up-fast for touch devices, will not trigger the pop-up. The user would have to reach delaySecondsAndTrigger or scrollPercentageToTrigger to get the popup.
89-
If true, your modal pop-up is set to get triggered on user exit-intent.
90-
**This one TRIGGERS the popup.**
103+
If false. Mouse out event, and scroll-up-fast for touch devices, will not trigger the pop-up. The user would have to reach delaySecondsAndTrigger or scrollPercentageToTrigger to get the popup.
104+
If true, your modal pop-up is set to get triggered on user exit-intent.
105+
**This one TRIGGERS the popup.**
91106

92107
- **scrollDebounceMillis**
93-
Time in milliseconds to debounce user's scrolling
108+
Time in milliseconds to debounce user's scrolling
94109

95110
- **triggerOnPageLoad**
96-
Show your modal pop-up immediately When a user visits your page.
97-
**This one TRIGGERS the popup.**
111+
Show your modal pop-up immediately When a user visits your page.
112+
**This one TRIGGERS the popup.**
98113

99114
- **handleScrollBars**
100-
Composable will handle the value of: `document.body.style.overflowY`.
101-
Will be eather `auto` (when isShowing is `false`), or `hidden` (when isShowing is `true`)
115+
Composable will handle the value of: `document.body.style.overflowY`.
116+
Will be eather `auto` (when isShowing is `false`), or `hidden` (when isShowing is `true`)
102117

103118
- **LSItemKey**
104-
Key of Local Storage item.
105-
You can use a different key to show multiple pop-ups with different behaviour/content.
119+
Key of Local Storage item.
120+
You can use a different key to show multiple pop-ups with different behaviour/content.
121+
122+
- **setupBeforeMount**
123+
Determines whether the initialization of the composable occurs during the `onBeforeMount` lifecycle hook instead of the default `onMounted` hook.
124+
This options allows you to set up the exit intent before your component is mounted.
106125

107126
## Contribute
108-
Feel free to contribute, message me for your ideas.
109-
- Write tests.
110-
- Report bugs.
111-
- Share this project.
112-
- Give a star if you like it.
113-
- Improve the documentation.
127+
128+
Feel free to contribute, message me for your ideas.
129+
130+
- Write tests.
131+
- Report bugs.
132+
- Share this project.
133+
- Give a star if you like it.
134+
- Improve the documentation.
114135
- Open an issue if you have any.
115136

116137
# Instructions for Contributors
@@ -138,14 +159,17 @@ npm run build
138159
```sh
139160
npm run lint
140161
```
162+
141163
## Recommended IDE Setup
142164

143165
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
144166

145167
## Customize configuration
146168

147169
See [Vite Configuration Reference](https://vitejs.dev/config/).
170+
148171
## License
172+
149173
```
150174
The MIT License (MIT)
151175
@@ -156,4 +180,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
156180
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
157181
158182
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
159-
```
183+
```

src/App.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts" setup>
2-
import modalPopUp from './components/modalPopUp.vue';
3-
import { useVueExitIntent } from './composables/useVueExitIntent.js';
2+
import modalPopUp from '@/components/modalPopUp.vue';
3+
import { useVueExitIntent } from '@/composables/useVueExitIntent.js';
44
55
const LSItemKey = 'demo-vue-exit-intent';
66

src/composables/useVueExitIntent.ts

+46-28
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { ref, onMounted, watch } from 'vue';
1+
import { ref, onMounted, watch, onBeforeMount } from 'vue';
22
import {
33
defaultOptions,
44
isTouchDevice,
55
isLocalStorageExpired,
66
mouseHandler,
77
scrollHandler,
88
touchDeviceHandler
9-
} from '../utils/';
10-
import type { IOptions, IUserOptions } from '../types';
9+
} from '@/utils';
10+
import type { Options } from '@/types';
1111

12-
export function useVueExitIntent(userOptions: IUserOptions = {}) {
13-
const options: IOptions = { ...defaultOptions, ...userOptions };
12+
export function useVueExitIntent(userOptions: Partial<Options> = {}) {
13+
const options: Options = { ...defaultOptions, ...userOptions };
1414

1515
const unsubscribedLSItemKey: string = options.LSItemKey + '-unsubscribed';
1616

@@ -54,43 +54,61 @@ export function useVueExitIntent(userOptions: IUserOptions = {}) {
5454
isAllowedToGetTriggered.value = false;
5555
}
5656

57-
onMounted(() => {
57+
const setup = () => {
5858
const unsubscribedValue = localStorage.getItem(unsubscribedLSItemKey);
5959
isUnsubscribed.value = unsubscribedValue
6060
? JSON.parse(unsubscribedValue)
6161
: false;
6262

6363
isAllowedToGetTriggered.value =
6464
isLocalStorageExpired(options) && !isUnsubscribed.value;
65-
});
65+
};
6666

67-
watch(isAllowedToGetTriggered, (newValue) => {
68-
if (newValue) {
69-
if (options.triggerOnExitIntent) {
70-
if (options.touchDeviceSensitivity && isTouchDevice()) {
71-
addTouchListeners();
72-
} else {
73-
addMouseListener();
74-
}
75-
}
76-
if (options.delaySecondsAndTrigger) {
77-
setTimeout(() => {
78-
fire();
79-
}, options.delaySecondsAndTrigger * 1000);
67+
const initialize = () => {
68+
if (options.triggerOnExitIntent) {
69+
if (options.touchDeviceSensitivity && isTouchDevice()) {
70+
addTouchListeners();
71+
} else {
72+
addMouseListener();
8073
}
81-
if (options.triggerOnPageLoad) {
74+
}
75+
if (options.delaySecondsAndTrigger) {
76+
setTimeout(() => {
8277
fire();
83-
}
84-
if (options.scrollPercentageToTrigger) {
85-
addScrollListener();
86-
}
78+
}, options.delaySecondsAndTrigger * 1000);
79+
}
80+
if (options.triggerOnPageLoad) {
81+
fire();
82+
}
83+
if (options.scrollPercentageToTrigger) {
84+
addScrollListener();
85+
}
86+
};
87+
88+
const disable = () => {
89+
removeMouseLeaveListeners();
90+
removeScrollListeners();
91+
removeTouchDeviceListeners();
92+
};
93+
94+
watch(isAllowedToGetTriggered, (newValue) => {
95+
if (newValue) {
96+
initialize();
8797
} else {
88-
removeMouseLeaveListeners();
89-
removeScrollListeners();
90-
removeTouchDeviceListeners();
98+
disable();
9199
}
92100
});
93101

102+
if (options.setupBeforeMount) {
103+
onBeforeMount(() => {
104+
setup();
105+
});
106+
} else {
107+
onMounted(() => {
108+
setup();
109+
});
110+
}
111+
94112
return {
95113
isShowing,
96114
isAllowedToGetTriggered,

src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { useVueExitIntent } from './composables/useVueExitIntent';
1+
export { useVueExitIntent } from '@/composables/useVueExitIntent';

src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { createApp } from 'vue';
2-
import App from './App.vue';
2+
import App from '@/App.vue';
33

44
createApp(App).mount('#app');

src/tests/composables/useVueExitIntent/basic.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ describe('useVueExitIntent composable basic funcionality', () => {
1111
const userOptions = { ...defaultOptions };
1212

1313
const App = {
14-
// TODO: Fix the need to have a template? Why to have a mounted hook?
1514
template: `<div></div>`,
1615
setup() {
1716
const { isShowing, isAllowedToGetTriggered, isUnsubscribed } =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test, expect, describe, afterEach } from 'vitest';
2+
import { useVueExitIntent } from '@/composables/useVueExitIntent';
3+
import { defaultOptions } from '@/utils';
4+
import { shallowMount } from '@vue/test-utils';
5+
6+
describe('Respects option setupBeforeMount', () => {
7+
afterEach(() => {
8+
localStorage.clear();
9+
});
10+
11+
test('Initializes correctly when setupBeforeMount is true', async () => {
12+
const userOptions = {
13+
...defaultOptions,
14+
setupBeforeMount: true,
15+
triggerOnPageLoad: true
16+
};
17+
18+
const App = {
19+
template: `<div></div>`,
20+
setup() {
21+
const { isShowing, isAllowedToGetTriggered, isUnsubscribed } =
22+
useVueExitIntent(userOptions);
23+
return {
24+
isShowing,
25+
isAllowedToGetTriggered,
26+
isUnsubscribed
27+
};
28+
}
29+
};
30+
31+
const wrapper = await shallowMount(App);
32+
33+
const { isShowing, isAllowedToGetTriggered, isUnsubscribed } = wrapper.vm;
34+
35+
expect(isShowing).toBe(true);
36+
expect(isAllowedToGetTriggered).toBe(false);
37+
expect(isUnsubscribed).toBe(false);
38+
expect(localStorage.getItem(userOptions.LSItemKey)).toBeTruthy();
39+
});
40+
});

0 commit comments

Comments
 (0)