To build anticipation and excitement for an upcoming event on web applications you can use a countdown timer, simply it will show how much time is left (as an example) for new product releases or updates.
In this post, I would like to share with you my implementation of building a Countdown Timer using Vuejs Composables.
Content
- Create a Vuejs composable.
- Layout the Timer.
- Conclusion, full code and working example.
If you want to follow along with me, it is recommended that you have a basic knowledge of Vuejs Composables.
Writing composables well is an essential Vuejs skill, you can make your composables even better by:
- Use an options object as the parameter.
- Make sure each composable only does one thing but does it well.
- Name them consistently by prefixing with (use, create).
- Accept both refs and primitive values as inputs.
Allow me to show you.
Create a Vuejs composable
The countdown timer composable that I'm going to create will accept options object as the prameter. The object has two props, a source
prop to hold the date HTML input ref or a primitive date-time string, the format for any of these values is required to be "YYYY-MM-DDTHH:mm", and a callback
prop to hold the function to be executed once the timer has completed counting.
It will also validate the date input to ensure that it is not empty or expired and has a valid syntax.
First method within the composable is startCounting
, it will tick through a time interval and calculate the difference between the current time value and the target time value, in a form of Unix Timestamp.
Second method is stopCounting
, it wil clear the ticking time interval and reset all the props to the default values.
It is very important to calculate the publishing time. I achieved this by utilizing the totalDays
prop out of the time interval.
Finally any composable must return a value, in this example it will return the values needed to show the counting progress to the user as following:
import { ref, isRef } from "vue";
export function useCountDownTimer({source = 0, callback = null}) {
let countingInterval = null;
const secs = ref(0);
const mins = ref(0);
const hrs = ref(0);
const days = ref(0);
const isCounting = ref(false);
const errMsg = ref(null);
const totalDays = ref(null);
const validatingDate = (date) => {
if(date.length === 0) {
errMsg.value = 'date is empty';
return false;
} else if((new Date(date)).getTime() - Date.now() < 0) {
errMsg.value = 'date is expired';
return false;
} else if(!(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})$/.test(date))) {
errMsg.value = 'date format must be YYYY-MM-DDTHH:mm';
return false;
} else {
errMsg.value = null;
return true;
}
};
const startCounting = () => {
const target = isRef(source) ? source.value : ref(source);
if( !validatingDate(target.value) ) return;
const unixTimeStamp = (new Date(target.value)).getTime();
isCounting.value = true;
totalDays.value = Math.floor( Math.floor( Math.abs( Date.now() - unixTimeStamp ) / 1000 ) / (60 * 60 * 24) );
countingInterval = setInterval(() => {
const diff = Math.floor( Math.abs( Date.now() - unixTimeStamp ) / 1000 );
if(diff === 0){
stopCounting();
if(callback) callback();
}
secs.value = Math.floor( diff % 60 );
mins.value = Math.floor( (diff % (60 * 60)) / 60 );
hrs.value = Math.floor( (diff % (60 * 60 * 24)) / (60 * 60) );
days.value = Math.floor(diff / (60 * 60 * 24));
}, 1000);
};
const stopCounting = () => {
clearInterval(countingInterval);
isCounting.value = false;
secs.value = 0;
mins.value = 0;
hrs.value = 0;
days.value = 0;
}
return {
secs,
mins,
hrs,
days,
startCounting,
stopCounting,
isCounting,
errMsg,
totalDays
};
}
Layout the Timer
The timer will present the counting down values through four components named as CounterCircle
. Each counter component will output the remaining amount of a single time unit value, and by using tailwind I will paint the necessary HTML that will render the timer to the user:
<div
class="counter-circle inline-block relative rounded-full"
:style="{'background-color': props.bgColor, 'color': props.fontColor}"
>
<span class="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%]">{{ count }}</span>
<svg className="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%] h-full w-full" viewBox="0 0 36 36">
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
stroke-linecap="round"
stroke="#f0edc4"
:stroke-width="borderWidth"
fill="none"
stroke-dasharray="100, 100"
/>
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
id="rating-circle"
ref="ratingCircle"
stroke-linecap="round"
:stroke="borderColor"
:stroke-width="borderWidth"
fill="none"
stroke-dashoffset="100"
/>
</svg>
</div>
Make the counter component responsive to multiple screens through SCSS styling, considering the font and the circle's width/height:
.circle-counter {
height: 18vw;
width: 18vw;
max-width: 15rem;
max-height: 15rem;
&, span {
min-height: 0vw;
font-size: clamp(24px, calc(1.5rem + ((1vw - 3.2px) * 8.9286)), 64px);
}
}
By watching the props.count
changes, CounterCircle
component will update the svg's attribute stroke-dashoffset
to map it's length with the new value of a specific time unit:
import { ref, watch } from 'vue';
const props = defineProps([ 'count', 'countFrom', 'bg-color', 'font-color', 'borderColor', 'borderWidth']);
const ratingCircle = ref(null);
watch(()=> props.count, count => {
const lengthToAnimate = 100 - Math.floor( ( count / props.countFrom ) * 100 );
ratingCircle.value.setAttribute('stroke-dashoffset', lengthToAnimate);
if(count === 0) ratingCircle.value.setAttribute('stroke-dashoffset', 100);
});
Conclusion
Vuejs composables are a great way to encapsulate and reuse stateful logic within web applications (frontend web applications).
It is also possible that composable functions can call other composable functions, and that can help to break down big complex app features into smaller reusable building blocks.
If you want more examples of well-written composables, look through VueUse source code.
Your feedback is greatly appreciated, thank you for reading.
Top comments (0)