惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
腾讯CDC
博客园 - 聂微东
爱范儿
爱范儿
罗磊的独立博客
P
Proofpoint News Feed
博客园 - Franky
博客园 - 三生石上(FineUI控件)
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
酷 壳 – CoolShell
酷 壳 – CoolShell
Jina AI
Jina AI
Blog — PlanetScale
Blog — PlanetScale
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 司徒正美
美团技术团队
MongoDB | Blog
MongoDB | Blog
WordPress大学
WordPress大学
A
About on SuperTechFans
I
InfoQ
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog

Jake Archibald's blog

Controlling when CSS custom properties are computed Fixing my tooltip accessibility mistake The Goldilocks customizable select height Importing vs fetching JSON The present and potential future of progressive image rendering Fetch streams are great, but not for measuring upload/download progress Making XML human-readable without XSLT Give footnotes the boot Animating zooming using CSS: transform order is important… sometimes How should <​selectedoption​> work? Video with alpha transparency on the web Garbage collection and closures
Firefox + custom elements + iframes bug
2025-02-14 · via Jake Archibald's blog

Over at Shopify we've been building a bunch of web components to use internally and in third party contexts. All of a sudden, we found some strange errors in our logs, all from Firefox. This is the post I wish existed when we discovered it.

Update: This is now fixed, and should land in Firefox 151.

The bug

The bug happens when a custom element (or web component) is moved to a document from another JavaScript Realm [spooky noises]. A Realm is a separate JavaScript context with its own global, own implementation of Array etc etc. An iframe or popup window provides a document in a new JavaScript Realm.

The result of the bug is that the element's custom prototype is lost, and things like instance methods disappear.

Ok ok, sorry, I'm trying to hit all the terms that people might search for to find a fix for this issue. Anyway, here's a simple custom element:

class MyElement extends HTMLElement {
  say(message) {
    console.log(message);
  }
  connectedCallback() {
    this.say('hello!');
  }
  disconnectedCallback() {
    this.say('goodbye!');
  }
}

customElements.define('my-element', MyElement);

And I'm going to create an instance of the element, and put it in an iframe:

// Create the iframe
const iframe = document.createElement('iframe');
document.body.append(iframe);

// Create the element, and put it in the iframe
const myElement = document.createElement('my-element');
iframe.contentDocument.body.append(myElement);

This fails in Firefox with "this.say is not a function", within connectedCallback. In fact, Firefox has lost all of the instance methods of the custom element. It's 'downgraded' to an instance of HTMLElement in the iframe Realm, rather than MyElement.

It's kinda funny, because the error happens within connectedCallback, which is an instance method, but even that instance method has gone. I assume the calling of connectedCallback was queued up before the prototype was lost.

If I put the element in the main document before moving it to the iframe, it fails in disconnectedCallback for the same reason.

Unfortunately this has been a known issue for 6 years.

The fix

The fix is kinda simple. Just… put the prototype back. As in, make it an instance of MyElement again.

class CustomElementBase extends HTMLElement {
  // This is called when the element is moved to a new document.
  // This is where we solve the bug if the element is moved to an iframe
  // without first being put into the main document.
  adoptedCallback() {
    rescueElementPrototype(this);
  }

  // This is called when the element is disconnected from a document.
  // This happens whenever the element is moved around the DOM,
  // but it also happens when the element is moved to a new document.
  // This happens before adoptedCallback,
  // so we need to fix it here,
  // to avoid the bug in subclass disconnectedCallback calls.
  disconnectedCallback() {
    rescueElementPrototype(this);
  }
}

function rescueElementPrototype(element) {
  // Return if everything looks as expected.
  if (element instanceof CustomElementBase) return;

  // Otherwise, get the intended constructor…
  const constructor = customElements.get(element.tagName.toLowerCase());

  // …and set the prototype.
  Object.setPrototypeOf(element, constructor.prototype);
}

Thanks to my colleague Anthony Frehner who realised customElements.get is a simple way to get the original constructor back, rather than the mad WeakMap hack I was using.

Now, make sure your custom elements extend this base class, and ensure you call super methods:

class MyElement extends CustomElementBase {
  say(message) {
    console.log(message);
  }
  connectedCallback() {
    super.connectedCallback?.();
    this.say('hello!');
  }
  disconnectedCallback() {
    super.disconnectedCallback?.();
    this.say('goodbye!');
  }
}

And that's it! The bug is undone, and everything works as expected.

View this page on GitHub