Why can't you create Custom Elements in content scripts?
Asked Answered
E

5

29

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.

Epicene answered 15/3, 2017 at 2:24 Comment(4)
Does it work on regular pages?Unific
Related: crbug.com/273126, crbug.com/390807Karyn
@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.Epicene
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: 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.Outgo
H
9

- Problem: customElements is implemented in most modern browsers but it's not available from a content script because they are run in an isolated environment. In other words content scripts are not sharing the same window global interface as the current webpage they execute in.

- Solution: We will need to use a polyfill. Interestingly this is all what we need to do to make the custom element leak and become usable inside the main context.

(important note: tested using Manifest v3 only)


First, install the polyfill:

npm i @webcomponents/webcomponentsjs

Then add the polyfill in content_scripts.js field in the 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 work:) Cheers!

Hepburn answered 7/4, 2022 at 13:23 Comment(2)
I'm getting an error from the polyfill Cannot read properties of null (reading 'insertBefore') Did you have to add anything else?Erechtheus
@JoeyGrisafe If you get this error it's probably because you are using "run_at": "document_start", you have to run your content script at the end or when it's idle to make sure everything is available for the polyfill to load.Hepburn
D
6

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

Dowden answered 23/3, 2023 at 17:46 Comment(1)
I'm assuming the note about lit/polyfill-support.js applies only if one is using the lit package, and is not needed just to access the customElements global...Shaun
T
3

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.

Tufts answered 18/10, 2019 at 5:25 Comment(2)
I still get Uncaught TypeError: Cannot read property 'define' of null when calling window.customElements.define in a script injected with chrome.tabs.executeScriptOslo
@Oslo need to include a polyfill github.com/webcomponents/polyfills/tree/master/packages/…Tufts
P
1

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: (runs in extension content script context)

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: (runs in webpage main world context)

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

customElements.define("my-element", MyElement);
Phalan answered 28/2, 2023 at 8:7 Comment(2)
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.Dowden
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.Phalan
L
1

In your shell:

yarn add @webcomponents/custom-elements

At the very beginning of your content script:

import '@webcomponents/custom-elements';

Note that if you ignore the "very beginning" requirement, you'll get TypeError: Illegal constructor exception.

Lamanna answered 18/4 at 19:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.