rmariuzzo / react-new-window

🔲 Pop new windows in React, using `window.open`.
https://rmariuzzo.github.io/react-new-window/
MIT License
445 stars 109 forks source link

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

Open tkuben opened 5 years ago

tkuben commented 5 years ago

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

cmfcmf commented 4 years ago

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:

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;
}
dmt0 commented 3 years ago

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>
  )}
}
ValerianGonnot commented 2 years ago

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.

rmariuzzo commented 2 years ago

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

ValerianGonnot commented 2 years ago

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

rmariuzzo commented 2 years ago

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

ValerianGonnot commented 2 years ago

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

AbolfazlHeidarpour commented 1 year ago

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?

rmariuzzo commented 1 year ago

I will need help on how we could tackle this.