Pass a variable from content script to popup
I'm messing around (trying to learn) how to make a chrome extension. Right now I'm just making super simple one where it counts instances of a certain word on a page. I have this part working.
What I want to do is send this information to the pop so I can use it to do some other stuff.
Here is what I have so far:
manifest.json
{
"manifest_version": 2,
"name": "WeedKiller",
"description": "Totally serious $100% legit extension",
"version": "0.1",
"background": {
"persistent": false,
"scripts": ["background.js"]
},
"permissions":[
"tabs",
"storage"
],
"browser_action": {
"default_icon": "icon.png",
"default_title": "WeedKiller",
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"content.js"
],
"run_at": "document_end"
}
]
}
content.js
var elements = document.getElementsByTagName('*');
var count = 0;
function tokeCounter(){
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
for (var j = 0; j < element.childNodes.length; j++) {
var node = element.childNodes[j];
if (node.nodeType === 3) {
var text = node.nodeValue;
if(text == '420'){
count++;
}
var replacedText = text.replace(/420/, '+1');
if (replacedText !== text) {
element.replaceChild(document.createTextNode(replacedText), node);
}
}
}
}
}
tokeCounter();
So what I want to happen is to send the count
variable to the popup so that I can use it there.
I have looked around and found that I need to do something with chrome.runtime.sendMessage
.
I have it so I add this line to the end of content.js:
chrome.runtime.sendMessage(count);
and then in background.js:
chrome.runtime.onMessage.addListener(
function(response, sender, sendResponse){
temp = response;
}
);
I'm sort of stuck here as I'm not sure how to send this information to popup and use it.
As you have properly noticed, you can't send data directly to the popup when it's closed. So, you're sending data to the background page.
Then, when you open the popup, you want the data there. So, what are the options?
Please note: this answer will give bad advice first, and then improve on it. Since OP is learning, it's important to show the thought process and the roadbumps.
First solution that comes to mind is the following: ask the background page, using Messaging again. Early warning: this will not work or work poorly
First off, establish that there can be different types of messages. Modifying your current messaging code:
// content.js
chrome.runtime.sendMessage({type: "setCount", count: count});
// background.js
chrome.runtime.onMessage.addListener(
function(message, sender, sendResponse) {
switch(message.type) {
case "setCount":
temp = message.count;
break;
default:
console.error("Unrecognised message: ", message);
}
}
);
And now, you could in theory ask that in the popup:
// popup.js
chrome.runtime.sendMessage({type: "getCount"}, function(count) {
if(typeof count == "undefined") {
// That's kind of bad
} else {
// Use count
}
});
// background.js
chrome.runtime.onMessage.addListener(
function(message, sender, sendResponse) {
switch(message.type) {
case "setCount":
temp = message.count;
break;
case "getCount":
sendResponse(temp);
break;
default:
console.error("Unrecognised message: ", message);
}
}
);
Now, what are the problems with this?
-
What's the lifetime of
temp
? You have explicitly stated"persistent": false
in your manifest. As a result, the background page can be unloaded at any time, wiping state such astemp
.You could fix it with
"persistent": true
, but keep reading. -
Which tab's count do you expect to see?
temp
will have the last data written to it, which may very well not be the current tab.You could fix it with keeping tabs (see what I did there?) on which tab sent the data, e.g. by using:
// background.js /* ... */ case "setCount": temp[sender.tab.id] = message.count; break; case "getCount": sendResponse(temp[message.id]); break; // popup.js chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { // tabs is a single-element array after this filtering chrome.runtime.sendMessage({type: "getCount", id: tabs[0].id}, function(count) { /* ... */ }); });
It's a lot of work though, isn't it? This solution works fine though for non-tab-specific data, after fixing 1.
Next improvement to consider: do we need the background page to store the result for us? After all, chrome.storage
is a thing; it's a persistent storage that all extension scripts (including content scripts) can access.
This cuts the background (and Messaging) out of the picture:
// content.js
chrome.storage.local.set({count: count});
// popup.js
chrome.storage.local.get("count", function(data) {
if(typeof data.count == "undefined") {
// That's kind of bad
} else {
// Use data.count
}
});
This looks cleaner, and completely bypasses problem 1 from above, but problem 2 gets trickier. You can't directly set/read something like count[id]
in the storage, you'll need to read count
out, modify it and write it back. It can get slow and messy.
Add to that that content scripts are not really aware of their tab ID; you'll need to message background just to learn it. Ugh. Not pretty. Again, this is a great solution for non-tab-specific data.
Then the next question to ask: why do we even need a central location to store the (tab-specific) result? The content script's lifetime is the page's lifetime. You can ask the content script directly at any point. Including from the popup.
Wait, wait, didn't you say at the very top you can't send data to the popup? Well, yes, kinda: when you don't know if it's there listening. But if the popup asks, then it must be ready to get a response, no?
So, let's reverse the content script logic. Instead of immediately sending the data, wait and listen for requests:
chrome.runtime.onMessage.addListener(
function(message, sender, sendResponse) {
switch(message.type) {
case "getCount":
sendResponse(count);
break;
default:
console.error("Unrecognised message: ", message);
}
}
);
Then, in the popup, we need to query the tab that contains the content script. It's a different messaging function, and we have to specify the tab ID.
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {type: "getCount"}, function(count) {
/* ... */
});
});
Now that's much cleaner. Problem 2 is solved: we query the tab we want to hear from. Problem 1 seems to be solved: as long as a script counted what we need, it can answer.
Do note, as a final complication, that content scripts are not always injected when you expect them to: they only start to activate on navigation after the extension was (re)loaded. Here's an answer explaining that in great detail. It can be worked around if you wish, but for now just a code path for it:
function(count) {
if(typeof count == "undefined") {
// That's kind of bad
if(chrome.runtime.lastError) {
// We couldn't talk to the content script, probably it's not there
}
} else {
// Use count
}
}