I recently wrote an article explaining how you can create a countdown timer using HTML, CSS and JavaScript. Now, let’s look at how we can make that a reusable component by porting it into Vue using basic features that the framework provides.
Why do this at all? Well there are few reasons, but two stand out in particular:
- Keeping UI in sync with the timer state: If you look at the code from the first post, it all lives in the timerInterval function, most noticeably the state management. Each time it runs (every second) we need to manually find the proper element on our document — whether it’s the time label or the remaining time path or whatever — and change either its value or an attribute. Vue comes with an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying Vue instance’s data. That takes all the burden of finding and updating proper UI elements so we can rely purely on the component instance’s properties.
- Having a highly reusable component: The original example works fine when only one timer is present on our document, but imagine that you want to add another one. Oops! We rely the element’s ID to perform our actions and using the same ID on multiple instances would prevent them from working independently. That means we would have to assign different IDs for each timer. If we create a Vue component, all it’s logic is encapsulated and connected to that specific instance of the component. We can easily create 10, 20, 1,000 timers on a single document without changing a single line in the component itself!
Here’s the same timer we created together in the last post, but in Vue.
Template and styles
From the Vue docs:
Vue uses an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying Vue instance’s data. All Vue.js templates are valid HTML that can be parsed by spec-compliant browsers and HTML parsers.
Let’s create our component by opening a new file called BaseTimer.vue
. Here’s the basic structure we need for that:
// Our template markup will go here
<template>
// ...
</template>
// Our functional scripts will go here
<script>
// ...
</script>
// Our styling will go here
<style>
// ...
</style>
In this step, we will concentrate on the <template>
and <style>
sections. Let’s move our timer template to the <template>
section and all our CSS to <style>
section. The markup mostly consists of SVG and we can use the exact same code we used from the first article.
<template>
// The wrapper for the timer
<div class="base-timer">
// This all comes from the first article
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
// The label showing the remaining time
<span
id="base-timer-label"
class="base-timer__label"
>
${formatTime(timeLeft)}
</span>
</div>
</template>
// "scoped" means these styles will not leak out to other elements on the page
<style scoped>
.base-timer {
position: relative;
width: 100px;
height: 100px;
}
</style>
Let’s have a look at the template we just copied to identify where we can use our framework. There are few parts that are responsible for making our timer count down the time and show the remaining time.
stroke-dasharray
: A value passed to the SVG<path>
element that is responsible for holding the remaining time.remainingPathColor
: A CSS class responsible for changing the color of the timer’s circular ring, giving is a way to visually indicate that time is running out.formatTime(timeLeft)
: A value responsible for showing how much time is left inside the timer
We can control our timer by manipulating those values.
Constants and variables
OK, let’s go down to our <script>
section and see what Vue gives us out of the box to make our life easier. One thing it lets us do is define our constants up front, which keeps them scoped to the component.
In the last post, we spent a little time tweaking the stroke-dasharray
value to make sure the animation of the timer’s top layer (the ring that animates and changes color as time progresses) is perfectly in line with its bottom layer (the gray ring that indicates past time). We also defined “thresholds” for when the top layer should change colors (orange at 10 remaining seconds and red at five seconds). We also created constants for those colors.
We can move all of those directly into the <script>
section:
<script>
// A value we had to play with a bit to get right
const FULL_DASH_ARRAY = 283;
// When the timer should change from green to orange
const WARNING_THRESHOLD = 10;
// When the timer should change from orange to red
const ALERT_THRESHOLD = 5;
// The actual colors to use at the info, warning and alert threshholds
const COLOR_CODES = {
info: {
color: "green"
},
warning: {
color: "orange",
threshold: WARNING_THRESHOLD
},
alert: {
color: "red",
threshold: ALERT_THRESHOLD
}
};
// The timer's starting point
const TIME_LIMIT = 20;
</script>
Now, let’s have a look at our variables:
let timePassed = 0;
let timeLeft = TIME_LIMIT;
let timerInterval = null;
let remainingPathColor = COLOR_CODES.info.color;
We can identify two different types of variables here:
- Variables in which the values are directly re-assigned in our methods:
timerInterval
: Changes when we start or stop the timertimePassed
: Changes each second when the timer is running
- Variables in which the values change when other variables change:
timeLeft
: Changes when the value oftimePassed
changesremainingPathColor
: Changes when the value oftimeLeft
breaches the specified threshold
It is essential to identify that difference between those two types as it allows us to use different features of the framework. Let’s go through each of the type separately.
Variables in which values are directly re-assigned
Let’s think what we want to happen when we change the timePassed
value. We want to calculate how much time is left, check if we should change the top ring’s color, and trigger re-render on a part of our view with new values.
Vue comes with its own reactivity system that updates the view to match the new values of specific properties. To add a property to Vue’s reactivity system we need to declare that property on a data
object in our component. By doing that,Vue will create a getter and a setter for each property that will track changes in that property and respond accordingly.
<script>
// Same as before
export default {
data() {
return {
timePassed: 0,
timerInterval: null
};
}
</script>
There are two important things we need to remember.
- We need to declare all reactive variables in our
data
object up front. That means if we know that a variable will exist but we don’t know what the value will be, we still need to declare it with some value. If we forgot to declare it indata
it will not be reactive, even if it is added later. - When declaring our
data
option object, we always need to return a new object instance (usingreturn
). This is vital because, if we don’t follow this rule, the declared properties will be shared between all instances of the component.
You can see that second issue in action:
Variables in which values change when other variable change
These variables rely on the value of another variable. For example, timeLeft
relies purely on timePassed
. In our original example that uses vanilla JavaScript, we were calculating that value in the interval that was responsible for changing the value of timePassed
. With Vue, we can extract that value to a computed
property.
A computed
property is a function that returns a value. These values are bound to the dependency values and only update when required. Even more importantly, computed
properties are cached, meaning they remember the values that the computed
property depends on and calculate the new value only if that dependent property value changed. If the value does not change, the previously cached value is returned.
<script>
// Same as before
computed: {
timeLeft() {
return TIME_LIMIT - this.timePassed;
}
}
}
</script>
The function passed to the computed
property must be a pure function. It can’t cause any side effects and must return a value. Also, the output value must only be dependent on the values passed into the function.
Now, we can move more logic to computed
properties:
circleDasharray
: This returns a value previously that is calculated in thesetCircleDasharray
method.formattedTimeLeft
: This returns a value from theformatTime
method.timeFraction
: This is an abstraction of thecalculateTimeFraction
method.remainingPathColor
: This is an abstraction of thesetRemainingPathColor
method.
<script>
// Same as before
computed: {
circleDasharray() {
return `${(this.timeFraction * FULL_DASH_ARRAY).toFixed(0)} 283`;
},
formattedTimeLeft() {
const timeLeft = this.timeLeft;
const minutes = Math.floor(timeLeft / 60);
let seconds = timeLeft % 60;
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
},
timeLeft() {
return TIME_LIMIT - this.timePassed;
},
timeFraction() {
const rawTimeFraction = this.timeLeft / TIME_LIMIT;
return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
},
remainingPathColor() {
const { alert, warning, info } = COLOR_CODES;
if (this.timeLeft <= alert.threshold) {
return alert.color;
} else if (this.timeLeft <= warning.threshold) {
return warning.color;
} else {
return info.color;
}
}
}
</script>
We now have all the values we need! But now we need to put them to use in our template.
Using data and computed properties in the template
Here’s where we left off with our template:
<template>
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span
id="base-timer-label"
class="base-timer__label"
>
${formatTime(timeLeft)}
</span>
</div>
</template>
Let’s start with formatTime(timeLeft)
. How we can dynamically bind the rendered value to our formattedTimeLeftcomputed
property?
Vue uses HTML-based template syntax that allowsus to declaratively bind the rendered DOM to the underlying data of the Vue instance. That means all properties are available in the template section. To render any of them, we use text interpolation using the “Mustache” syntax (double curly braces, or {{ }}
).
<span
id="base-timer-label"
class="base-timer__label"
>
{{ formattedTimeLeft }}
</span>
Next will be stroke-dasharray
. We can see we don’t want to render that value. Instead, we want to change the value of the <path>
attribute. Mustache cannot be used inside HTML attributes, but fear not! Vue comes with another way: the v-bind
directive. We can bind a value to an attribute like this:
<path v-bind:stroke-dasharray="circleDasharray"></path>
To facilitate the usage of that directive, we can also use a shorthand.
<path :stroke-dasharray="circleDasharray"></path>
The last one is remainingPathColor
, which adds a proper class to an element. We can do that using the same v-bind
directive as above, but assign the value to the class
attribute of an element.
<path :class="remainingPathColor"></path>
Let’s have a look at our template after changes.
<template>
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
:stroke-dasharray="circleDasharray"
class="base-timer__path-remaining"
:class="remainingPathColor"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span class="base-timer__label">{{ formattedTimeLeft }}</span>
</div>
</template>
We have our template ready, we moved all variables to data
or computed
, and we got rid off most of the methods by creating corresponding computed
properties. We are still missing one vital part, though: we need to start our timer.
Methods and component lifecycle hooks
If we look at our startTimer
method, we can see that all the calculations, changes in attributes, etc. happen in the interval.
function startTimer() {
timerInterval = setInterval(() => {
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
document.getElementById("base-timer-label").innerHTML = formatTime(
timeLeft
);
setCircleDasharray();
setRemainingPathColor(timeLeft);
if (timeLeft === 0) {
onTimesUp();
}
}, 1000);
}
Since we’ve already moved all that logic into the computed
property, all we need to do in our timerInterval
is change the value of timePassed
— the rest will happen magically in the computed
properties
<script>
// Same as before
methods: {
startTimer() {
this.timerInterval = setInterval(() => (this.timePassed += 1), 1000);
}
}
</script>
We have the method ready, but we still don’t call it anywhere. Each Vue component comes with a series of hooks that allows us to run a specific logic within a specific period of the component’s lifecycle. These are called lifecycle hooks. In our case, as we want to call our method immediately when the component gets loaded. That makes mounted
the lifecycle hook what we want.
<script>
// Same as before
mounted() {
this.startTimer();
},
// Same methods as before
</script>
That’s it, we just turned our timer into a consistent and reusable component using Vue!
Let’s say we now want to use this component in another component. That requires a few things:
- First, we import the component.
- Next, we register the component.
- Finally, we instantiate the component in the template.
// App.vue
import BaseTimer from "./components/BaseTimer"
export default {
components: {
BaseTimer
}
};
That’s a wrap!
This example shows how we can move a component from vanilla JavaScript to a component-based front-end framework, like Vue.
We can now treat the timer as a standalone component where all the markup, logic and styling is contained in a way that won’t leak out to or conflict with other elements. Components are often children of a larger parent component that assembles multiple components together — like a form or perhaps a card — where the parent’s properties can be accessed and shared. Here’s an example of the timer component where it’s taking orders from a parent component
I hope I got you interested in Vue and the power of components! I’d encourage you to go to Vue docs to get more detailed description of the features we used in our example. There’s so much Vue can do!