Vue.js Nation 2023 - Lightning Talk
Vue.js Nation 2023 - Lightning Talk
Let’s take a look at an example of an "impolite popup"
The goal is that the visitors have to
before they get asked to sign up.
Let’s start by writing a Vue composable for our polite popup:
export const usePolitePopup = () => { const visible = ref(false); const trigger = () => {} return { visible, trigger, }; };
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, }; };
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, } }
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, } }
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 } }
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>
<template> <main> <ContentDoc /> </main> </template> <script setup lang="ts"> const route = useRoute(); const { trigger } = usePolitePopup(); if (route.path === "/vue") { trigger(); } </script>