DEV Community

Cover image for Creating Countdown Timer with Vuejs Composables
Andrew Zachary
Andrew Zachary

Posted on • Updated on

Creating Countdown Timer with Vuejs Composables

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

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
    };
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);

});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Full Code

Working Example

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.

Oldest comments (0)