react-new-window
react-new-window copied to clipboard
How to force Dynamic JSS Styles using Material UI to be created within the new window?
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
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
StylesProviderwith 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;
}
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>
)}
}
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.
That's an interesting scenario... @ValerianGonnot can you recreate this scenario in a codesandbox?
@rmariuzzo Yes, sure, here is the link : https://codesandbox.io/s/mui-portal-with-style-copy-owm2e?file=/src/App.tsx:4509-4511
Thank you @ValerianGonnot, I'm gonna check it later today or during this week. I really appreciate your effort.
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
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?
I will need help on how we could tackle this.