theme | title | colorSchema | exportFilename | download | globalBottomPosition |
---|---|---|---|---|---|
mokkapps |
Building a Polite Popup with Nuxt 3 |
light |
vuejs-nation-2023-lightning-talk-polite-popup |
true |
left |
Vue.js Nation 2023 - Lightning Talk
layout: iframe-right url: "https://vuejsnation-2023-polite-popup-nuxt-3.netlify.app/?demoMode=impolite-popup"
Let's take a look at an example of an "impolite popup"
- Problem 1: Another annoying full-page popup on the landing page
- Problem 2: Visitors haven't engaged with the related content
- Problem 3: Visitors are asked every time they visit the page
layout: iframe-right url: >- https://vuejsnation-2023-polite-popup-nuxt-3.netlify.app/?demoMode=polite-popup
A polite popup appears to visitors if they
- are visiting a page with Vue-related content as the newsletter targets Vue developers
- are actively scrolling the current page for 6 seconds or more
- scroll through at least 35% of the current page during their visit
layout: image image: https://media.giphy.com/media/13GIgrGdslD9oQ/giphy.gif
Let's start by writing a Vue composable for our polite popup:
export const usePolitePopup = () => {
const visible = ref(false);
const trigger = () => {}
return {
visible,
trigger,
};
};
The visitor must be actively scrolling the current page for 6 seconds or more.
import { useTimeoutFn } from '@vueuse/core'
const config = { timeoutInMs: 6000 } as const
export const usePolitePopup = () => {
const visible = ref(false);
const readTimeElapsed = ref(false)
const { start } = useTimeoutFn(
() => {
readTimeElapsed.value = true
},
config.timeoutInMs,
{ immediate: false }
)
const trigger = () => {
readTimeElapsed.value = false
start()
}
return {
visible,
trigger,
};
};
The visitor must scroll through at least 35% of the current page during their visit.
import { useWindowSize, useWindowScroll } from '@vueuse/core'
const config = { timeoutInMs: 6000, contentScrollThresholdInPercentage: 35, } as const
export const usePolitePopup = () => {
//...
const { height: windowHeight } = useWindowSize()
const { y: scrollTop } = useWindowScroll()
// Returns percentage scrolled (ie: 80 or NaN if trackLength == 0)
const amountScrolledInPercentage = computed(() => {
const documentScrollHeight = document.documentElement.scrollHeight
const trackLength = documentScrollHeight - windowHeight.value
const scrollPercent = scrollTop.value / trackLength;
const scrollPercentRounded = Math.floor(scrollPercent * 100);
return scrollPercentRounded;
})
const scrolledContent = computed(() => {
return amountScrolledInPercentage.value >= config.contentScrollThresholdInPercentage
)}
return {
visible,
trigger,
}
}
We have now all information available to update the visible
reactive variable:
export const usePolitePopup = () => {
const visible = ref(false)
const readTimeElapsed = ref(false)
//...
const scrolledContent = computed(() => amountScrolledInPercentage.value >= config.contentScrollThresholdInPercentage)
watch([readTimeElapsed, scrolledContent], ([newReadTimeElapsed, newScrolledContent]) => {
if (newReadTimeElapsed && newScrolledContent) {
visible.value = true
}
})
return {
visible,
trigger,
}
}
import { useLocalStorage } from '@vueuse/core'
interface PolitePopupStorageDTO {
status: 'unsubscribed' | 'subscribed'
seenCount: number
lastSeenAt: number
}
export const usePolitePopup = () => {
//...
const storedData: Ref<PolitePopupStorageDTO> = useLocalStorage('polite-popup', {
status: 'unsubscribed',
seenCount: 0,
lastSeenAt: 0,
})
//...
watch(
[readTimeElapsed, scrolledContent],
([newReadTimeElapsed, newScrolledContent]) => {
if (newReadTimeElapsed && newScrolledContent) {
visible.value = true;
storedData.value.seenCount += 1;
storedData.value.lastSeenAt = new Date().getTime();
}
}
);
//...
return {
visible,
trigger
}
}
In [..slug].vue
we trigger the timer if the route path is equal to /vue
:
<template>
<main>
<ContentDoc />
</main>
</template>
<script setup lang="ts">
const route = useRoute();
const { trigger } = usePolitePopup();
if (route.path === "/vue") {
trigger();
}
</script>
layout: image-right image: https://media.giphy.com/media/dkGhBWE3SyzXW/giphy.gif
We implemented the main logic for a polite popup in Nuxt 3 💪🏻
Thanks to the amazing people behind
For more details read the corresponding blog post
Questions?