Vandhana
Vandhana

Reputation: 521

Open a component in new window on a click in react

I have a component which displays a data. I have to open this component in a new window on clicking a button/ link from a parent component.

export default class Parent extends Component {
    construtor(props) {
        super(props);
    }

    viewData = () => {
        window.open('childcomponent.js','Data','height=250,width=250');
    }

    render() {
        return (
            <div> <a onclick={this.viewData}>View Data</a></div>
        )
    }
}

I dont know how to invoke another component and also display it in a new size specified window.

Actually I need to send a props to that child component with which it will fetch me the data from database and render it.

Upvotes: 32

Views: 107347

Answers (7)

CD..
CD..

Reputation: 74176

You can use ReactDOM.createPortal to render a component in a new window as David Gilbertson explains in his post:

class MyWindowPortal extends React.PureComponent {
  constructor(props) {
    super(props);
    // STEP 1: create a container <div>
    this.containerEl = document.createElement('div');
    this.externalWindow = null;
  }
  
  render() {
    // STEP 2: append props.children to the container <div> that isn't mounted anywhere yet
    return ReactDOM.createPortal(this.props.children, this.containerEl);
  }

  componentDidMount() {
    // STEP 3: open a new browser window and store a reference to it
    this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');

    // STEP 4: append the container <div> (that has props.children appended to it) to the body of the new window
    this.externalWindow.document.body.appendChild(this.containerEl);
  }

  componentWillUnmount() {
    // STEP 5: This will fire when this.state.showWindowPortal in the parent component becomes false
    // So we tidy up by closing the window
    this.externalWindow.close();
  }
}

Upvotes: 34

Ryan Taylor
Ryan Taylor

Reputation: 8892

This answer is based on David Gilbertson's post. It has been modified to work in Edge. To make this work in Edge div and style elements must be created with the window into which they will be rendered.

class MyWindowPortal extends React.PureComponent {
  constructor(props) {
    super(props);
    this.containerEl = null;
    this.externalWindow = null;
  }

  componentDidMount() {
    // STEP 1: Create a new window, a div, and append it to the window. The div 
    // *MUST** be created by the window it is to be appended to (Edge only)
    this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
    this.containerEl = this.externalWindow.document.createElement('div');
    this.externalWindow.document.body.appendChild(this.containerEl);
  }

  componentWillUnmount() {
    // STEP 2: This will fire when this.state.showWindowPortal in the parent component
    // becomes false so we tidy up by just closing the window
    this.externalWindow.close();
  }
  
  render() {
    // STEP 3: The first render occurs before componentDidMount (where we open the
    // new window) so container may be null, in this case render nothing.
    if (!this.containerEl) {
      return null;
    } 
    
    // STEP 4: Append props.children to the container <div> in the new window
    return ReactDOM.createPortal(this.props.children, this.containerEl);  
  }
}

The full modified source can be found here https://codepen.io/iamrewt/pen/WYbPWN

Upvotes: 10

Dr. Onix
Dr. Onix

Reputation: 60

For anyone having problem with this answer regarding copying styles and it's not working on production build.

In case you open new window using window.open('', ...), the new window will most likely have about:blank as URL and won't find css files as they have relative paths. You need to set absolute path to href attribute instead:

function copyStyles(src, dest) {
  Array.from(src.styleSheets).forEach((styleSheet) => {
    const styleElement = styleSheet.ownerNode.cloneNode(true);
    styleElement.href = styleSheet.href;
    dest.head.appendChild(styleElement);
  });
  Array.from(src.fonts).forEach((font) => dest.fonts.add(font));
}

Upvotes: 2

Pepe Alvarez
Pepe Alvarez

Reputation: 1634

If someone is having trouble adding the styles to your new window, the trick is to copy the whole DOM head from the parent to your new popup window. First you need to stablish the whole html skeleton

newWindow.document.write("<!DOCTYPE html");
newWindow.document.write("<html>");
newWindow.document.write("<head>");
newWindow.document.write("</head>");
newWindow.document.write("<body>");
newWindow.document.write("</body>");
newWindow.document.write("</html>");
// Append the new container to the body of the new window
newWindow.document.body.appendChild(container);

Now in the new window's DOM we have an empty head tag, we traverse the parent head tag and append its children to the new head.

const parentHead= window.document.querySelector("head").childNodes;
parentHead.forEach( item =>{
    newWindow.document.head.appendChild(item.cloneNode(true)); // deep copy
})

And that's all. With the new window having the same head children than the parent, all your styles should be working now.

P.S. some styles might be a little stubborn and will not work, the hack that I found worked for those ones is to add a setTimeout in the componentDidMount or in the useEffect with the right time so as you can update your new head with the parent head. Something like this

setTimeout(() => {
  updateHead();
}, 5000);

Upvotes: 0

rob-gordon
rob-gordon

Reputation: 1488

The upvoted answer works great!

Just leaving a function component version here in case people are searching for that in the future.

const RenderInWindow = (props) => {
  const [container, setContainer] = useState(null);
  const newWindow = useRef(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(
        "",
        "",
        "width=600,height=400,left=200,top=200"
      );
      // Append container
      newWindow.current.document.body.appendChild(container);

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

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

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

Upvotes: 32

amiregelz
amiregelz

Reputation: 1875

Adding to the current answers - to copy the styles from the original window to the popup window, you can do:

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

and then add

copyStyles(window.document, popupWindow.document);

after the call to window.open and once the ref is initialized

Upvotes: 2

samanime
samanime

Reputation: 26615

You wouldn't open the component directly. You'll need a new page/view that will show the component. When you open the window, you'll then point it at the appropriate URL.

As for size, you provide it as a string in the third parameter of open, which you actually have correct:

window.open('http://example.com/child-path','Data','height=250,width=250');

Note, however, that browsers may, for a variety of reasons, not respect your width and height request. For that reason, it's probably a good idea to also apply appropriate CSS to get a space the right size in case it does open larger than you wanted.

Upvotes: 6

Related Questions