Как установить содержимое iframe для реагирующего компонента

Я пытаюсь установить содержимое iframe в компоненте React, но я не могу этого сделать. У меня есть компонент, в котором содержится функция, которую нужно вызывать, когда iframe заканчивает загрузку. В этой функции я устанавливаю содержимое, но не похоже, что функция onload вызывается вообще. Я тестирую его в браузере Chrome. Я пытаюсь сделать следующее:

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;

Что я делаю неправильно?

Ответ 1

Изменить 10-25-2018

Весь этот материал стал фактически тривиальным с введением Portals в React 16. Реализация и использование не только намного проще, содержимое iframe также является фактическим потомком "родительского" виртуального домена, что означает общую систему событий, контексты и так далее. Круто, верно?

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>
    )
  }
}

Это становится еще более лаконичным при использовании 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>
  )
}

Использование:

import Frame from './Frame'

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

Дальнейший контроль, например, над iframes <head>, может быть легко достигнут, как показано в этом Гисте.

Существует также response-frame-component, пакет, который IMHO предлагает практически все, что вам нужно при работе с фреймами в React.

SSR

Одна вещь, которую (по крайней мере, насколько мне известно) вы вряд ли когда-нибудь получите с Portals, - это рендеринг на стороне сервера, потому что порталы можно рендерить, только когда они имеют ссылку на реальный DOM-узел. Так что если вы...

... тогда вы могли бы начать с чего-то вроде этого:

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
        }
      />
    )
  }
}

Счастливого обрамления в 2018 году!

Оригинальный пост

Что касается IFrame, решение на самом деле очень простое: вы должны создать новый рендер DOM для содержимого IFrame и синхронизировать его с остальной частью приложения. Такой компонент может выглядеть примерно так:

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();
    }
});

Это не очень удобно для композиции. Вы не можете использовать React.props.children.only и лайки, потому что они всегда указывают на уже скомпилированные/созданные элементы, которые уже являются частью дерева различий. И поскольку нам нужно новое дерево различий для содержимого в рамке, вам нужно будет определить новый компонент для каждого содержимого в рамке.

Введите компоненты высшего порядка. Цель состоит в том, чтобы создать вид декоратора, который можно применить к любому элементу, который вы хотите создать:

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();
        }
    });
}

Используйте как это:

var MyFramedComponent = FramedComponent(MyComponent);

Редактировать 3-7-2017 Одним из главных преимуществ React перед, вероятно, всеми другими в настоящее время модными виртуальными библиотеками DOM, является его синтетическая система событий. Хороший газон, он просто работает и пузырится так очень удобно.

Однако при таком подходе вы (вполне преднамеренно) вырежете одно дерево различий из следующего, и это также верно для системы событий. Для большинства событий, которые не имеют большого значения:

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

Это будет просто отлично работать. Но есть некоторые не столь заметные исключения, которые, особенно учитывая тот факт, что контролируемые iFrames в React чаще всего используются для простого создания области действия, просто не работают, как ожидалось. Еще один не очень выдающийся пример: onBeforeInsert. Что делает предварительную проверку экземпляров проекта очень утомительной задачей. И опять же, это, вероятно, не имеет значения для большинства случаев использования. Просто убедитесь, что ваше дерьмо захвачено так, как вы ожидаете, прежде чем делать (WYSIWYG) случай для iFrames в React. Был там, сделал это, поверь мне.

Ответ 2

Существует более простое решение, если кто-то просто хочет отобразить маленький HTML внутри iframe.

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

Максимальная длина содержимого составляет 32768 символов.

Существует также простой в использовании пакет response-frame-component, который был упомянут в принятом ответе.

Ответ 4

Вы можете использовать атрибут srcdoc iframe. Это будет работать!

srcdoc: встроенный HTML для встраивания, переопределяющий атрибут src.

Читайте: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe