2

I'm using react-new-window to open a popup. The popup includes a few dynamic components (a toggle, a dropdown, etc) styled with styled-components.

Everything is displayed properly until I try to interact with one of the dynamic components and it changes state (say I switch a toggle from off to on). Then it turns out that the CSS class for the new component state, that is normally generated and attached to the <head>, actually gets attached to the <head> of the parent window, not the popup. So my component appears to lose styling.

I have the same bunch of components in the parent window as well. So if I interact with them before opening the popup, the styles get attached to the <head> as usual, and then get copied to the popup and all looks good.

So I see two possible ways I could solve it:

  1. I could tell styled-component to somehow talk to the new window, not the parent.
  2. As a workaround I could somehow programmatically pre-generate all the styles (there isn't a lot of them).

The problem is I'm not sure how to do either of those two things. Any ideas welcome!

Dmitry Shvedov
  • 3,169
  • 4
  • 39
  • 51

3 Answers3

1

If anyone needs it, here is a working example for a functional component

const Parent = () => {
  const [showPopout, setShowPopout] = useState(false)
  const [newWindowNode, setNewWindowNode] = useState(null)

  const nwRef = useCallback(node => setNewWindowNode(node), [])

  return showPopout 
    ? (
      <StyleSheetManager target={newWindowNode}>
        <NewWindow
          title="Title"
          features={{width: '960px', height: '600px'}}
          onUnload={() => setShowPopout(false)}
        >
          <div ref={nwRef}>
            <Popout isPopout={true}>
              ... popup stuff
            </Popout>
          </div>
        </NewWindow>
      </StyleSheetManager>
    ) : null
}
0

Option 1 solution is actually possible through styled-component API:

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>
  )}
}
Dmitry Shvedov
  • 3,169
  • 4
  • 39
  • 51
0

Using StyleSheetManager give me really poor performance, maybe the stylesheet is recreated at each re-render. I prefer to simply copy the stylesheet like suggested in https://github.com/JakeGinnivan/react-popout/issues/15#issuecomment-527429574 .

import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

// src: https://stackoverflow.com/questions/47574490/open-a-component-in-new-window-on-a-click-in-react
// you may also check: https://github.com/rmariuzzo/react-new-window/blob/main/src/NewWindow.js
// for stylesheet copy see: https://github.com/JakeGinnivan/react-popout/issues/15
export const RenderInWindow = (props: React.PropsWithChildren<
  {
    winName?: string;
  }
>) => {
  const [container, setContainer] = useState<HTMLDivElement|null>(null);
  const newWindow = useRef<Window|null>();

  useEffect(() => {
    // Create container element on client-side
    setContainer(document.createElement("div"));
  }, []);

  useEffect(() => {
    // When container is ready
    if (container) {
      // Create window
      newWindow.current = window.open(
        '',
        props.winName ?? '',
        "width=600,height=400,left=200,top=200"
      );

      if (!newWindow.current) {
        throw new Error('Failed to create window.');
      }

      // Append container
      newWindow.current.document.body.appendChild(container);

      // Save reference to window for cleanup
      const curWindow = newWindow.current;

      copyStyles(document, newWindow.current!.document)

      // Return cleanup function
      return () => curWindow.close();
    }
  }, [container, props.winName]);

  return container && createPortal(props.children, container);
};

const copyStyles = (src: Document, dest: Document) => {
  Array.from(src.styleSheets).forEach(styleSheet => {
    dest.head.appendChild(styleSheet.ownerNode!.cloneNode(true))
  })
  Array.from(src.fonts).forEach(font => dest.fonts.add(font))
}

Ambroise Rabier
  • 3,636
  • 3
  • 27
  • 38