27

I attempted to create a custom element in a Chrome extension content script but customElements.define is null.

customElements.define('customElement', class extends HTMLElement {
    constructor() {
        super();
    }
    ...
});

So apparently Chrome doesn't want content scripts to create custom elements. But why? Is it a security risk?

I can't seem to find anything in Chrome's extension guide that says it's not allowed.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Jerinaw
  • 5,260
  • 7
  • 41
  • 54
  • Does it work on regular pages? – Daniel Herr Mar 15 '17 at 05:27
  • 5
    Related: https://crbug.com/273126, https://crbug.com/390807 – wOxxOm Mar 15 '17 at 10:07
  • @DanielHerr Yes, I actually build the element in a "test" page. It was easier to debug without having to reload the extension. When I moved it all into the extension it errored out. – Jerinaw Mar 15 '17 at 11:27
  • 3
    I agree this is an oversight. Extensions need protection from the host page CSS. Shadow DOM is the way to do that, but encapsulating the Shadow DOM in a custom element just seems like good programming practice. It's been a question since 2014: https://bugs.chromium.org/p/chromium/issues/detail?id=390807 I assume the developers have some tricky math to reconcile extensions with custom elements, so if it hasn't happened by now, it's not going to happen. – NewEndian Apr 15 '17 at 16:00

4 Answers4

5

I found the solution reading this page but the information was so cumbersome I wanted to write this answer for future readers (I am using Manifest v3)

Firstly, install the polyfill :

npm install @webcomponents/webcomponentsjs -D

Then add the polyfill in your content_scripts block in your manifest file :

"content_scripts": [{
  "matches": [ "..." ],
  "js": [
    "./node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js",
    "content.js"
  ]
}]

(important: you have to load it before your content script of course as the polyfill needs to load before you can use it)

Now it should works. Cheers

Note: the customElements feature is implemented in most modern browsers but for some reasons the interface is not available from a content script because the scripts are run in an isolated environment (not sharing the same window object space from the webpage the extension runs in).

vdegenne
  • 12,272
  • 14
  • 80
  • 106
  • I'm getting an error from the polyfill ```Cannot read properties of null (reading 'insertBefore')``` Did you have to add anything else? – Joey Grisafe Jan 23 '23 at 19:18
2

As of now custom element can be used in chrome extensions UI. In Popup ui, option page ui and in the content script as well But it requires a polyfill which is this. https://github.com/GoogleChromeLabs/ProjectVisBug - this is the one big custom element in the chrome extension.

fxnoob
  • 166
  • 8
  • 7
    I still get `Uncaught TypeError: Cannot read property 'define' of null` when calling `window.customElements.define` in a script injected with `chrome.tabs.executeScript` – Tom Apr 11 '20 at 18:10
  • @Tom need to include a polyfill https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs – fxnoob Sep 16 '20 at 17:24
2

After testing, I found that in content scripts, only the customElements object is missing, while other objects can be used. For example: Shadow DOM, HTML Templates, CSS Shadow Parts, CSS variables.

My test results:

console.log('customElements', window.customElements);  // null
console.log('Shadow DOM', Element.prototype.attachShadow); // yes
console.log('HTML Templates', document.createElement('template').content); // yes

const style = document.createElement('style');
style.textContent = ':root::part(test) {}';
document.head.appendChild(style);
console.log('CSS Shadow Parts', !!style.sheet!.cssRules); // yes

const style2 = document.createElement('style');
style2.textContent = ':root { --test: 0; }';
document.head.appendChild(style);
console.log('CSS variables', !!style.sheet!.cssRules.length); // yes

enter image description here

Therefore, there is no need to use a bloated webcomponentsjs polyfill, but instead use the lightweight custom-elements polyfill.

https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements

When using the custom-elements polyfill, it is also necessary to use lit/polyfill-support.js. The purpose of lit/polyfill-support.js is to connect Lit with the polyfill.

Pay attention to the import order!

import '@webcomponents/custom-elements';
import 'lit/polyfill-support.js';

https://lit.dev/docs/tools/requirements/#polyfills

weiya ou
  • 2,730
  • 1
  • 16
  • 24
0

You don't need any polyfills. I thank @fxnoob for pointing me to ProjectVisBug, but it doesn't use a polyfill.

Instead of using customElements.define in the main content script that is loaded with chrome.tabs.executeScript, VisBug uses that script as merely a wrapper to inject a <script> tag. This tag loads the main JavaScript bundle and the custom element is defined there.

manifest.json:

{
    "manifest_version": 2,
    "version": "0.0.1",
    "name": "My Extension",
    "permissions": ["activeTab"],
    "background": {
        "scripts": ["background.js"]
    },
    "browser_action": {
        "default_title": "Click to toggle"
    },
    "web_accessible_resources": ["build/*"]
}

background.js:

chrome.browserAction.onClicked.addListener(function(activeTab) {
    chrome.tabs.executeScript(activeTab.id, {
        file: "./inject.js",
        runAt: "document_start",
    });
});

inject.js:

const script = document.createElement("script");
script.type = "module";
script.src = chrome.runtime.getURL("build/main.js");
document.body.appendChild(script);

const myElement = document.createElement("my-element");
document.body.prepend(myElement);

build/main.js:

class MyElement extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: "open" });
    }
}

customElements.define("my-element", MyElement);
dodov
  • 5,206
  • 3
  • 34
  • 65
  • 1
    This approach is simply injecting a script into the tab via background.js, much like manipulating the DOM through the console. However, it messes up several things, such as variable isolation and inability to access extension APIs, among others. – weiya ou Mar 23 '23 at 18:05
  • Yes, this approach has its trade-offs, but it appears to be the only way to use custom elements in your extension. I guess you could still use `window.postMessage()` to establish communication between the background script and the injected script. – dodov Mar 24 '23 at 11:52