Pass event listener from popup.js to background.js script

Hi,

I’m working on a simple time tracker extension. When I started out I had all the js code in one popup.js file, but I found that when the extension pop-up was closed the timer stopped running. I came across the use of a background script to use in junction with the already created popup.js script of mine. What I’ve now done is that I have the evenListener logic from the HTML page in the popup.js, I then use the browser.runtime.sendMessage to send the data to the background.js script to handle the timer. Is this a correct setup or should I do it differently?

I can’t figure out how to pass the click event from the post to the background script, and then use the eventListener in the background script to start the timer.

Here is my public GitHub repo for the timetracker extension. https://github.com/krullmizter/timetracker

In your notifyBackgroundPage() function in the popup.js file, you don’t need to send the HTML button to the background script. Instead, you’d probably want something like this:

const sending = browser.runtime.sendMessage({
    action: startBtn.id
});
sending.then(handleRes, handleErr);

That will send the background script an object that has a message object with “start” as the action. Then, in your background script, you can use a switch statement (or if/else statements if you really want to) to call the appropriate function.

The click event listeners need to all be in the popup.js file and each onclick should send a message to the background to perform the desired action.

So your background script would be become something like this:

function popupMsgReceived(msg, sender, sendRes) {
    console.log("Msg received", msg);

    switch (msg.action) {
        case "start":
            handleStart();
            break;
        case "stop":
            handleStop();
            break;
        case "reset":
            handleReset();
            break;
    }

    sendRes({response: "Response from background script"});
}

browser.runtime.onMessage.addListener(popupMsgReceived);

// Set the initial clock values
let hr  = 0;
let min = 0;
let sec = 0;
 
let interval;

/* 
*  Three even listeners for the three buttons
*  The startBtn event will set an interval to run each 1000ms (1sec) and run the startTimer function
*  timerStartedAt function will also run och the btn event
*/
function handleStart() {
    timerStartedAt();
    clearInterval(interval);
    interval = setInterval(startTimer, 1000);
}

function handleStop() {
    clearInterval(interval);
}

function handleReset() {
    clearInterval(interval);
    hr  = 0;
    min = 0;
    sec = 0;
    timer.innerHTML = '00:00:00';
    displayDate.innerHTML = ''
}

As far as for setting the timer, I’d recommend using the Alarms API instead of using Javascript timeouts or intervals. Alarms are a lot more stable for browser add-ons. Another be positive to that is that you could create and edit alarms from the popup.js without the need to send messages and the alarm will stay alive even after the popup page is closed.

Another thing to think of future-wise is that manifest version 3 will eventually be the standard in Firefox. That version of the API does not have persistent background pages. Therefore, Javascript timeouts and intervals won’t work in background scripts at that point and you will need to change your code later.

Thank your for the answer! I had some questions regarding the updating of the popup DOM. For example timer.innerHTML = '00:00:00'; how could I update the popup DOM within the backend.js script, or should I do that in the popup.js script? If I do the DOM updating from the popup.js then what do I do in the backend.js then?

Firstly, you should avoid using innerHTML because the add-on might get rejected if you try to upload it to the Firefox AMO website. Instead, use textContent.

As for your question, the background script can’t directly update the DOM of your popup. You could do this by sending messages from the background script to the popup script.

That said, one of the big benefits of using the Alarms API instead of Javascript timeouts/intervals is that your popup should be able to access the alarms that you created, even if they were made by the background script. So for your popup page to see what alarms you have running, it doesn’t need to ask the background script for anything.

For example, if you wanted to show the live timer of your script, you could load the list of alarms when your popup loads and then just use Javascript intervals on the popup page to keep the timer ticking in the UI and then register a listener on the popup page for when an alarm is fired, so that the popup can remove it from the UI.

Updating the popup UI using the background script is doable using messages, but in my experience it can get pretty complex.

As a side note, if you are sending a lot of messages between the popup and background script, sometimes it’s a good idea to use port-based messaging instead of just the regular runtime messaging APIs.

Ok thanks for the input. I will try to create the alarms in the background.js script with the Alarm API, and then update the UI in the popup.js. But with the Alarms API one only gets to use the periodInMinutes as the timeframe, if I want it in seconds that can’t be done with it?

function handleStart() {
    //timerStartedAt();
    browser.alarms.onAlarm.addListener(timerStartedAt);
    browser.alarms.create('timerStartedAt', {
        periodInMinutes: 1
    });
}

The Alarms API should be able to accept a decimal value for the periodInMinutes value. If you divide the number of seconds by 60, that should work.

Just to clarify. I can run the Alarms API in the background.js file to run every second, it then runs my timer functions startTimer() witch calculates the sec, min and hours. But should the startTimer() be inside the background.js or the popup.js script? In essence should the core time tracker function startTimer() and the Alarm both be inside the background.js, or should JUST the alarm be in the background.js, and then the popup can check the alarm and calculate run the startTimer() from the popup?

I’m not sure Alarms can fire every second, at least in Chrome when you release the addon, it’s capped at 1 per minute. That’s why alarms has minute-granularity: alarms.create() - Mozilla | MDN

Also, why does it need to run every second? Sounds like a bad design. Unless it’s for an UI element showing “seconds” :slight_smile:.

Also - alarms are global:

Alarms do not persist across browser sessions. They are created globally across all contexts of a single extension. E.g. alarm created in background script will fire onAlarm event in background script, options page, popup page and extension tabs (and vice versa).

And lastly - background script is running all the time, so put there things that should run all the time. Popup runs when it’s opened, so all UI related things should be in the popup code.
See also: Anatomy of an extension - Mozilla | MDN

Indeed, the alarm would be targeting the “final” time, while you’d have a normal setInterval/Timeout in your popup script to keep updating a live timer display. That way you have both a persistent way to track the remaining time and a real time remaining display when needed

Thanks all for the info. What I want is to simply have a popup with a start, stop and reset. When the user presses the start button and even closes the popup a timer should begin to count in the popup. I now have a working console.log solution of this on the development branch of my GitHub. Here I can now install the extension as a temporary one, and open the popup and have the timer tick in the console. What I now would want is to change the DOM of the popup to show the timer ticking. https://github.com/krullmizter/timetracker/tree/development any ideas of this being a good solution, and how to reflect the timer in the backend to the popup DOM?

should I just do it with a setInterval, or is the Alarms API needed to do this task?

@freaktechnik @juraj.masiar @

When user starts the timer, all you need is store the current time - Date.now().
And the popup UI will simply subtract current time from the stored one and draw the result.
Pausing and resuming will be more challenging, but still doable. You can simply store time of each pause/resume event and then calculate the total time from all the values. And stopping the timer will remove all values from the store.
You don’t even need a background script at all.

That seems like a timer, I’m in the process of creating sort of a stopwatch that starts from 00:00:00 and counts upwards for every second. This I’ve acchived in the background.js due to that it needs to run even when the popup get closed. I now struggle with updating the popup DOM with the timer data (secs, mins, and hours) that is happening in the background.js script.

What I wrote is exactly the stopwatch.
Let me clarify:

  1. user pres Start - save Date.now() - 1642707219556
  2. save the value in store
  3. close the popup - value is still in the store
  4. open popup, read the value from the store - 1642707219556
  5. get current time - 1642707287994
  6. calculate diff - 1642707287994 - 1642707219556 = 68_438 (68 seconds)
  7. display 68 seconds (you can recalculate and redraw every second)
2 Likes

Thanks for the clarification it really helped!

I’ve now managed to create a stopwatch in the popup.js that calculates the Date.now() - the stored initial start time in localStorage.

What I now have problems with is to handle the timer after it has been started and the popup is closed. The timer logic seems to go on when one closes the popup, after the timer has been started, but the UI doesn’t update when one opens the popup again, only after pressing start again does it show the timer value.

I also find the start of the timer to be slow, like 1 - 2 seconds slow.

Here is the current popup.js code. https://github.com/krullmizter/timetracker/blob/development/popup/popup.js

Thanks again for the help!

You should be able to debug these things. But since popup debugging is pretty complicated and buggy, it’s best to open the popup in a new tab - as normal web page.

To do that, open “about:debugging#/runtime/this-firefox” page and click on the “Manifest URL”. Then change the URL in the addressbar by replacing “manifest.json” with “popup/popup.html”.

Then you can open developer console and see what is happening. Use console.log if needed, or debugger statement.

Also, before you start coding, you should take a pen a paper and draw the architecture you are going to implement. Think about all the use cases, and try to write a simple process diagram that clearly shows what actions should happen after what action.

Also, when using localStorage, it’s best to wrap “save” operations with JSON.stringify and “read” operations with JSON.parse - that way if you save number, you will get back number, not a string. Plus you can then store objects and arrays as well.

Thanks for all the help, it’s been really interesting! I have now worked on the code a bit more.

An issue I now have is that when the pause button gets pressed the UI timer stops, but somehow the interval keeps on counting somehow in the “background”, no console.log catches this when the timer is paused. So now one can start, then pause the timer, and if you press the start button again the timer “jumps” in time to the counted time it has counted in the “background”.

There is also a similar issue with the UI not updating when a user starts a timer, then closes the popup and opens it again. The same happens as above, the UI stops to update, but the interval seems to keep on counting even when I call clearInterval() it doesn’t stop the timer counting in the “background”…

Here is the current code issue: https://github.com/krullmizter/timetracker/blob/development/popup/popup.js#L28-L37 as it explains it should stop the interval in the else statement. The intervalController gets ran in the pause button click event listener here: https://github.com/krullmizter/timetracker/blob/development/popup/popup.js#L81-L84

There is no “counting in the background”. You are simply computing time difference between now and the starting time. This is a correct way, this is how all timers works.

To implement pause feature, you need to know when was the pause / resume clicked - this way you can then calculate total time by summing the individual intervals.
For example, imagine you click start, wait 4 seconds, then pause, then wait 2 seconds and then resume and then after 3 seconds stop. It would look like this:

// imagine each "-" is 1 second:
start - - - - pause - - resume - - - stop

How long was it running? You can compute it as a sum of difference between all start (or resume) and pause (or stop) events.
So in this case it’s:
(pause - start) + (stop - resume) = 7 seconds

So what you need to do is instead of storing a single value when the time started, store an array of all these events.
So for the example above, after pressing start, you would store:

[
  {start: 1643100032706, },
]

Then after pressing pause, you would update it with stop time:

[
  {start: 1643100032706, stop: 1643100034706},
]

Then after pressing resume, you would add a new item:

[
  {start: 1643100032706, stop: 1643100036706},
  {start: 1643100038706, },
]

And then after stop you would update the last item again:

[
  {start: 1643100032706, stop: 1643100036706},
  {start: 1643100038706, stop: 1643100041706},
]

And to compute total time you simply iterate all items in the array and sum all diffs. Piece of cake :slight_smile:.

EDIT:
PS: code to sum all diffs:

[
  {start: 1643100032706, stop: 1643100036706},
  {start: 1643100038706, stop: 1643100041706},
].reduce((acc, x) => acc + (x.stop - x.start), 0)    // returns 7000
// although, if the "x.stop" can be missing (for the last event), use `acc + ((x.stop || Date.now()) - x.start)`