How to set iframe content of a react component

守給你的承諾、 提交于 2020-05-24 08:10:49

问题


I am trying to set the content of an iframe in a React component but I am not able to do it. I have a component in which contains a function which has to be called when the iframe finishes loading. In that function i am setting the content but it doesnt seem like the onload function is called at all. I am testing it in chrome browser. I am trying the following:

var MyIframe = React.createClass({
    componentDidMount : function(){
        var iframe = this.refs.iframe.getDOMNode();
        if(iframe.attachEvent){
            iframe.attacheEvent("onload", this.props.onLoad);
        }else{
            iframe.onload = this.props.onLoad;
        }
    },
    render: function(){
        return <iframe ref="iframe" {...this.props}/>;
    }
});

var Display = React.createClass({
    getInitialState : function(){
        return {
            oasData : ""
        };
    },
    iframeOnLoad : function(){
        var iframe = this.refs.bannerIframe;
        iframe.contentDocument.open();
        iframe.contentDocument.write(['<head></head><style>body {margin: 0; overflow: hidden;display:inline-block;} html{ margin: 0 auto; text-align: center;} body > a > img {max-width: 100%; height: inherit;}', extraCss, '</style></head><body>', this.state.oasData.Ad[0].Text, '</body>'].join(''));
        iframe.contentDocument.close();
    },
    setOasData : function(data){
        this.setState({
            oasData : JSON.parse(data)
        });
    },
    componentDidMount : function(){
        var url = "getJsonDataUrl";

        var xhttp = new XMLHttpRequest();
        var changeOasDataFunction = this.setOasData;
        xhttp.onreadystatechange = function () {
            if (xhttp.readyState == 4 && xhttp.status == 200) {
                changeOasDataFunction(xhttp.responseText);
            }
        };
        xhttp.open("GET", url, true);
        xhttp.send();
    },
    render : function(){
        return (
            <MyIframe refs="bannerIframe" onLoad={this.iframeOnLoad} />
        );
    }
});

module.exports = Display;

What am i doing wrong?


回答1:


Edit 10-25-2018

This whole frame stuff became actually trivial with the introduction of Portals in React 16. Not only is the implementation and usage much more straightforward, the iframe contents are also actual children of the «parent» virtual dom, which means shared event system, contexts and so on. Awesome, right?

import React, { Component } from 'react'
import { createPortal } from 'react-dom'

export default class Frame extends Component {
  constructor(props) {
    super(props)

    this.setContentRef = node =>
      (this.contentRef =
        ((!node || !node.contentWindow) && null) ||
        node.contentWindow.document.body)
  }

  render() {
    const { children, ...props } = this.props // eslint-disable-line
    return (
      <iframe {...props} ref={this.setContentRef}>
        {this.contentRef &&
          createPortal(
            React.Children.only(children),
            this.contentRef
          )}
      </iframe>
    )
  }
}

It gets even more terse when using React Hooks:

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

export const IFrame = ({ children, ...props }) => {
  const [contentRef, setContentRef] = useState(null)
  const mountNode = contentRef && contentRef.contentWindow.document.body

  return (
    <iframe {...props} ref={setContentRef}>
      {mountNode &&
        createPortal(
          React.Children.only(children),
          mountNode
        )}
    </iframe>
  )
}

Usage:

import Frame from './Frame'

const MyComp = () => <Frame><h1>Hello Content!</h1></Frame>

Further control, for example over the iframes <head>, can easily be achieved as this Gist shows.

There is also react-frame-component, a package that IMHO offers pretty much everything you need when working with iframes in React.

SSR

One thing that (at least to my knowledge) you'll hardly ever get right with Portals though, is server side rendering, because Portals are only ever renderable, when they have a reference to an actual DOM node. So if you ...

  • ... desperately need your iframe contents pre-rendered
  • ... don't mind cutting the virtual dom tree
  • ... target browsers, that support the srcdoc attribute on iframes

... then you could start from something like this:

import React, { Component } from 'react'
import { renderToString } from 'react-dom/server'
import { hydrate, render } from 'react-dom'   

const wrapWithMountNode = html => {
  return `<!DOCTYPE html><html><head></head><body><div id="frame">${html}</div></body></html>`.trim()
}

export default class SSRFrame extends Component {
  constructor(props) {
    super(props)
    this.initialMarkup = wrapWithMountNode(
      renderToString(
        React.Children.only(this.props.children)
      )
    )

    this.contentRef = null
    this.setContentRef = node => {
      this.contentRef =
        ((!node || !node.contentWindow) && null) ||
        node.contentWindow.document.getElementById('frame')
    }
  }

  componentDidMount() {
    this.contentRef &&
      hydrate(
        React.Children.only(this.props.children),
        this.contentRef
      )
  }

  componentDidUpdate() {
    this.contentRef &&
      render(
        React.Children.only(this.props.children),
        this.contentRef
      )
  }

  componentWillUnmount() {
    this.contentRef = null
  }

  render() {
    const { children, ...props } = this.props // eslint-disable-line
    return (
      <iframe
        {...props}
        ref={this.setContentRef}
        srcDoc={
          (!this.contentRef && this.initialMarkup) ||
          undefined
        }
      />
    )
  }
}

Happy framing in 2018!

Original Post

As far as the IFrame is concerned, the solution is actually very simple: You have to create a new DOM renderer for the IFrame content and sync it with the rest of the app. Such a component could look something like this:

var React =  require('react');
var ReactDOM = require('react-dom');

var IFrame = React.createClass({

    propTypes = {
        frameProps: React.PropTypes.object,
    },

    defaultProps = {
        frameProps: {
            frameBorder: 0
        }
    },

    updateIFrameContents: function() {
        ReactDOM.render((
            // Here you put the components that should be in the IFrame
            <div {...this.props}>Hello IFrame!</div>
        ), this.el);
    },

    render: function() {
        return (
            <iframe {...this.props.frameProps} />
        );
    },

    componentDidMount: function() {
        var frameBody = ReactDOM.findDOMNode(this).contentDocument.body,
            el = document.createElement('div');
            frameBody.appendChild(el);
        this.el = el;
        this.updateIFrameContents();
    },

    componentDidUpdate: function() {
        this.updateIFrameContents();
    }
});

Now, this is not very composition-friendly. You can't use React.props.children.only and the likes, because these always point at already compiled/created elements that are already part of the diff tree. And because we want a new diff tree for framed contents, you'd have to define a new component for every framed content.

Enter Higher Order Components. The goal is to create kind of a decorator that can be applied to any element you want to have framed:

function FramedComponent(Component) {
    return React.createClass({

        propTypes = {
            frameProps: React.PropTypes.object,
        },

        defaultProps = {
            frameProps: {
                frameBorder: 0
            }
        },

        updateIFrameContents: function() {
            ReactDOM.render((
                <Component {...this.props} />
            ), this.el);
        },

        render: function() {
            return (
                <iframe {...this.props.frameProps} />
            );
        },

        componentDidMount: function() {
            var frameBody = ReactDOM.findDOMNode(this).contentDocument.body,
                el = document.createElement('div');
                frameBody.appendChild(el);
            this.el = el;
            this.updateIFrameContents();
        },

        componentDidUpdate: function() {
            this.updateIFrameContents();
        }
    });
}

Use like this:

var MyFramedComponent = FramedComponent(MyComponent);

Edit 3-7-2017 One of the main advantages of React over probably all the other currently en vogue virtual DOM libraries, is its synthetic event system. Good lawd, it just works and bubbles so very conveniently.

With this approach though, you'll (quite intentionally) cut one diff tree from the next one and that is true for the event system as well. For most events that doesn't make that much of a difference:

var logNestedClicks = function(event) {
    console.log(event);
}
// and then
<MyFramedComponent onClick={logNestedClicks) />

This will just work fine. But there are some not-so-prominent exceptions, that, especially given the fact that controlled iFrames in React are more often than not used to merely create scope, are just not working as expected. A again not-so-prominent example: onBeforeInsert. Which renders scoping Draft instances a very tedious task. And then again, this is probably irrelevant for most use cases. Just make sure your shit gets captured the way you'd expect before making a (WYSIWYG) case for iFrames in React. Been there, done that, believe me.




回答2:


There is an easier solution if someone just wants to display small HTML inside the iframe.

<iframe src={"data:text/html,"+encodeURIComponent(content)}/>

The max length of content is 32768 characters.

There is also easy to use react-frame-component package which was mentioned in the accepted answer.




回答3:


You can use srcdoc attribute of iframe. It will work!

srcdoc: Inline HTML to embed, overriding the src attribute.

Read: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe




回答4:


This works too (not supported in IE).

const myHTML = <h1>Hello World</h1>
<iframe srcDoc={myHTML} />

More info here: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe




回答5:


Using the DOMParser constructor's parseFromString to parse the html is a little simpler than the accepted answer. Here is an example where the parsed html is retrieved from the DOMParser's generated document. If you're sending an element to the iframe, leave out the .body.innerText part of the parseHtml.

class SimpleIframe extends Component {
    render() {
        const parseHtml = html => new DOMParser().parseFromString(html, 'text/html').body.innerText;
        return <iframe srcDoc={parseHtml(this.props.wholeHTMLDocumentString)} />;
    }
}


来源:https://stackoverflow.com/questions/34743264/how-to-set-iframe-content-of-a-react-component

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!