lwc icon indicating copy to clipboard operation
lwc copied to clipboard

Error when rendering the same stylesheet to two different documents (iframes, templates)

Open jmrog opened this issue 3 years ago • 7 comments

Description

This issue appears to affect Chrome only, and happens if (1) your lwc version is >= 2.3.7, (2) you render an LWC component using lwc:dom="manual", (3) your manually-constructed DOM also includes an LWC component, and (4) the LWC component in the manually-constructed DOM has its own CSS stylesheet. If any of those conditions are not satisfied (e.g., you use lwc version <= 2.3.4, or the LWC component in the manually-constructed DOM does not have its own stylesheet), the issue does not occur.

The issue: under the aforementioned conditions, in Chrome, the lwc library throws an uncaught DOMException, and the app fails to render (crashes). The DOMException is as follows:

Uncaught DOMException: Failed to set the 'adoptedStyleSheets' property on 'ShadowRoot': Sharing constructed stylesheets in multiple documents is not allowed
    at insertConstructableStyleSheet (http://localhost:3001/app-d1335320964653800ef9.js:7691:35)
    at Object.insertStylesheet (http://localhost:3001/app-d1335320964653800ef9.js:7887:13)
    at createStylesheet (http://localhost:3001/app-d1335320964653800ef9.js:5973:18)
    at http://localhost:3001/app-d1335320964653800ef9.js:6209:71
    at ReactiveObserver.observe (http://localhost:3001/app-d1335320964653800ef9.js:597:7)
    at isUpdatingTemplate (http://localhost:3001/app-d1335320964653800ef9.js:6178:9)
    at runWithBoundaryProtection (http://localhost:3001/app-d1335320964653800ef9.js:7201:5)
    at evaluateTemplate (http://localhost:3001/app-d1335320964653800ef9.js:6163:3)
    at invokeComponentRenderMethod (http://localhost:3001/app-d1335320964653800ef9.js:6370:39)
    at renderComponent (http://localhost:3001/app-d1335320964653800ef9.js:6428:18)
    at rehydrate (http://localhost:3001/app-d1335320964653800ef9.js:6795:22)
    at connectRootElement (http://localhost:3001/app-d1335320964653800ef9.js:6551:3)
    at callNodeSlot (http://localhost:3001/app-d1335320964653800ef9.js:7919:9)
    at HTMLDivElement.appendChild (http://localhost:3001/app-d1335320964653800ef9.js:7929:16)
    at http://localhost:3001/app-d1335320964653800ef9.js:8437:15
    at NodeList.forEach (<anonymous>)
    at App.insertDocHtml (http://localhost:3001/app-d1335320964653800ef9.js:8428:14)
    at App.renderedCallback (http://localhost:3001/app-d1335320964653800ef9.js:8448:12)
    at callHook (http://localhost:3001/app-d1335320964653800ef9.js:6523:13)
    at http://localhost:3001/app-d1335320964653800ef9.js:6300:5
    at runWithBoundaryProtection (http://localhost:3001/app-d1335320964653800ef9.js:7201:5)
    at invokeComponentCallback (http://localhost:3001/app-d1335320964653800ef9.js:6299:3)
    at runRenderedCallback (http://localhost:3001/app-d1335320964653800ef9.js:6881:5)
    at patchShadowRoot (http://localhost:3001/app-d1335320964653800ef9.js:6853:5)
    at rehydrate (http://localhost:3001/app-d1335320964653800ef9.js:6796:5)
    at connectRootElement (http://localhost:3001/app-d1335320964653800ef9.js:6551:3)
    at callNodeSlot (http://localhost:3001/app-d1335320964653800ef9.js:7919:9)
    at HTMLDivElement.appendChild (http://localhost:3001/app-d1335320964653800ef9.js:7929:16)
    at http://localhost:3001/app-d1335320964653800ef9.js:25006:33
    at http://localhost:3001/app-d1335320964653800ef9.js:25007:3
    at http://localhost:3001/app-d1335320964653800ef9.js:25194:12

The error is thrown at the moment that the adoptedStylesheets property of the target is assigned in insertConstructableStyleSheet.

Steps to Reproduce

I created a very small GitHub repo that reproduces the issue: https://github.com/jmrog/lwc-stylesheets-bug. I was unable to reproduce the issue in the webcomponents.dev environment, but am unsure what version of lwc is being used there anyway. The steps to reproduce the issue are available on the just-mentioned repo, but here they are again:

  1. Clone the repo and run yarn install or npm install.
  2. Start the application using yarn watch or npm run watch.
  3. Direct Chrome to http://localhost:3001 to open the app.
  4. Observe that nothing renders.
  5. Open the browser's developer tools and observe the error mentioned above.

Again, the issue does not appear if you remove the child component's CSS file, or if you do not construct the DOM manually (e.g., change this.isManual = true; to this.isManual = false; in the constructor in app.js), or if you downgrade lwc enough (e.g., downgrading lwc-services to 3.1.0 in the aforementioned repo works), etc. It also does not appear in Firefox or Safari (only Chrome, as far as I know).

Expected Results

The app renders as expected, and no error is thrown. Visual from the above repo:

image

Actual Results

The app fails to render (crashes) and shows an uncaught DOMException in the browser console, in Chrome only. Visual from the above repo:

image

Browsers Affected

Chrome, latest version (96.0.4664.110 as of this writing).

Version

  • LWC: 2.3.7+

Possible Solutions

Not really a solution, but just want to note that the changes introduced in #2460 seem to have triggered this issue.

jmrog avatar Dec 16 '21 17:12 jmrog

Thanks for the very thorough bug report! I wrote a minimal repro (https://github.com/nolanlawson/lwc-barebone/commit/431bec0c1e96197cf5847b8a3ccbb46ebd219515) based on yours:

document.body.appendChild(createElement("x-app", { is: App }));

const template = document.createElement('template');
template.content.appendChild(createElement("x-app", { is: App })); // throws

The issue is that (apparently) adopted stylesheets cannot be reused across two documents – in this case, the main document and a <template>. Iframes have the same issue:

document.body.appendChild(createElement("x-app", { is: App }));

const iframe = document.createElement('iframe');
iframe.contentDocument.appendChild(createElement("x-app", { is: App }));  // throws

One workaround you can use is to avoid <template>s. For instance, here is a modified version of your repro (https://github.com/nolanlawson/lwc-stylesheets-bug/commit/014de8599871f6f802b542b1d4911a2372d277a5) that doesn't throw an error.

We could try to track constructable stylesheets on a per-document basis, but I'm not sure if that's feasible. This is a tricky bug, and there's some more discussion of it here: https://github.com/WICG/construct-stylesheets/issues/23

nolanlawson avatar Dec 16 '21 19:12 nolanlawson

This issue has been linked to a new work item: W-10323566

uip-robot-zz avatar Dec 16 '21 19:12 uip-robot-zz

@nolanlawson Nice minimal repro, and thanks for the workaround. As far as my team is concerned, that workaround is probably sufficient -- and extremely minimal in cost -- for us to move to the latest version of lwc.

Thanks for the link to that discussion, too. Lots to think about there!

jmrog avatar Dec 16 '21 22:12 jmrog

So this is actually a really subtle issue. I have a minimal repro:

  const template = document.createElement('template')
  const div = document.createElement('div')
  div.attachShadow({ mode: 'open' })
  template.content.appendChild(div)

  const sheet = new CSSStyleSheet()
  div.shadowRoot.adoptedStyleSheets = [sheet]

In Chrome this throws:

DOMException: Failed to set the 'adoptedStyleSheets' property on 'ShadowRoot': Sharing constructed stylesheets in multiple documents is not allowed

In Firefox with the flag layout.css.constructable-stylesheets.enabled enabled, it throws:

DOMException: ShadowRoot.adoptedStyleSheets setter: Each adopted style sheet's constructor document must match the document or shadow root's node document

Looking through various discussions on this (e.g. https://github.com/WICG/construct-stylesheets/issues/133 and this Chromium issue), I'm not sure if there's a solution. There is no way AFAICT to associate the CSSStyleSheet's constructor document with the template's owner document. Apparently there are elaborate solutions involving adoptNode, but I haven't yet gotten them to work.

In iframes, I assume this would not be a problem, because assuming the LWC engine is running inside the iframe, it would be getting CSSStyleSheet from the window of the iframe, not the containing page. So maybe this just affects templates.

nolanlawson avatar Mar 01 '22 20:03 nolanlawson

Interestingly the same pattern works fine with Lit. The difference is this:

  const template = document.createElement('template');
  const div = document.createElement('x-foo');
  div.attachShadow({ mode: 'open' });
  template.content.appendChild(div); // LWC assigns adoptedStyleSheets here
  
  document.body.appendChild(template.content) // Lit assigns adoptedStyleSheets here

The reason for the difference is that LWC doesn't use the "real" connectedCallback, and we fire it at the incorrect time. Minimal repro:

Screen Shot 2022-03-01 at 1 54 27 PM

If we fix that bug, then assigning adoptedStyleSheets should "just work."

nolanlawson avatar Mar 01 '22 21:03 nolanlawson

Turns out you don't even need a <template>; you can repro with a <div>:

  const div = document.createElement('div');
  console.log('appending to div')
  div.appendChild(elm);
  console.log('appending div to document.body')
  document.body.appendChild(div);

Screen Shot 2022-03-01 at 3 34 27 PM

nolanlawson avatar Mar 01 '22 22:03 nolanlawson

Related to https://github.com/salesforce/lwc/issues/1102

nolanlawson avatar May 26 '22 17:05 nolanlawson

Duplicate of #3198

pmdartus avatar Jan 20 '23 08:01 pmdartus