Lwc: can't define a native CustomElement using CustomElementConstructor when component is depended upon by another component

Created on 19 Aug 2020  路  12Comments  路  Source: salesforce/lwc

Description

For some context: we are trying to see if we can ship some lightning web components of an app we are building so they could potentially be used standalone by other apps that don't use LWC yet.

The bug is that when using customElements.define(tagName, LightningWebComponentSubclass.CustomElementConstructor) to convert Lightning Web Components to native Web Components, you can't register custom elements that are dependencies of other components.

The scenario:

  • Two components: CustomA and CustomB
  • CustomA renders CustomB in its template: <custom-b></custom-b>
  • Defining CustomA as a native web component via CustomElementConstructor works without error: customElements.define('custom-a', CustomA.CustomElementConstructor)
  • Defining CustomB as a native web component immediately after with the same method raises the NotSupportedError below: customElements.define('custom-b', CustomB.CustomElementConstructor)
  • Changing the order components are defined does not suppress this error (and I don't think that requiring developers to figure out dependency trees should be necessary either)

When started with npm run watch, and loading in the browser, we get the following runtime error:

NotSupportedError: Failed to execute 'attachShadow' on 'Element': Shadow root cannot be created on a host which already hosts a shadow tree.

screenshot showing full stack trace from webpack output

Steps to Reproduce

To reproduce you can clone this example project, run npm install, and then npm run watch and visit localhost:3001 in the browser.

Code where error gets triggered when defining LWC as regular web components using customElements.define:

// src/index.js

import A from './modules/custom/a/a';
import B from './modules/custom/b/b';

// no error when registering A which depends on B
customElements.define('custom-a', A.CustomElementConstructor);
// error when trying to register B: NotSupporteErorr: Failed to execute 'attachShadow' on 'Element'...
customElements.define('custom-b', B.CustomElementConstructor);

Component A:

// src/modules/custom/a/a.js
import { LightningElement } from 'lwc';

export default class extends LightningElement {}
<!-- src/modules/custom/a/a.html -->
<template>
    This is the A Component
    <custom-b></custom-b>
</template>

Component B:

// src/modules/custom/b/b.js
import { LightningElement } from 'lwc';

export default class extends LightningElement {}
<!-- src/modules/custom/b/b.html -->
<template>
    This is the B component
</template>

Expected Results

No error is thrown

Actual Results

Error thrown:

NotSupportedError: Failed to execute 'attachShadow' on 'Element': Shadow root cannot be created on a host which already hosts a shadow tree.

Browsers Affected

All supported browsers, all versions.

Version

Node 12.13.0
package.json
versions: "[email protected]"

  • LWC: 1.7.12
enhancement

Most helpful comment

meaning LWC will still throw at compile time for unresolved custom elements

No, the planned fix is generic enough for you to resolve to a constructor that is registered as a custom element in the global registry. we don't do anything with the constructor all the way down to the creation of the element. that means you can configure the compiler to resolve to a custom element that does not have to be lwc derivate.

All 12 comments

I'll let the LWC team address how/if they plan to solve this problem, but here's the rollup plugin I used to solve it.

/*
 This is VERY HACKY

 When the LWC engine adds child components, it calls document.createElement()
 with a tag name that matches the namespace/name of the component,
 even though it's not actually a custom element.

 https://github.com/salesforce/lwc/blob/master/packages/%40lwc/engine-dom/src/apis/create-element.ts#L108

 This causes problems when the engine tries to mount a component
 that is also a registed custom element.

 This hack goes through the source code and adds a prefix to the lwc
 elements so they don't collide with registed custom elements.

 Compiled templates look like this when creating a custom component:
 api_custom_element("th-header-desktop", _thHeaderDesktop, {})
*/
function HACK_namespaceLWC() {
  return {
    renderChunk: (code, chunk, options) => {
      return {
        code: code.replace(
          /api_custom_element\(\"/g,
          'api_custom_element("lwc-'
        ),
        map: null
      };
    }
  };
}

One thing to note about this solution is that you would need to add the prefix if you were referencing element names in the css / or querying the DOM with querySelector.

Thanks @aputinski. We could give that a try as a workaround in the meantime!

I also found this interesting comment with createElement:

https://github.com/salesforce/lwc/blob/1d3e920ad33a8e11455f2a23910215bbc3c79b3c/packages/%40lwc/engine-dom/src/apis/create-element.ts#L108-L114

While it seems like createElement is an escape hatch and not desired to be used directly, I wonder if the same check could be ported over to buildCustomElementConstructor? Not super familiar with the internals myself but the code paths look similar to createElement in the way that createVM is called:

https://github.com/salesforce/lwc/blob/1d3e920ad33a8e11455f2a23910215bbc3c79b3c/packages/%40lwc/engine-dom/src/apis/build-custom-element-constructor.ts#L66-L71

I think that's referencing the top level createElement export of lwc and not the document.createElement used to insert new DOM elements during the lwc render cycle.

But I also have no idea what I'm taking about 馃槀

this will be fixed by #2007 :), good timing!

Just to clarify, "allow usage of previously defined custom elements tag names in a LWC template" is only for elements defined with CustomElementConstructor (meaning LWC will still throw at compile time for unresolved custom elements)?

meaning LWC will still throw at compile time for unresolved custom elements

No, the planned fix is generic enough for you to resolve to a constructor that is registered as a custom element in the global registry. we don't do anything with the constructor all the way down to the creation of the element. that means you can configure the compiler to resolve to a custom element that does not have to be lwc derivate.

Would love to see this land soon. ;-)

pending approval and release drills, but it will take a while to get it into platform, but in OSS it will be ready soon :)

OSS is what I care about. ;-)

fixed by #2007 and #2027

Was this page helpful?
0 / 5 - 0 ratings

Related issues

reiniergs picture reiniergs  路  5Comments

ravijayaramappa picture ravijayaramappa  路  4Comments

priandsf picture priandsf  路  5Comments

juvian picture juvian  路  5Comments

caridy picture caridy  路  3Comments