123

I know this this has been asked in numerous posts but honestly I don't get them. I am new to JavaScript, Chrome Extensions and everything and I have this class assignment. So I need to make a plugin that would count DOM objects on any given page using Cross Domain Requests. I've been able to achieve this so far using Chrome Extension API's. Now the problem is I need to show the data on my popup.html page from the contentScript.js file. I don't know how to do that I've tried reading the documentation but messaging in chrome I just can't understand what to do.

following is the code so far.

manifest.json

{
"manifest_version":2,

"name":"Dom Reader",
"description":"Counts Dom Objects",
"version":"1.0",

"page_action": {
    "default_icon":"icon.png",
    "default_title":"Dom Reader",
    "default_popup":"popup.html"
},

"background":{
    "scripts":["eventPage.js"],
    "persistent":false
},

"content_scripts":[
    {
        "matches":["http://pluralsight.com/training/Courses/*", "http://pluralsight.com/training/Authors/Details/*",                                          "https://www.youtube.com/user/*", "https://sites.google.com/site/*", "http://127.0.0.1:3667/popup.html"],
        "js":["domReader_cs.js","jquery-1.10.2.js"]
        //"css":["pluralsight_cs.css"]
    }
],

"permissions":[
    "tabs",
    "http://pluralsight.com/*",
    "http://youtube.com/*",
    "https://sites.google.com/*",
    "http://127.0.0.1:3667/*"
]

popup.html

<!doctype html>
<html>

    <title> Dom Reader </title>    
    <script src="jquery-1.10.2.js" type="text/javascript"></script>
    <script src="popup.js" type="text/javascript"></script>

<body>
    <H1> Dom Reader </H1>
    <input type="submit" id="readDom" value="Read DOM Objects" />

   <div id="domInfo">

    </div>
</body>
</html>

eventPage.js

var value1,value2,value3;

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.action == "show") {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
        chrome.pageAction.show(tabs[0].id);
    });
}

value1 = request.tElements;
});

popup.js

$(function (){
$('#readDom').click(function(){
chrome.tabs.query({active: true, currentWindow: true}, function (tabs){
    chrome.tabs.sendMessage(tabs[0].id, {action: "readDom"});

 });
});
});

contentScript

var totalElements;
var inputFields;
var buttonElement;

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse){
if(request.action == "readDom"){

    totalElements = $("*").length;
    inputFields = $("input").length;
    buttonElement = $("button").length;


}
})

chrome.runtime.sendMessage({ 
action: "show", 
tElements: totalElements, 
Ifields: inputFields, 
bElements: buttonElement 

});

Any help would be appreciated and please avoid any noobness I did :)

Sumair Baloch
  • 1,245
  • 2
  • 9
  • 6

3 Answers3

209

Although you are definitely in the right direction (and actually pretty close to the end), there are several (imo) bad practises in your code (e.g. injecting a whole library (jquery) for such a trivial task, declaring unnecessary permissions, making superflous calls to API methods etc).
I did not test your code myself, but from a quick overview I believe that correcting the following could result in a working solution (although not very close to optimal):

  1. In manifest.json: Change the order of the content scripts, putting jquery first. According to the relevant docs:

"js" [...] The list of JavaScript files to be injected into matching pages. These are injected in the order they appear in this array.

(emphasis mine)

  1. In contentscript.js: Move the chrome.runtime.sendMessage({...}) block inside the onMessage listener callback.

That said, here is my proposed approach:

Control flow:

  1. A content script is injected into each page matching some criteria.
  2. Once injected, the content scripts send a message to the event page (a.k.a. non-persistent background page) and the event page attaches a page-action to the tab.
  3. As soon as the page-action popup is loaded, it sends a message to the content script, asking for the info it needs.
  4. The content script processes the request, and responds so the page-action popup can display the info.

Directory structure:

          root-directory/
           |__img
               |__icon19.png
               |__icon38.png
           |__manifest.json
           |__background.js
           |__content.js
           |__popup.js
           |__popup.html

manifest.json:

{
  "manifest_version": 2,
  "name": "Test Extension",
  "version": "0.0",
  "offline_enabled": true,

  "background": {
    "persistent": false,
    "scripts": ["background.js"]
  },

  "content_scripts": [{
    "matches": ["*://*.stackoverflow.com/*"],
    "js": ["content.js"],
    "run_at": "document_idle",
    "all_frames": false
  }],

  "page_action": {
    "default_title": "Test Extension",
    //"default_icon": {
    //  "19": "img/icon19.png",
    //  "38": "img/icon38.png"
    //},
    "default_popup": "popup.html"
  }

  // No special permissions required...
  //"permissions": []
}

background.js:

chrome.runtime.onMessage.addListener((msg, sender) => {
  // First, validate the message's structure.
  if ((msg.from === 'content') && (msg.subject === 'showPageAction')) {
    // Enable the page-action for the requesting tab.
    chrome.pageAction.show(sender.tab.id);
  }
});

content.js:

// Inform the background page that 
// this tab should have a page-action.
chrome.runtime.sendMessage({
  from: 'content',
  subject: 'showPageAction',
});

// Listen for messages from the popup.
chrome.runtime.onMessage.addListener((msg, sender, response) => {
  // First, validate the message's structure.
  if ((msg.from === 'popup') && (msg.subject === 'DOMInfo')) {
    // Collect the necessary data. 
    // (For your specific requirements `document.querySelectorAll(...)`
    //  should be equivalent to jquery's `$(...)`.)
    var domInfo = {
      total: document.querySelectorAll('*').length,
      inputs: document.querySelectorAll('input').length,
      buttons: document.querySelectorAll('button').length,
    };

    // Directly respond to the sender (popup), 
    // through the specified callback.
    response(domInfo);
  }
});

popup.js:

// Update the relevant fields with the new data.
const setDOMInfo = info => {
  document.getElementById('total').textContent = info.total;
  document.getElementById('inputs').textContent = info.inputs;
  document.getElementById('buttons').textContent = info.buttons;
};

// Once the DOM is ready...
window.addEventListener('DOMContentLoaded', () => {
  // ...query for the active tab...
  chrome.tabs.query({
    active: true,
    currentWindow: true
  }, tabs => {
    // ...and send a request for the DOM info...
    chrome.tabs.sendMessage(
        tabs[0].id,
        {from: 'popup', subject: 'DOMInfo'},
        // ...also specifying a callback to be called 
        //    from the receiving end (content script).
        setDOMInfo);
  });
});

popup.html:

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="popup.js"></script>
  </head>
  <body>
    <h3 style="font-weight:bold; text-align:center;">DOM Info</h3>
    <table border="1" cellpadding="3" style="border-collapse:collapse;">
      <tr>
        <td nowrap>Total number of elements:</td>
        <td align="right"><span id="total">N/A</span></td>
      </tr>
      <tr>
        <td nowrap>Number of input elements:</td>
        <td align="right"><span id="inputs">N/A</span></td>
      </tr>
      <tr>
        <td nowrap>Number of button elements:</td>
        <td align="right"><span id="buttons">N/A</span></td>
      </tr>
    </table>
  </body>
</html>
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • hello, Sorry I haven't been on for some time, just checked on your comment. I can't accept any answer. it says you need 15 reputation points to do so. – Sumair Baloch Nov 30 '13 at 13:04
  • Would you mind helping me solve a very similar problem? Here My current shot at this is pretty much your code from this answer. But I still cant get it to work and cant seem to find any good help on the matter . – wuno Dec 26 '15 at 02:37
  • why you used `chrome.tabs.sendMessage` in popupjs and `chrome.runtime.onMessage.addListener` in content.js why `.tabs` for popupjs and `.runtime` in content.js – Habib Kazemi Jan 22 '16 at 20:38
  • 2
    @hkm: Because that is how you send and receive messages. It's all in [the docs](https://developer.chrome.com/extensions/tabs#method-sendMessage). – gkalpak Jan 23 '16 at 09:46
  • So one thing - does the background.js/content.js parse the DOM before you click the extension? It seems to me that it does. I want to do the same thing here - but only parse the DOM if the user clicks the extension - but I still want the extension to be grayed out when not at the correct site. Would that require some extra message passing - or does your code already do this? It seems that the popup.js will send a message any time the DOM is loaded - but really I only want popup.js to send a message when the user clicks. – DanGordon Jun 30 '16 at 22:28
  • The popup didn't came up in my case! I need help :D – Farbod Aprin Dec 10 '19 at 13:50
  • I am doing something similar to this except I have an extra listener for a button click event. Instead of using the information pulled from the page to change HTML text on the popup I am using it to make an API call when I press a button in the popup. In popup.js I am trying to use ```const setDOMinfo``` to store the text of ```info.total``` so I can send it when my 'click' event listener is activated. My problem is that info.total is undefined. Likely because it is not connected to the response in content.js. How should I connect them or otherwise pass the info from content.js to popup.js? – Zach Morris Aug 11 '22 at 22:54
  • @gkalpak I am running exactly your code, but getting error in popup inspect, looks like unable to interact with content ```Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.``` – Sayan Dey Aug 29 '23 at 13:54
12

You can use localStorage for that. You can store any data in a hash table format in browser memory and then access it any time. I'm not sure if we can access localStorage from the content script (it was blocked before), try to do it by yourself. Here's how to do it through you background page (I pass data from content script to background page first, then save it in localStorage):

in contentScript.js:

chrome.runtime.sendMessage({
  total_elements: totalElements // or whatever you want to send
});

in eventPage.js (your background page):

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse){
       localStorage["total_elements"] = request.total_elements;
    }
);

Then you can access that variable in popup.js with localStorage["total_elements"].

Maybe you can access localStorage directly from the content script in modern browsers. Then you don't need to pass the data through your background page.

Nice reading about localStorage: http://diveintohtml5.info/storage.html

Gangula
  • 5,193
  • 4
  • 30
  • 59
gthacoder
  • 2,213
  • 3
  • 15
  • 17
  • 2
    Please, do not promote the use of the deprecated **chrome.extension.onRequest/sendRequest** (which btw do not load a non-persistent background page before executing). Use **chrome.runtime.\*** instead. – gkalpak Nov 16 '13 at 18:17
  • 4
    @ExpertSystem Please, if you see such outdated information, suggest/make an edit. – Xan Jun 12 '14 at 07:03
  • 4
    @Xan: So, you are the successor in [Google-Chrome-Extension] :) I don't like editing people's posts (I find it too intrusive). Besides, with so many outdated tutorials and examples (at least back then), I found it better to have an explicit comment on why it is **bad** to use `.extension.xxx`, instead of just showing the right way (which some people might think of as an "equally OK alternative"). It's more a matter of style I guess. Keep up the good work ! – gkalpak Jun 12 '14 at 08:08
  • Using `localStorage` or `storage.sync` is a good option to store persistent data, but if you want to update the DOM for your popup.html for every tab, then I suggest using [gkalpak's answer](https://stackoverflow.com/a/20023723/6908282), which uses the `setDOMInfo ` callback to update the DOM in pupup – Gangula Aug 17 '22 at 07:59
5

I will give you a simple solution

The question is how to send message from content script to popup and not popup to content script

In popup you have to send message from popup

document.addEventListener('DOMContentLoaded', function() {
        chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
            chrome.tabs.sendMessage(tabs[0].id, {type:"msg_from_popup"}, function(response){
            alert(response)
            });
        });
    })

In content script you have to add event listener but the catch is you have to return true this means you can use sendResponse asynchronously if you dont mention return true it will work synchronously you have to immediately return sendResponse message

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
     
        if(request["type"] == 'msg_from_popup'){
            console.log("msg receive from popup");
            
        sendResponse("msg received and sending back reply");// this is how you send message to popup
           
        }
        return true; // this make sure sendResponse will work asynchronously
        
    }
);
pguardiario
  • 53,827
  • 19
  • 119
  • 159
Shersha Fn
  • 1,511
  • 3
  • 26
  • 34