So I have a Lit element I am trying to move to a new document. I can get it to move and all of the functionality is still in place, as well as the communicated between it and the original document. However it is missing all of its styles.
For instance if I have a simple Lit element as such:
@customElement("my-el")
export class customEl extends LitElement {
popout () {
const newWindow = window.open('', '_blank', 'title=Search Controls,height=1447,width=405,top=0,…o,status=no,menubar=no,scrollbars=no,resizable=no');
newWindow.document.body.appendChild( newWindow.document.adoptNode( this ) );
}
static styles = css`
.custom-el {
background-color: #666;
color: #fff;
}
.custom-el .custom-el_text {
padding: 15px;
border: 1px solid red;
font-family: "Comic Sans", "Arial", "Helvetica", sans-serif;
font-size: 44px;
font-weight: 900;
}
`;
render() {
return html`
<div class="custom-el">
<p class="custom-el_text">Hello World</p>
</div>
`;
}
}
If the popout
method is called, the element is moved to the new window with everything intact except the styles.
Digging into the Lit NPM package I tried tracking down where it was setting the adoptedStyleSheets
property. I didn't find it on window
, document
, renderRoot
, or shadowRoot
. I was hoping that I could somehow migrate the adopted stylesheet from one document to another.
Using importNode
instead of adoptNode
. This results in re-setting the state of the element, which is highly undesirable for my use case, but also gives the error Failed to set the 'adoptedStyleSheets' property on 'ShadowRoot': Sharing constructed stylesheets in multiple documents is not allowed
. I figure this is probably the root of why the styles aren't moved over, but I'm not sure what to do about it.
Need a way to move element from one document to another while preserving state, functionality, and styles. State and functionality are covered with the current method, but styles are not preserved. Looking for a way to maintain all three while opening the element in a new window (popup).
You can find a working example of this problem here: https://codepen.io/Arrbjorn/pen/bGaPMBa
So the issue here is that the spec for constructed stylesheets does not support sharing constructed stylesheets across documents, hence the error Failed to set the 'adoptedStyleSheets' property on 'ShadowRoot': Sharing constructed stylesheets in multiple documents is not allowed
.
For more information on why this decision was made you can read the original discussion here: https://github.com/WICG/construct-stylesheets/issues/23
There is, however, a workaround for web components that need to be opened in a new document, whether it is an iframe
, a new window
, or anything else. We can address this desired behavior in the adoptedCallback
function for our web component. Here is an example of what that might look like:
import { CSSResultGroup, supportsAdoptingStyleSheets } from "lit";
adoptedCallback(){
// If the browser supports adopting stylesheets
if (supportsAdoptingStyleSheets) {
// If the styles is an array of CSSResultGroup Objects
// This happens when styles is passed an array i.e. => static styles = [css`${styles1}`, css`${styles2}`] in the component
if ( ((this.constructor as typeof LitElement).styles as CSSResultGroup[]).length ) {
// Define the sheets array by mapping the array of CSSResultGroup objects
const sheets = ((this.constructor as typeof LitElement).styles as CSSResultGroup[]).map( s => {
// Create a new stylesheet in the context of the owner document's window
// We have to cast defaultView as any due to typescript definition not allowing us to call CSSStyleSheet in this conext
// We have to cast CSSStyleSheet as <any> due to typescript definition not containing replaceSync for CSSStyleSheet
const sheet = (new (this.ownerDocument.defaultView as any).CSSStyleSheet() as any);
// Update the new sheet with the old styles
sheet.replaceSync(s);
// Return the sheet
return sheet;
});
// Set adoptedStyleSheets with the new styles (must be an array)
(this.shadowRoot as any).adoptedStyleSheets = sheets;
} else {
// Create a new stylesheet in the context of the owner document's window
// We have to cast defaultView as any due to typescript definition not allowing us to call CSSStyleSheet in this conext
// We have to cast CSSStyleSheet as <any> due to typescript definition not containing replaceSync for CSSStyleSheet
const sheet = (new (this.ownerDocument.defaultView as any).CSSStyleSheet() as any);
// Update the new sheet with the old styles
sheet.replaceSync( (this.constructor as typeof LitElement).styles );
// Set adoptedStyleSheets with the new styles (must be an array)
(this.shadowRoot as any).adoptedStyleSheets = [ sheet ];
}
}
}
This block of code references the original styles of your custom element and creates a new CSSStyleSheet
(or CssStyleSheet[]
) in the context of the new document and applies them to the element. This way we aren't sharing the original styles to a new document, we are using a new stylesheet created BY the new document.
If, like me, you are using this in many components that may be moved to another document you can abstract this out into a utility function that can be imported into any component and called during adoptedCallback
.
import { CSSResultGroup, supportsAdoptingStyleSheets } from "lit";
// This function migrates styles from a custom element's constructe stylesheet to a new document.
export function adoptStyles ( shadowRoot: ShadowRoot, styles: CSSResultGroup | CSSResultGroup[], defaultView: Window) {
// If the browser supports adopting stylesheets
if (supportsAdoptingStyleSheets) {
// If the styles is an array of CSSResultGroup Objects
// This happens when styles is passed an array i.e. => static styles = [css`${styles1}`, css`${styles2}`] in the component
if ( (styles as CSSResultGroup[]).length ) {
// Define the sheets array by mapping the array of CSSResultGroup objects
const sheets = (styles as CSSResultGroup[]).map( s => {
// Create a new stylesheet in the context of the owner document's window
// We have to cast defaultView as any due to typescript definition not allowing us to call CSSStyleSheet in this conext
// We have to cast CSSStyleSheet as <any> due to typescript definition not containing replaceSync for CSSStyleSheet
const sheet = (new (defaultView as any).CSSStyleSheet() as any);
// Update the new sheet with the old styles
sheet.replaceSync(s);
// Return the sheet
return sheet;
});
// Set adoptedStyleSheets with the new styles (must be an array)
(shadowRoot as any).adoptedStyleSheets = sheets;
} else {
// Create a new stylesheet in the context of the owner document's window
// We have to cast defaultView as any due to typescript definition not allowing us to call CSSStyleSheet in this conext
// We have to cast CSSStyleSheet as <any> due to typescript definition not containing replaceSync for CSSStyleSheet
const sheet = (new (defaultView as any).CSSStyleSheet() as any);
// Update the new sheet with the old styles
sheet.replaceSync(styles);
// Set adoptedStyleSheets with the new styles (must be an array)
(shadowRoot as any).adoptedStyleSheets = [ sheet ];
}
}
}
adoptedCallback() {
// Adopt the old styles into the new document
adoptStyles( this.shadowRoot!, (this.constructor as typeof LitElement).styles!, this.ownerDocument.defaultView! )
}