react-new-window icon indicating copy to clipboard operation
react-new-window copied to clipboard

How to force Dynamic JSS Styles using Material UI to be created within the new window?

Open tkuben opened this issue 7 years ago • 9 comments
trafficstars

I am using material UI with the HOC withStyles. I have tab components within the new window and when I click into a tab within the new Window, thats when the styles for the tabs are loaded (dynamically) cause of the JSS/withStyles. The problem is these styles are being created dynamically at the parent window. How can I get it to create within the new window that was created using your library?

Cheers, TJ

tkuben avatar Nov 19 '18 06:11 tkuben

I stumbled upon this issue while working on my own window implementation. While this solution does not use react-new-window, you should be able to apply the same pattern. Four things were key to success:

  • do not copy over JSS styles (in fact, if you use purely JSS and do not import any normal CSS stylesheet, you don't need to copy any styles).
  • create a new StylesProvider with a fresh JSS instance and a custom insertion point
  • reset the sheetsManager
  • re-apply the CssBaseline, if you use it
import React, { useRef, useState, useEffect } from "react";
import { create } from "jss";
import { jssPreset, StylesProvider, CssBaseline } from "@material-ui/core";
import ReactDOM from "react-dom";

function useConst<T>(init: () => T) {
  // We cannot useMemo, because it is not guranteed to never rerun.
  // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
  const ref = useRef<T | null>(null);
  if (ref.current === null) {
    ref.current = init();
  }
  return ref.current;
}

export function MyWindowPortal({
  title,
  children,
  onClose
}: React.PropsWithChildren<{ title: string; onClose: () => void }>) {
  const titleEl = useConst(() => document.createElement("title"));
  const stylesInsertionPoint = useConst(() => document.createComment(""));
  const containerEl = useConst(() => document.createElement("div"));
  const jss = useConst(() =>
    create({ ...jssPreset(), insertionPoint: stylesInsertionPoint })
  );

  const [isOpened, setOpened] = useState(false);

  useEffect(() => {
    const externalWindow = window.open(
      "",
      "",
      "width=600,height=400,left=200,top=200,scrollbars=on,resizable=on,dependent=on,menubar=off,toolbar=off,location=off"
    );
    if (!externalWindow) {
      onClose();
      return;
    }

    externalWindow.document.head.appendChild(titleEl);
    externalWindow.document.body.appendChild(stylesInsertionPoint);
    externalWindow.document.body.appendChild(containerEl);

    (Array.from(document.styleSheets) as CSSStyleSheet[]).forEach(
      styleSheet => {
        const owner = styleSheet.ownerNode as HTMLElement;
        if (owner.dataset.jss !== undefined) {
          // Ignore JSS stylesheets
          return;
        }
        if (styleSheet.cssRules) {
          // for <style> elements
          const newStyleEl = document.createElement("style");
          Array.from(styleSheet.cssRules).forEach(cssRule => {
            // write the text of each rule into the body of the style element
            newStyleEl.appendChild(document.createTextNode(cssRule.cssText));
          });
          externalWindow.document.head.appendChild(newStyleEl);
        } else if (styleSheet.href) {
          // for <link> elements loading CSS from a URL
          const newLinkEl = document.createElement("link");
          newLinkEl.rel = "stylesheet";
          newLinkEl.href = styleSheet.href;
          externalWindow.document.head.appendChild(newLinkEl);
        }
      }
    );

    const windowCheckerInterval = setInterval(() => {
      if (externalWindow.closed) {
        setOpened(false);
        onClose();
        clearInterval(windowCheckerInterval);
      }
    }, 200);

    setOpened(true);

    return () => {
      externalWindow.close();
      clearInterval(windowCheckerInterval);
    };
  }, [containerEl, onClose, stylesInsertionPoint, titleEl]);

  useEffect(() => {
    titleEl.innerText = title;
  }, [title, titleEl]);

  return isOpened
    ? ReactDOM.createPortal(
        <StylesProvider jss={jss} sheetsManager={new Map()}>
          <CssBaseline />
          {children}
        </StylesProvider>,
        containerEl
      )
    : null;
}

cmfcmf avatar Feb 16 '20 16:02 cmfcmf

Here's the solution for the same issue with styled-components:

import React from 'react';
import styled, {StyleSheetManager} from 'styled-components';
import NewWindow from 'react-new-window';

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showPopout: false,
    };
    this.nwRef = React.createRef();
  }

  render () {
    ... some stuff
    this.state.showPopout && (
    <StyleSheetManager target={this.nwRef.current}>
      <NewWindow
        title="Title"
        features={{width: '960px', height: '600px'}}
        onUnload={() => this.setState({showPopout: false})}
      >
        <div ref={this.nwRef}>
          <Popout isPopout={true}>
            ... popup stuff
          </Popout>
        </div>
      </NewWindow>
    </StyleSheetManager>
  )}
}

dmt0 avatar Sep 17 '20 20:09 dmt0

The solution provided by @cmfcmf works well for the styles that have already been injected in the dom by material UI, but it doesn't include the styles that haven't been injected yet.

For example, concerning an Autocomplete component that haven't been already clicked in the parent window, the styles of the entries won't be included in the dom of the child window. Thus, when clicking on the component in the child window, the list elements won't be styled.

ValerianGonnot avatar Oct 27 '21 09:10 ValerianGonnot

That's an interesting scenario... @ValerianGonnot can you recreate this scenario in a codesandbox?

rmariuzzo avatar Oct 27 '21 14:10 rmariuzzo

@rmariuzzo Yes, sure, here is the link : https://codesandbox.io/s/mui-portal-with-style-copy-owm2e?file=/src/App.tsx:4509-4511

ValerianGonnot avatar Nov 02 '21 13:11 ValerianGonnot

Thank you @ValerianGonnot, I'm gonna check it later today or during this week. I really appreciate your effort.

rmariuzzo avatar Nov 02 '21 14:11 rmariuzzo

I got a really simple solution in this issue using emotion's CacheProvider. It makes style injection available in the child window.

Here is an updated sandbox : https://codesandbox.io/s/mui-portal-with-style-solution-ez35b?file=/src/App.tsx

ValerianGonnot avatar Nov 04 '21 09:11 ValerianGonnot

I got a really simple solution in this issue using emotion's CacheProvider. It makes style injection available in the child window.

Here is an updated sandbox : https://codesandbox.io/s/mui-portal-with-style-solution-ez35b?file=/src/App.tsx

Well I used this method to inject MUI styles to my new window, BUT I got some performance issues, because CacheProvider adds styles inside body element. For example after displaying a spinner dialog, I cannot click in window for a second. Is there any solution to inject theme in the head?

AbolfazlHeidarpour avatar Oct 30 '22 13:10 AbolfazlHeidarpour

I will need help on how we could tackle this.

rmariuzzo avatar Nov 27 '22 21:11 rmariuzzo