0

I was wondering if there is a nicer object oriented way of creating this timer? (without global vars!)

let secondsPassed = 0;
let timerId;

function startTimer() {
  clearInterval(timerId);
  timerId = setInterval(function() {
    const seconds = twoDigits((Math.floor(secondsPassed )) % 60);
    const minutes = twoDigits(Math.floor(secondsPassed / 60) % 60);
    const hours = Math.floor(secondsPassed / 60 / 60);
    $('#timer').text(`${hours}:${minutes}:${seconds}`);
    secondsPassed++;
  }, 1000);
  
  $(window).blur(function() {
      clearInterval(timerId) // stop timer when user leaves tab
  });
  
  $(window).focus(function() {
    startTimer(); // continue timer when user comes back
  });
}
Wim den Herder
  • 1,197
  • 9
  • 13
  • 1
    What exactly is Object Oriented in your example? – Roko C. Buljan Jan 26 '22 at 13:00
  • 1
    Well, yes – encapsulate the global variables into e.g. a class? – AKX Jan 26 '22 at 13:00
  • 1
    Is the goal to use classes/objects or is the goal to not use global variables? Those are two different goals. The latter can be accomplished by just wrapping the whole thing in an IIFE. – David Jan 26 '22 at 13:01
  • IIFE is nice, but I wanted this: `const timer = new Timer('#timer');` with the jquery focus inside it? Or otherwise you have to add `$(window).blur(() => timer.pause()); $(window).focus(() => timer.start());` – Wim den Herder Jan 26 '22 at 13:07
  • 1
    If you mean OOP, then you mean that you would like to initialize an object for the timer, like new Timer()? With start, reset and stop methods? This is of course possible. You just need to name a function "Timer" and add the methods by "this.reset = function() { }" and so on. Here is a good starting point for reading into the topic: https://appdividend.com/2019/05/22/javascript-class-example-how-to-use-class-in-javascript-tutorial/ – Kevin Glier Jan 26 '22 at 13:09
  • I added a kinda "blueprint" answer. All you have to do is using the Link above to implement your first Timer class. It should be an easy one and also be worth it to structure your future code the OOP way. Hope it helps! :) – Kevin Glier Jan 26 '22 at 13:21
  • 1
    Maybe something like this? https://jsfiddle.net/g94exqmb/ – Wim den Herder Jan 26 '22 at 14:02

2 Answers2

2

Your current implementation is actually wrong. Every time you call startTimer, it installs startTimer as a new window focus event handler, leading to multiple started intervals when you focus the window the second time; growing exponentially. The onfocus handler should only run the timerId = setInterval(…) line - put that in a nested helper function to call only that.

This also makes it unnecessary to declare the variables globally.

function createTimer() {
  let secondsPassed = 0;
  let timerId;
  function resume() {
    if (timerId) return; // prevent multiple intervals running at the same time
    timerId = setInterval(() => {
      const seconds = twoDigits((Math.floor(secondsPassed )) % 60);
      const minutes = twoDigits(Math.floor(secondsPassed / 60) % 60);
      const hours = Math.floor(secondsPassed / 60 / 60);
      $('#timer').text(`${hours}:${minutes}:${seconds}`);
      secondsPassed++;
    }, 1000);
  }
  function pause() {
    clearInterval(timerId);
    timerId = undefined;
  }

  $(window).blur(pause); // stop timer when user leaves tab
  $(window).focus(resume); // continue timer when user comes back

  resume(); // now start the timer
}

Now how to make that object-oriented? Just return an object from createTimer. Put resume and pause as methods on that object. Maybe add some more methods for starting, stopping, resetting, whatever you need. Maybe use a property on the object instead of the secondsPassed local variable. Or expose the local variable using a getter.

And to make it reusable, of course you can make createTimer accept arguments, from the selector of the output element, to the output element itself, to a callback function that will be called with the current time on every tick.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 4
    Btw, [using `setInterval` and `secondsPassed++` creates an inaccurate, drifting timer](https://stackoverflow.com/a/29972322/1048572). Better remember when the timer started, and get the difference to the current time. Pause and resume then only need to shift around the start date. This will also allow sub-second resolution, making it work even in the case when the window is repeatedly focused for only a split second. – Bergi Jan 26 '22 at 13:55
  • Supernice, `if (timerId) return;` is semantically better than always `clearInterval(timerId);` – Wim den Herder Jan 26 '22 at 14:34
  • What is wrong with my answer @WimdenHerder? This one here is not really OOP style. You could use the ES6-implementation as suggested by yourself and use it like I described it in my answer. The code I provided is actually like a controller. – Kevin Glier Jan 26 '22 at 14:39
  • Kevin, I made an implementation of the OOP style here: https://jsfiddle.net/g94exqmb/ – Wim den Herder Jan 26 '22 at 14:43
  • @KevinGlier Not sure what you mean by "*not really OOP style*". Would you want me to spell out the `return { resume, pause };` in the code? – Bergi Jan 26 '22 at 14:44
  • 1
    @WimdenHerder ... in addition to what *Bergi* said about *drifting timers* there is [example code which gives an idea of the inaccuracy](https://stackoverflow.com/questions/63976801/how-does-one-refactor-best-this-timer-stopwatch-code-base-towards-a-better-code) that comes with just interval based timer incrementation. – Peter Seliger Jan 26 '22 at 14:49
  • I would suppose to mean by OOP, that we have the possibility to create a new timer object and call its methods from outside (like in my example in the answer). Single responsibility principle means, that the Timer basically handles one thing: Counting seconds. You handle events of DOM in your code. It breaks the principle. And "new createTimer()" also doesn't sound that OOP :D – Kevin Glier Jan 26 '22 at 14:50
  • 1
    @KevinGlier I think it's fine if the timer also takes the responsibility for pausing while the window has no focus - that's what the OP asked for. And yes, the `$('#timer').text(…)` should be abstracted out, as suggested in the last paragraph. `createTimer` is a factory function that is not supposed to be called with `new`. You don't need `class` syntax for OOP. – Bergi Jan 26 '22 at 16:15
  • You can write `this.resume = function() { ...`, create an instance with `const t = new createTimer()` and `t.resume()` is accessible now – Wim den Herder Jan 27 '22 at 06:43
  • @WimdenHerder You could, but `class Timer { … }` is a more appropriate syntax for that. As suggested, a `createTimer` factory function should `return { resume, pause };`. – Bergi Jan 27 '22 at 14:11
-1

Edit: With this answer, you have to implement the Timer class yourself first. The code only shows how you could name the methods of the timer, how you create the instance and call its functions. The timer should (principle "separation of concerns") only handle the counting and provide the functionalities needed, like starting and stopping.

If you want to have an OOP solution for your timer, you shouldn't let the Timer class know the ID of the DOM container (like one of your comments to your question suggested).

You should read into the topic using this: https://appdividend.com/2019/05/22/javascript-class-example-how-to-use-class-in-javascript-tutorial/

Let us assume, that you already implemented the class. Your code above should look like the following:

// Create own scope for the function, so that variable are not assigned to windows-object.
(function() {
    let secondsPassed = 0;

    let timer = new Timer();
    // events, if necessary
    timer.onTick((seconds) => { secondsPassed = seconds });
    timer.onStop(() => { secondsPassed = 0; })

    // Called by a button
    function startTimer() {
        timer.start();
    }

    // Example: Display alert with current timer seconds on click
    function displaySecondsOfTimer() {
        alert(timer.getSeconds());
    }

    $(window).blur(function() {
        timer.stop(); // stop timer when user leaves tab
    });

    $(window).focus(function() {
        timer.start(); // continue timer when user comes back
    });
})();

So I think, you have a good example to code your first Timer class in native JavaScript! :)

Kevin Glier
  • 1,346
  • 2
  • 14
  • 30
  • OP asked how to do it without global variables like `secondsPassed`. Also it's not really clear how/where `displaySecondsOfTimer` would be called. – Bergi Jan 26 '22 at 14:42
  • Okay, you're right. I changed the code a little bit now, so that the code is within its own scope so that variables are not assigned to the window-object. The body of the function is more like a controller, where the timer and events of the page can be handled. – Kevin Glier Jan 26 '22 at 14:47
  • Hm, the problem isn't so much that the variable was global, but that the user of the `timer` object has to keep around an extra variable at all. What is it even used for? – Bergi Jan 26 '22 at 16:18
  • @Bergi This is just an example. You can remove the window.events and also the secondPasses variable. It is an example of an controller-like function. The focus was just to show how the Timer class could look like from outside and how it could be worked with. This wasn't meant to be a complete answer. – Kevin Glier Jan 28 '22 at 11:20