将 Lit 元素移动到新文档时未保留样式

Styles not preserved when moving Lit element to new document

问题


所以我有一个 Lit 元素,我正试图移动到一个新文档。我可以让它移动并且所有功能仍然存在,以及它与原始文档之间的通信。但是它缺少所有样式。

例如,如果我有这样一个简单的 Lit 元素:

  @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>
       `;
    }
}

如果调用 popout 方法,元素将移动到新的 window 中,除了样式之外一切都完好无损。

我试过的


深入研究 Lit NPM 包,我尝试追踪设置 adoptedStyleSheets 属性 的位置。我没有在 windowdocumentrenderRootshadowRoot 上找到它。我希望我能以某种方式将采用的样式表从一个文档迁移到另一个文档。

使用 importNode 而不是 adoptNode。这导致重新设置元素的状态,这对我的用例来说是非常不希望的,但也会给出错误 Failed to set the 'adoptedStyleSheets' property on 'ShadowRoot': Sharing constructed stylesheets in multiple documents is not allowed。我认为这可能是样式未被移动的根本原因,但我不确定该怎么做。

我需要什么


需要一种方法将元素从一个文档移动到另一个文档,同时保留状态、功能和样式。当前方法覆盖状态和功能,但不保留样式。在新 window(弹出窗口)中打开元素时寻找一种方法来维护所有这三个元素。

工作示例


您可以在此处找到此问题的工作示例:https://codepen.io/Arrbjorn/pen/bGaPMBa

问题的根源


所以这里的问题是构造样式表的规范不支持跨文档共享构造样式表,因此出现错误 Failed to set the 'adoptedStyleSheets' property on 'ShadowRoot': Sharing constructed stylesheets in multiple documents is not allowed

有关做出此决定的原因的更多信息,您可以在此处阅读原始讨论:https://github.com/WICG/construct-stylesheets/issues/23

解决方案


但是,对于需要在新文档中打开的 Web 组件,无论是 iframe、新的 window 还是其他文档,都有一个解决方法。我们可以在 Web 组件的 adoptedCallback 函数中解决这个期望的行为。这是一个可能看起来像的示例:

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 ];
        }
    }
}

这是在做什么?

此代码块引用自定义元素的原始样式,并在新文档的上下文中创建新的 CSSStyleSheet(或 CssStyleSheet[])并将它们应用于该元素。这样我们就不会将原始样式共享到新文档,而是使用 BY 新文档创建的新样式表。

正在优化


如果您像我一样在许多组件中使用它,这些组件可能会移动到另一个文档,您可以将其抽象为一个实用函数,该函数可以导入任何组件并在 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! )
}