Learning Vue Part 3: Building a pomodoro timer

willowjr

William Kwadwo Owusu

Posted on September 8, 2024

Learning Vue Part 3: Building a pomodoro timer

For my third project, I decided to create a Pomodoro Timer, a productivity tool designed to help manage deep work sessions followed by short breaks. In building this, I expanded my understanding of Vue.js, particularly when it comes to managing time-based states, form handling, and user interactions. Here’s an overview of the key lessons I’ve learned.

1. Handling Form Submissions with Vue: Custom Work and Break Times

The first part of the project involved setting up a simple form to allow the user to set their desired work and break times. Just like in my previous projects, I used Vue’s @submit.prevent to stop the form from refreshing the page:

<form class="mt-4" @submit.prevent="setTime">
    <div class="form-row mx-2">
        <div class="col-4">
            <label class="font-weight-bold">Work (min)</label>
            <input type="number" v-model="workTime" class="form-control" placeholder="eg: 25">
        </div>
        <div class="col-4">
            <label class="font-weight-bold">Break (min)</label>
            <input type="number" v-model="breakTime" class="form-control" placeholder="eg: 5">
        </div>
        <div class="col-4">
            <label style="visibility: hidden;">Submit</label>
            <input type="submit" class=" form-control btn btn-outline-light text-white">
        </div>
    </div>
</form>
Enter fullscreen mode Exit fullscreen mode

Vue’s v-model allows for two-way data binding, making it easy to capture and manipulate the input values for work and break time. Validating user input (e.g., ensuring the input is a valid number) was also something I had to account for, which I handled within the setTime method:

const setTime = () => {
    if (workTime.value == '' || breakTime.value == '' || workTime.value <= 0 || breakTime.value <= 0) {
        alert("Please enter a valid number of minutes!");
        return;
    }
    totalSeconds.value = (workTime.value * 60);
    initialWorkTime = (workTime.value * 60);
    initialBreakTime = (breakTime.value * 60);
    updateMinutesSeconds();
    alert("Work and break time successfully updated");
};
Enter fullscreen mode Exit fullscreen mode

This was a valuable exercise in handling form validation and ensuring data integrity.

2. State Management: Tracking Time with Reactive Properties

Managing the timer functionality required me to keep track of the number of seconds and minutes remaining. Vue’s ref method was crucial for this, as it made the data reactive and ensured the UI updated automatically as the timer counted down:

const totalSeconds = ref(initialWorkTime);
const seconds = ref(totalSeconds.value % 60);
const minutes = ref(Math.floor(totalSeconds.value / 60));
Enter fullscreen mode Exit fullscreen mode

To continuously update the timer, I used setInterval to decrement the time every second:

const startTimer = () => {
    isPaused.value = false;
    if (!nIntervalId.value) {
        nIntervalId.value = setInterval(() => {
            if (totalSeconds.value > 0 && isPaused.value == false) {
                totalSeconds.value--;
                updateMinutesSeconds();
            } else if (totalSeconds.value == 0) {
                clearInterval(nIntervalId.value);
                nIntervalId.value = null;
                setBreak();
            }
        }, 1000);
    }
};
Enter fullscreen mode Exit fullscreen mode

This not only allowed the timer to work correctly but also highlighted how Vue’s reactivity system handles time-sensitive operations well.

3. Computed Properties: Formatting Time for Display

When building a timer, it's important to display the time in a readable format. To ensure that the seconds display with a leading zero (e.g., 04 instead of 4), I used a computed property in Vue:

const formattedSeconds = computed(() => {
    return seconds.value < 10 ? `0${seconds.value}` : seconds.value;
});
Enter fullscreen mode Exit fullscreen mode

This was a great opportunity to use Vue’s computed property, which efficiently recalculates values only when dependencies change, making it ideal for time formatting.

4. Handling Timer Control: Start, Pause, Reset

Implementing start, pause, and reset functionalities required careful management of the timer’s state. I used Vue’s reactivity to track whether the timer was running or paused:

let isPaused = ref(false);
Enter fullscreen mode Exit fullscreen mode
  • Start Timer: This resumes the countdown by setting isPaused to false and starting the interval if it isn’t already running.
  • Pause Timer: I simply set isPaused to true when the user wants to pause the timer.
  • Reset Timer: This clears the interval and restores the timer to its initial state:
const resetTimer = () => {
    clearInterval(nIntervalId.value);
    nIntervalId.value = null;
    totalSeconds.value = initialWorkTime;
    updateMinutesSeconds();
    isPaused.value = false;
    workPeriod = true;
};
Enter fullscreen mode Exit fullscreen mode

This was a key lesson in how to manage state transitions in a Vue app, especially when working with intervals and reactive variables.

5. Switching Between Work and Break Periods

A Pomodoro timer involves alternating between a deep work period and a break period. When the work period ends, I switch to break mode using the setBreak method:

const setBreak = () => {
    totalSeconds.value = initialBreakTime;
    updateMinutesSeconds();
    workPeriod = false;
};
Enter fullscreen mode Exit fullscreen mode

This logic ensures that after the timer hits zero during the work session, it transitions automatically to the break session, and vice versa.

6. Enhancing User Experience with Tooltips and Icons

To make the UI more user-friendly, I added Bootstrap icons and tooltips. For example, I used buttons with icons for Start, Pause, Reset, and Switch to Break, each with tooltips to clarify their functionality:

<button class="btn btn-lg btn-light text-danger mx-3" data-toggle="tooltip" data-placement="top" title="Start/Resume timer" @click="startTimer"><i class="bi bi-play-fill"></i></button>
<button class="btn btn-lg btn-light text-danger mx-3" data-toggle="tooltip" data-placement="top" title="Pause timer" @click="pauseTimer"><i class="bi bi-pause-fill"></i></button>
<button class="btn btn-lg btn-light text-danger mx-3" data-toggle="tooltip" data-placement="top" title="Reset timer" @click="resetTimer"><i class="bi bi-arrow-counterclockwise"></i></button>
<button class="btn btn-lg btn-light text-danger mx-3" data-toggle="tooltip" data-placement="top" title="Go to break time" @click="setBreak"><i class="bi bi-lightning-charge-fill"></i></button>
Enter fullscreen mode Exit fullscreen mode

These small enhancements added a lot to the user experience by making the interface more intuitive.

Final Thoughts: What I’ve Gained from Building a Pomodoro Timer

This project deepened my understanding of how Vue.js can be used for time-based, interactive apps. I learned how to:

  • Effectively manage timers and state transitions with setInterval.
  • Use computed properties to handle dynamic UI changes, such as formatting time.
  • Implement a user-friendly form to capture and validate input, ensuring smooth user interaction.
  • Create an intuitive user interface with Bootstrap icons and tooltips.

By completing this Pomodoro Timer, I gained a solid grasp of more advanced state management and how to create user-friendly experiences with Vue.

Looking forward to more Vue adventures!

💖 💪 🙅 🚩
willowjr
William Kwadwo Owusu

Posted on September 8, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related