This time, we are going to build our own Pomodoro Clock. No, this is not a Pomodoro, it’s actually a Commodore:
The Pomodoro clock has it’s own history, which is somewhat irrelevant to us at this point, but bear with me while I take you in a journey through time and space, a time of… Nevermind, it’s really just a clock. Actually, not even a clock, but a timer that allows us to set a few parameters. Namely: Pomodoro cycle time (25 min default) and break time (5 min default).
As a note, I will mention that we are not going to be using jQuery or any other library in this process. At the end of this post, you can find links to a live version of this exercise, along with an AngularJS and React version.
These are the user stories that we must implement:
- As a user, I can start a 25 minute pomodoro, and the timer will go off once 25 minutes has elapsed.
- Bonus: As a user, I can reset the clock for my next pomodoro.
- Bonus: As a user, I can customize the length of each pomodoro.
We’re awesome people so we’ll do all three of them. I’ll let you take care of the layout and overall design, but will guide you through the logic behind the code. I’m going to create a very simple timer and UI controls, just enough to have a working clock quick.
Here’s the basic layout:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Pomodoro Norrington</title> <link rel="stylesheet" href="css/font-awesome.min.css"> <link rel="stylesheet" href="css/main.css"> </head> <body><!-- -->25
<!-- --></div>5
<br />25:00
Click on play to start!
</div> http://js/app.js </body> </html>@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700); * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; } body { background-color: #1E2A42; color: #fff; } .main-wrapper { width: 80%; margin: 0 auto; max-width: 960px; } .time-config { .ui-control { display: inline-block; width: 50%; padding: 10px 2%; text-align: center; h3 { position: relative; top: 8px; padding: 0 3%; display: inline-block; font-size: 3rem; font-weight: 400; } button.circle-button { background-color: transparent; color: #fff; height: 1.6rem;; width: 1.6rem; line-height: 1.6rem; border: 1px solid #fff; border-radius: 0.8rem; cursor: pointer; } } } .timer-display { text-align: center; p#timer { font-size: 5rem; font-weight: 200; } } .timer-controls { text-align: center; i { cursor: pointer; margin: 0 2%; transition: 0.1s ease-in all; &:hover { color:aquamarine; } } }
Let’s get into the working logic of our Pomodoro Clock.
I’m going to be creating a Pomodoro object constructor, and then add several methods to it. It’ll be a nice way to contain most of the logic behind the actual clock within a single object.
I’m going to start by listing the methods and properties that we could find useful in this object, and briefly cover them. It may just be personal taste, but I prefer to plan them out instead of brainlessly adding methods and changing my mind as I go. Let’s start with the properties that the Pomodoro clock needs to properly work:
- Pomodoro cycle time: How long do we want each working cycle to be?
- Break cycle time: How long should each break cycle be?
- Time left: How much time is left for the current cycle (pomodoro or break) to end?
- State: Is the timer paused, running or has not started running yet? Let’s say that the states can be:
- 0: Initial state.
- 1: Paused.
- 2: Running a pomodoro cycle.
- 3: Running a break cycle.
- Display ID: ID for the DOM element in which the time left is displayed and updated.
- Message Display ID: Similar to the previous, we’ll ask for the ID on the element where we’ll show messages to the user.
Those six seem to be a good foundation for the properties that we should set for our clock, let’s start with the methods:
- Start: Starts the timer.
- Pause: Pauses the timer.
- Reset: Resets the timer to it’s original status and pauses it.
- Update times: Updates the values for the pomodoro cycle and break.
- Update display: I’ll be it a method that updates whatever element that we want updated with the new time.
- New state: We’ll change states using a method. This way, we can play sounds or alter the UI based on the current state.
Let’s start off with the constructor, then, we’ll add each method to the prototype. It may be confusing to those not familiar with JavaScripts prototypical inheritance, but it will make sense in time. If you want an in-depth look at how objects operate in JS, take a look at this post.
var Pomodoro = function(pomodoroTime, breakTime, displayElementId, messageDisplayId) { this.cycle = pomodoroTime * 60; // Pomodoro cycle in seconds. (We pass in minutes) this.break = breakTime * 60; // Break cycle in seconds.(We pass in minutes) this.state = 0; // Initial state. this.lastState = 2; // This additional property will be useful if we pause and want to go back to a break or pomodoro state. We set it to a pomodoro cycle state, since it will be the first that will run every time. this.timeLeft = pomodoroTime * 60; // Time left is initialized to be the same as a pomodoro cycle. this.timerDisplay = displayElementId; // ID for the DOM element where we want the time to be updated in. this.messageDisplay = messageDisplayId; // ID for the DOM element where we want to display messages for the user. }Now, we’ll start building up the methods I previously listed, let’s start with the start method (why would we do this? 😉 ). We need this method to do a few things. First, it should check the current state. It doesn’t make sense to start a timer that is already running. If the timer is currently stopped, we need to set the state to a pomodoro cycle (2) or break cycle (3) depending on the previous state -this will let us come back to a pomodoro/break cycle after we pause for example, that’s why we have a lastState property.
Then, it has to run a function that will decrease the time left every second and update the timer display accordingly (we’ll create the updateDisplay method later on), here’s what it looks like:
Pomodoro.prototype.start = function() { // START method. var self = this; // We create the variable self and have it be a reference to this. Why? Because inside the tick() function, "this" will refer to the function itself, and not the Pomodoro. So we'll use self instead of this. if (this.state === 0 || this.state === 1) { // If clock is stopped or paused: this.newState(this.lastState === 2 ? 2 : 3); // Set state to running pomodoro to a break or pomodoro state. We use the newState method (we'll write it later on) and check what the lastState value is in case we have paused before. tick(); // Tick function runs once (this is only done so that UI feedback is instantaneous and we don't need to wait for the interval). this.timer = setInterval(function() { // We create an interval that will run tick() every second. tick(); }, 1000); } // Notice the use of the self variable inside the function. function tick() { // The tick() function: self.timeLeft = self.timeLeft - 1; // It decreases the cycle time by one. self.updateDisplay(self.timeLeft); // Updates the display div that we passed in to the function (we'll create this method in just a second); if (self.timeLeft === 0) { // If time reaches zero, we start a pomodoro cycle or break cycle depending on the current state. self.timeLeft = self.state === 2 ? self.break : self.cycle; // We reset the timeLeft property to the next cycle. self.newState(self.state === 2 ? 3 : 2); // We change the state to the next cycle. } } }
We can get rid of the self variable by using apply, bind or call too, we’ll look at them in depth in an upcoming post.
Let’s write the pause method now; this method should check the current state and make sure that the clock is running first (for the same reason as above, it doesn’t make sense to pause a timer that is not running at all). Then, it should set the lastState property to the state before we paused (contained in state) and then, set current state to paused (1). Finally, we pause the actual timer by clearing the interval we created in the start method.
Pomodoro.prototype.pause = function() { // PAUSE method. if (this.state === 2 || this.state === 3) { // Make sure that the clock is running and not already paused. this.newState(1); // Set's current state to paused. clearInterval(this.timer); // Clears the interval we created previously. } }
We can start and we can pause, how about we teach our pomodoro how to reset to the initial state?
The reset method will simply set the current state to 0, lastState to 2 (since once reset, we’ll start with a new pomodoro cycle) and then update the timeLeft to it’s initial value (a whole pomodoro cycle), clear the timer (same as in the pause method) and update the display to show the initial values:
Pomodoro.prototype.reset = function() { // RESET method. this.newState(0); // Set the state to the initial state. this.timeLeft = this.cycle; // Set the timer to a pomodoro cycle. clearInterval(this.timer); // Clears the previous interval. this.updateDisplay(this.timeLeft); // Updates the display. }
We’ve been updating the display all the time, but we don’t actually have any logic that deals with it yet. That needs fixing. We’ll write the updateDisplay method next, unlike the other methods, it will take in two arguments, the time left for the current cycle and a message. This method will format the time and update the timer div element, and will show a message to the user:
Pomodoro.prototype.updateDisplay = function(time, message) { // UPDATE DISPLAY method. document.getElementById(this.timerDisplay).innerText = getFormattedTime(time); // We get the display element using it's ID and set the content to the time left. if (message) { // Displays a message to the user if there is one. document.getElementById(this.messageDisplay).innerText = message; } function getFormattedTime(seconds) { // This function formats the given seconds to look like: XX:YY var minsLeft = Math.floor(seconds / 60), secondsLeft = seconds - (minsLeft * 60); return zeroPad(minsLeft) + ':' + zeroPad(secondsLeft); function zeroPad(number) { return number < 10 ? '0' + number : number; } } }
The next method is updateTimes. This method is used when the user changes the cycle time for a pomodoro or break.
Pomodoro.prototype.updateTimes = function(cycleTime, breakTime) { // UPDATE TIMES method. We'll use this method to update the times when the user changes them in the UI. this.cycle = cycleTime * 60; // Set the pomodoro cycle. this.break = breakTime * 60; // Set the break cycle. this.reset(); // Reset the clock. }
This last method is very important. It will manage UI messages and sound feedback. This method will take a single argument, state and will act accordingly.
Pomodoro.prototype.newState = function(state) { this.lastState = this.state; // Set lastState to current state. this.state = state; // Updates current state. var message, audioFile; switch (state) { case 0: // If state is 0, set lastState to 2 and set message content and color. this.lastState = 2; console.info('New state set: Initial state.'); message = 'Click on play to start!'; document.getElementById('timer').style.color = '#E47143'; break; case 1: // If state is 1, set audio file to play, message content and color. console.info('New state set: Paused.'); audioFile = 'http://oringz.com/oringz-uploads/sounds-882-solemn.mp3'; message = 'Paused.'; document.getElementById('timer').style.color = '#CED073'; break; case 2: // If state is 2, set audio file to play, message content and color. console.info('New state set: Pomodoro Cycle.'); audioFile = 'http://oringz.com/oringz-uploads/sounds-766-graceful.mp3'; message = 'WORK WORK!'; document.getElementById('timer').style.color = '#C19AEA'; break; case 3: // If state is 3, set audio file to play, message content and color. console.info('New state set: Break cycle.'); audioFile = 'http://oringz.com/oringz-uploads/31_oringz-pack-nine-15.mp3'; message = 'Break time! Use your time wisely!'; document.getElementById('timer').style.color = '#53C56C'; } // If state is 1, 2 or 3, play audio file. if (state === 1 || state === 2 || state === 3) { var audio = new Audio(audioFile); audio.play(); } // Update display with current time and message. this.updateDisplay(this.timeLeft, message); }
Now that our pomodoro is a living, breathing piece of code, we need to create a way for the user to interact with it. We already created our basic layout with the buttons that we need, let’s add a few selectors for these buttons:
var elPomodoroTime = document.getElementById('pomodoro-time'), // Pomodoro cycle time display. elBreakTime = document.getElementById('break-time'), // Break cycle time display. elPomUp = document.getElementById('pomodoro-time-up'), // Increate pomodoro time button. elPomDown = document.getElementById('pomodoro-time-down'), // Decrease pomodoro time button. elBreakUp = document.getElementById('break-time-up'), // Increase break time button. elBreakDown = document.getElementById('break-time-down'), // Decrease break time button. elStart = document.getElementById('start'), // Start button. elPause = document.getElementById('pause'), // Pause button. elReset = document.getElementById('reset'); // Reset button.
There we go, now we need to have this buttons actually do things, but first, we need to somehow initialize our pomodoro clock:
var myPomodoro = new Pomodoro(25, 5, 'timer', 'message-display'); // We create a pomodoro using the constructor // above and set the pomodoro cycle to 25 mins, // break to 5 mins and provide the ID for the time // display (in this case, "timer");
Let’s add our event handlers after that:
elStart.addEventListener('click', function() { // Start button. myPomodoro.start(); // We call the start method. }); elPause.addEventListener('click', function() { // Pause button. myPomodoro.pause(); // We call the pause method. }); elReset.addEventListener('click', function() { // Reset button. myPomodoro.reset(); // We call the reset method. });
We now need to create the methods that will update the cycle times (by increasing pomodoro or break cycle time).
elPomUp.addEventListener('click', function() { // Increase pomodoro time button. elPomodoroTime.innerText = parseInt(elPomodoroTime.innerText) + 1; // We increase the pomodoro cycle value by one. myPomodoro.updateTimes(elPomodoroTime.innerText, elBreakTime.innerText); // Update pomodoro. }); elPomDown.addEventListener('click', function() { elPomodoroTime.innerText = parseInt(elPomodoroTime.innerText) === 1 ? 1 : parseInt(elPomodoroTime.innerText) - 1; // We decrease the pomodoro cycle value by one. Additionally, we check if the current time is 1, since we can't go any lower! myPomodoro.updateTimes(elPomodoroTime.innerText, elBreakTime.innerText); // Update pomodoro. }); elBreakUp.addEventListener('click', function() { elBreakTime.innerText = parseInt(elBreakTime.innerText) + 1; // We increase the break cycle value by one. myPomodoro.updateTimes(elPomodoroTime.innerText, elBreakTime.innerText); // Update pomodoro. }); elBreakDown.addEventListener('click', function() { elBreakTime.innerText = parseInt(elBreakTime.innerText) === 1 ? 1 : parseInt(elBreakTime.innerText) - 1; // We decrease the break cycle value by one. (And check for the lowest value! (1)) myPomodoro.updateTimes(elPomodoroTime.innerText, elBreakTime.innerText); // Update pomodoro. });
That should set you up for success… Not yet! There is a bug in the code, a use case that will cause some things to behave in unintended ways. See if you can find it, and fix it, that’ll be a nice warm-up.
You can check a live version of this clock here. Do not stop here though, you should still try to improve it. Adding a percentage display, showing more custom messages to the user as time goes on and setting up a nicer layout are just three examples!
Additionally, I’ve created the same application using AngularJS and React, since some of you may be interested in getting to learn one (or both) of these two frameworks at some point.
As always, you can email me, ping me on twitter and post below if you need help or guidance!