├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── example ├── app.jsx └── index.html ├── index.d.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── Content.jsx ├── Context.jsx ├── Frame.jsx └── index.js ├── test ├── .eslintrc ├── Content.spec.jsx ├── Context.spec.jsx └── Frame.spec.jsx ├── wallaby.conf.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "latest", "stage-2"], 3 | "plugins": "transform-class-properties" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "parserOptions": { 4 | "ecmaVersion": 7, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "impliedStrict": true, 8 | "jsx": true 9 | } 10 | }, 11 | "env": { 12 | "browser": true 13 | }, 14 | "parser": "babel-eslint", 15 | "rules": { 16 | "react/forbid-prop-types": "off", 17 | "react/prop-types": [0, { "ignore": ["children"]}], 18 | "no-underscore-dangle": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ryanseddon 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | build: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v2 7 | - uses: actions/setup-node@v1 8 | with: 9 | node-version: 14 10 | - run: npm install 11 | 12 | - name: Run headless test 13 | uses: GabrielBB/xvfb-action@v1 14 | with: 15 | run: npm test 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/processed 2 | node_modules 3 | lib/ 4 | npm-debug.log 5 | test-results.xml 6 | *-debug.log* 7 | dist 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test-results.xml 2 | wallaby.conf.js 3 | karma.conf.js 4 | .travis.yml 5 | *-debug.log* 6 | example/ 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ryan Seddon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React <Frame /> component 2 | 3 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][depstat-image]][depstat-url] 4 | 5 | This component allows you to encapsulate your entire React application or per component in an iFrame. 6 | 7 | ```bash 8 | npm install --save react-frame-component 9 | ``` 10 | 11 | ## How to use: 12 | 13 | ```js 14 | import Frame from 'react-frame-component'; 15 | ``` 16 | 17 | Go check out the [demo][demo-url]. 18 | 19 | ```html 20 | const Header = ({ children }) => ( 21 | 22 |

{children}

23 | 24 | ); 25 | 26 | ReactDOM.render(
Hello
, document.body); 27 | ``` 28 | 29 | Or you can wrap it at the `render` call. 30 | 31 | ```html 32 | ReactDOM.render( 33 | 34 |
Hello
35 | , 36 | document.body 37 | ); 38 | ``` 39 | 40 | ##### Props: 41 | 42 | ###### head 43 | `head: PropTypes.node` 44 | 45 | The `head` prop is a dom node that gets inserted before the children of the frame. Note that this is injected into the body of frame (see the blog post for why). This has the benefit of being able to update and works for stylesheets. 46 | 47 | ###### initialContent 48 | `initialContent: PropTypes.string` 49 | 50 | Defaults to `'
'` 51 | 52 | The `initialContent` props is the initial html injected into frame. It is only injected once, but allows you to insert any html into the frame (e.g. a head tag, script tags, etc). Note that it does *not* update if you change the prop. Also at least one div is required in the body of the html, which we use to render the react dom into. 53 | 54 | ###### mountTarget 55 | `mountTarget: PropTypes.string` 56 | 57 | The `mountTarget` props is a css selector (#target/.target) that specifies where in the `initialContent` of the iframe, children will be mounted. 58 | 59 | ```html 60 | 64 | 65 | ``` 66 | 67 | ###### contentDidMount and contentDidUpdate 68 | `contentDidMount: PropTypes.func` 69 | `contentDidUpdate: PropTypes.func` 70 | 71 | `contentDidMount` and `contentDidUpdate` are conceptually equivalent to 72 | `componentDidMount` and `componentDidUpdate`, respectively. The reason these are 73 | needed is because internally we call `ReactDOM.render` which starts a new set of 74 | lifecycle calls. This set of lifecycle calls are sometimes triggered after the 75 | lifecycle of the parent component, so these callbacks provide a hook to know 76 | when the frame contents are mounted and updated. 77 | 78 | ###### ref 79 | `ref: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) }) ])` 80 | 81 | The `ref` prop provides a way to access inner iframe DOM node. To utilitize this prop use, for example, one of the React's built-in methods to create a ref: [`React.createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) or [`React.useRef()`](https://reactjs.org/docs/hooks-reference.html#useref). 82 | 83 | ```js 84 | const MyComponent = (props) => { 85 | const iframeRef = React.useRef(); 86 | 87 | React.useEffect(() => { 88 | // Use iframeRef for: 89 | // - focus managing 90 | // - triggering imperative animations 91 | // - integrating with third-party DOM libraries 92 | iframeRef.current.focus() 93 | }, []) 94 | 95 | return ( 96 | 97 | 98 | 99 | ); 100 | } 101 | ``` 102 | 103 | ##### Accessing the iframe's window and document 104 | The iframe's `window` and `document` may be accessed via the `FrameContextConsumer` or the `useFrame` hook. 105 | 106 | The example with `FrameContextConsumer`: 107 | 108 | ```js 109 | import Frame, { FrameContextConsumer } from 'react-frame-component' 110 | 111 | const MyComponent = (props, context) => ( 112 | 113 | 114 | { 115 | // Callback is invoked with iframe's window and document instances 116 | ({document, window}) => { 117 | // Render Children 118 | } 119 | } 120 | 121 | 122 | ); 123 | 124 | ``` 125 | 126 | The example with `useFrame` hook: 127 | 128 | ```js 129 | import Frame, { useFrame } from 'react-frame-component'; 130 | 131 | const InnerComponent = () => { 132 | // Hook returns iframe's window and document instances from Frame context 133 | const { document, window } = useFrame(); 134 | 135 | return null; 136 | }; 137 | 138 | const OuterComponent = () => ( 139 | 140 | 141 | 142 | ); 143 | ``` 144 | 145 | ## More info 146 | 147 | I wrote a [blog post][blog-url] about building this component. 148 | 149 | ## License 150 | 151 | Copyright 2014, Ryan Seddon. 152 | This content is released under the MIT license http://ryanseddon.mit-license.org 153 | 154 | [npm-url]: https://npmjs.org/package/react-frame-component 155 | [npm-image]: https://badge.fury.io/js/react-frame-component.png 156 | 157 | [travis-url]: http://travis-ci.org/ryanseddon/react-frame-component 158 | [travis-image]: https://secure.travis-ci.org/ryanseddon/react-frame-component.png?branch=master 159 | 160 | [depstat-url]: https://david-dm.org/ryanseddon/react-frame-component 161 | [depstat-image]: https://david-dm.org/ryanseddon/react-frame-component.png 162 | 163 | [demo-url]: http://ryanseddon.github.io/react-frame-component/ 164 | [blog-url]: https://medium.com/@ryanseddon/rendering-to-iframes-in-react-d1cb92274f86 165 | -------------------------------------------------------------------------------- /example/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Frame from '../src'; 4 | 5 | const styles = { 6 | border: '1px solid', 7 | width: '100%', 8 | height: '100%' 9 | }; 10 | 11 | const Header = ({ children }) =>

{children}

; 12 | 13 | const Content = ({ children }) =>
{children}
; 14 | 15 | const App = () => ( 16 |
17 |
Frame example of wrapping application
18 | 19 |

This whole app is wrapped inside an iFrame

20 |
21 |
22 | ); 23 | 24 | ReactDOM.render( 25 | 26 | 27 | , 28 | document.querySelector('#example1') 29 | ); 30 | 31 | const Foobar = () => { 32 | const [toggle, updateToggle] = React.useState(false); 33 | return ( 34 | {'*{color:red}'}}> 35 |

Frame example of wrapping component

36 |

37 | This is also showing encapuslated styles. All text is red inside this 38 | component. 39 |

40 | {toggle &&

Hello

} 41 | 42 | 43 | ); 44 | }; 45 | 46 | ReactDOM.render(, document.querySelector('#example2')); 47 | 48 | const ExternalResources = () => { 49 | const initialContent = ` 50 | 51 | 52 | 53 |
`; 54 | 55 | return ( 56 | 57 |

External Resources

58 |

59 | This tests loading external resources via initialContent which can 60 | create timing issues with onLoad and srcdoc in cached situations 61 |

62 | 63 | ); 64 | }; 65 | 66 | ReactDOM.render(, document.querySelector('#example3')); 67 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Frame Component Examples 6 | 18 | 19 | 20 |

<Frame /> examples

21 |

Two examples below showing how you can encapsulate individual components or your entire application

22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-frame-component' { 2 | import * as React from 'react'; 3 | 4 | export interface FrameComponentProps 5 | extends React.IframeHTMLAttributes, 6 | React.RefAttributes { 7 | head?: React.ReactNode | undefined; 8 | mountTarget?: string | undefined; 9 | initialContent?: string | undefined; 10 | contentDidMount?: (() => void) | undefined; 11 | contentDidUpdate?: (() => void) | undefined; 12 | children: React.ReactNode; 13 | } 14 | 15 | const FrameComponent: React.ForwardRefExoticComponent; 16 | export default FrameComponent; 17 | 18 | export interface FrameContextProps { 19 | document?: Document; 20 | window?: Window; 21 | } 22 | 23 | export const FrameContext: React.Context; 24 | 25 | export const FrameContextProvider: React.Provider; 26 | 27 | export const FrameContextConsumer: React.Consumer; 28 | 29 | export function useFrame(): FrameContextProps; 30 | } 31 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = { 2 | devtool: 'inline-source-map', 3 | resolve: { 4 | extensions: ['', '.js', '.jsx'] 5 | }, 6 | module: { 7 | noParse: [/node_modules\/sinon/], 8 | loaders: [ 9 | { test: /\.js(x|)$/, loader: 'babel-loader', exclude: /node_modules/ }, 10 | { test: /\.json$/, loader: 'json-loader' } 11 | ] 12 | } 13 | }; 14 | 15 | module.exports = function configure(config) { 16 | config.set({ 17 | basePath: '', 18 | files: [{ pattern: 'test/**/*.spec.js*', watched: true }], 19 | preprocessors: { 20 | 'test/**/*.spec.js*': ['webpack', 'sourcemap'] 21 | }, 22 | webpack, 23 | frameworks: ['mocha'], 24 | reporters: ['progress', 'osx'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: false, 29 | browsers: ['ChromeHeadlessNoSandbox'], 30 | customLaunchers: { 31 | ChromeHeadlessNoSandbox: { 32 | base: 'Chromium', 33 | flags: [ 34 | '--no-sandbox', 35 | '--headless', 36 | '--disable-gpu', 37 | '--remote-debugging-port=9222' 38 | ] 39 | } 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-frame-component", 3 | "version": "5.2.7", 4 | "description": "React component to wrap your application or component in an iFrame for encapsulation purposes", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "index.d.ts" 9 | ], 10 | "scripts": { 11 | "precommit": "lint-staged", 12 | "clean": "rimraf lib", 13 | "test": "npm-run-all --parallel lint karma:once --sequential build", 14 | "serve": "webpack-dev-server --host 0.0.0.0 --hot --inline --history-api-fallback", 15 | "start": "npm-run-all --parallel karma:dev serve", 16 | "karma:once": "karma start --single-run", 17 | "karma:dev": "karma start --browsers Chrome", 18 | "babel": "babel src -d lib", 19 | "build": "npm-run-all clean babel", 20 | "lint": "eslint '*.js' '{src,test}/**/*.js*'", 21 | "prepublish": "npm run build", 22 | "predeploy": "webpack", 23 | "deploy": "gh-pages -d dist", 24 | "publish": "npm run deploy" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/ryanseddon/react-frame-component.git" 29 | }, 30 | "keywords": [ 31 | "React", 32 | "component", 33 | "iFrame", 34 | "browser" 35 | ], 36 | "author": "Ryan Seddon", 37 | "contributors": [ 38 | "Chris Trevino " 39 | ], 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/ryanseddon/react-frame-component/issues" 43 | }, 44 | "homepage": "https://github.com/ryanseddon/react-frame-component", 45 | "devDependencies": { 46 | "babel-cli": "^6.18.0", 47 | "babel-core": "^6.21.0", 48 | "babel-eslint": "^7.1.1", 49 | "babel-loader": "^6.3.2", 50 | "babel-plugin-transform-class-properties": "^6.19.0", 51 | "babel-preset-latest": "^6.16.0", 52 | "babel-preset-react": "^6.16.0", 53 | "babel-preset-stage-2": "^6.18.0", 54 | "babel-register": "^6.18.0", 55 | "chai": "^3.5.0", 56 | "eslint": "^3.13.1", 57 | "eslint-config-airbnb": "^14.0.0", 58 | "eslint-config-prettier": "^2.9.0", 59 | "eslint-plugin-import": "^2.2.0", 60 | "eslint-plugin-jsx-a11y": "^3.0.2", 61 | "eslint-plugin-react": "^6.9.0", 62 | "gh-pages": "^1.1.0", 63 | "html-webpack-plugin": "^2.28.0", 64 | "husky": "^0.14.3", 65 | "karma": "^6.3.16", 66 | "karma-chrome-launcher": "^2.0.0", 67 | "karma-mocha": "^1.3.0", 68 | "karma-osx-reporter": "^0.2.1", 69 | "karma-sourcemap-loader": "^0.3.7", 70 | "karma-webpack": "^2.0.2", 71 | "lint-staged": "^7.1.3", 72 | "mocha": "^3.2.0", 73 | "mocha-junit-reporter": "^1.13.0", 74 | "mocha-multi": "^0.10.0", 75 | "mocha-osx-reporter": "^0.1.2", 76 | "npm-run-all": "^4.0.1", 77 | "prettier": "^1.13.4", 78 | "prop-types": "^15.5.9", 79 | "react": "^17.0.1", 80 | "react-dom": "^17.0.1", 81 | "rimraf": "^2.5.4", 82 | "sinon": "2.0.0-pre", 83 | "wallaby-webpack": "^3.9.16", 84 | "webpack": "1.x", 85 | "webpack-dev-server": "^1.16.3" 86 | }, 87 | "dependencies": {}, 88 | "lint-staged": { 89 | "**/*.{js,jsx}": [ 90 | "eslint --fix", 91 | "prettier --write", 92 | "git add" 93 | ] 94 | }, 95 | "peerDependencies": { 96 | "prop-types": "^15.5.9", 97 | "react": ">= 16.3", 98 | "react-dom": ">= 16.3" 99 | }, 100 | "prettier": { 101 | "singleQuote": true, 102 | "trailingComma": "none" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Content.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Children } from 'react'; // eslint-disable-line no-unused-vars 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class Content extends Component { 5 | static propTypes = { 6 | children: PropTypes.element.isRequired, 7 | contentDidMount: PropTypes.func.isRequired, 8 | contentDidUpdate: PropTypes.func.isRequired 9 | }; 10 | 11 | componentDidMount() { 12 | this.props.contentDidMount(); 13 | } 14 | 15 | componentDidUpdate() { 16 | this.props.contentDidUpdate(); 17 | } 18 | 19 | render() { 20 | return Children.only(this.props.children); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Context.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | let doc; 4 | let win; 5 | if (typeof document !== 'undefined') { 6 | doc = document; 7 | } 8 | if (typeof window !== 'undefined') { 9 | win = window; 10 | } 11 | 12 | export const FrameContext = React.createContext({ document: doc, window: win }); 13 | 14 | export const useFrame = () => React.useContext(FrameContext); 15 | 16 | export const { 17 | Provider: FrameContextProvider, 18 | Consumer: FrameContextConsumer 19 | } = FrameContext; 20 | -------------------------------------------------------------------------------- /src/Frame.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { FrameContextProvider } from './Context'; 5 | import Content from './Content'; 6 | 7 | export class Frame extends Component { 8 | // React warns when you render directly into the body since browser extensions 9 | // also inject into the body and can mess up React. For this reason 10 | // initialContent is expected to have a div inside of the body 11 | // element that we render react into. 12 | static propTypes = { 13 | style: PropTypes.object, // eslint-disable-line 14 | head: PropTypes.node, 15 | initialContent: PropTypes.string, 16 | mountTarget: PropTypes.string, 17 | contentDidMount: PropTypes.func, 18 | contentDidUpdate: PropTypes.func, 19 | children: PropTypes.oneOfType([ 20 | PropTypes.element, 21 | PropTypes.arrayOf(PropTypes.element) 22 | ]) 23 | }; 24 | 25 | static defaultProps = { 26 | style: {}, 27 | head: null, 28 | children: undefined, 29 | mountTarget: undefined, 30 | contentDidMount: () => {}, 31 | contentDidUpdate: () => {}, 32 | initialContent: 33 | '
' 34 | }; 35 | 36 | constructor(props, context) { 37 | super(props, context); 38 | this._isMounted = false; 39 | this.nodeRef = React.createRef(); 40 | this.state = { iframeLoaded: false }; 41 | } 42 | 43 | componentDidMount() { 44 | this._isMounted = true; 45 | 46 | const doc = this.getDoc(); 47 | 48 | if (doc) { 49 | this.nodeRef.current.contentWindow.addEventListener( 50 | 'DOMContentLoaded', 51 | this.handleLoad 52 | ); 53 | } 54 | } 55 | 56 | componentWillUnmount() { 57 | this._isMounted = false; 58 | 59 | this.nodeRef.current.removeEventListener( 60 | 'DOMContentLoaded', 61 | this.handleLoad 62 | ); 63 | } 64 | 65 | getDoc() { 66 | return this.nodeRef.current ? this.nodeRef.current.contentDocument : null; // eslint-disable-line 67 | } 68 | 69 | getMountTarget() { 70 | const doc = this.getDoc(); 71 | if (this.props.mountTarget) { 72 | return doc.querySelector(this.props.mountTarget); 73 | } 74 | return doc.body.children[0]; 75 | } 76 | 77 | setRef = node => { 78 | this.nodeRef.current = node; 79 | 80 | const { forwardedRef } = this.props; // eslint-disable-line react/prop-types 81 | if (typeof forwardedRef === 'function') { 82 | forwardedRef(node); 83 | } else if (forwardedRef) { 84 | forwardedRef.current = node; 85 | } 86 | }; 87 | 88 | handleLoad = () => { 89 | clearInterval(this.loadCheck); 90 | // Bail update as some browsers will trigger on both DOMContentLoaded & onLoad ala firefox 91 | if (!this.state.iframeLoaded) { 92 | this.setState({ iframeLoaded: true }); 93 | } 94 | }; 95 | 96 | // In certain situations on a cold cache DOMContentLoaded never gets called 97 | // fallback to an interval to check if that's the case 98 | loadCheck = () => 99 | setInterval(() => { 100 | this.handleLoad(); 101 | }, 500); 102 | 103 | renderFrameContents() { 104 | if (!this._isMounted) { 105 | return null; 106 | } 107 | 108 | const doc = this.getDoc(); 109 | 110 | if (!doc) { 111 | return null; 112 | } 113 | 114 | const contentDidMount = this.props.contentDidMount; 115 | const contentDidUpdate = this.props.contentDidUpdate; 116 | 117 | const win = doc.defaultView || doc.parentView; 118 | const contents = ( 119 | 123 | 124 |
{this.props.children}
125 |
126 |
127 | ); 128 | 129 | const mountTarget = this.getMountTarget(); 130 | 131 | if (!mountTarget) { 132 | return null; 133 | } 134 | 135 | return [ 136 | ReactDOM.createPortal(this.props.head, this.getDoc().head), 137 | ReactDOM.createPortal(contents, mountTarget) 138 | ]; 139 | } 140 | 141 | render() { 142 | const props = { 143 | ...this.props, 144 | srcDoc: this.props.initialContent, 145 | children: undefined // The iframe isn't ready so we drop children from props here. #12, #17 146 | }; 147 | delete props.head; 148 | delete props.initialContent; 149 | delete props.mountTarget; 150 | delete props.contentDidMount; 151 | delete props.contentDidUpdate; 152 | delete props.forwardedRef; 153 | 154 | return ( 155 | 158 | ); 159 | } 160 | } 161 | 162 | export default React.forwardRef((props, ref) => ( 163 | 164 | )); 165 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Frame'; 2 | 3 | export { FrameContext, FrameContextConsumer, useFrame } from './Context'; -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "react/no-render-return-value": "off", 7 | "react/no-find-dom-node": "off", 8 | "no-unused-expressions": "off", 9 | "react/no-multi-comp": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/Content.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import sinon from 'sinon/pkg/sinon'; 5 | import Content from '../src/Content'; 6 | 7 | describe('The Content component', () => { 8 | it('should render children', () => { 9 | const content = ReactTestUtils.renderIntoDocument( 10 | null} contentDidUpdate={() => null}> 11 |
12 | 13 | ); 14 | 15 | const div = ReactTestUtils.findRenderedDOMComponentWithTag(content, 'div'); 16 | expect(div.className).to.equal('test-class-1'); 17 | }); 18 | 19 | it('should call contentDidMount on initial render', () => { 20 | const didMount = sinon.spy(); 21 | const didUpdate = sinon.spy(); 22 | 23 | ReactTestUtils.renderIntoDocument( 24 | 25 |
26 | 27 | ); 28 | 29 | expect(didMount.callCount).to.equal(1); 30 | expect(didUpdate.callCount).to.equal(0); 31 | }); 32 | 33 | it('should call contentDidUpdate on subsequent updates', (done) => { 34 | const didMount = sinon.spy(); 35 | const didUpdate = sinon.spy(); 36 | 37 | const content = ReactTestUtils.renderIntoDocument( 38 | 39 |
40 | 41 | ); 42 | 43 | expect(didUpdate.callCount).to.equal(0); 44 | 45 | content.setState({ foo: 'bar' }, () => { 46 | expect(didMount.callCount).to.equal(1); 47 | expect(didUpdate.callCount).to.equal(1); 48 | 49 | content.setState({ foo: 'gah' }, () => { 50 | expect(didMount.callCount).to.equal(1); 51 | expect(didUpdate.callCount).to.equal(2); 52 | 53 | done(); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/Context.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import { 5 | FrameContextProvider, 6 | FrameContextConsumer, 7 | FrameContext, 8 | useFrame 9 | } from '../src/Context'; 10 | 11 | describe('The DocumentContext Component', () => { 12 | it('will establish context variables', done => { 13 | const document = { x: 1 }; 14 | const window = { y: 2 }; 15 | 16 | const Child = () => ( 17 | 18 | {({ document: doc, window: win }) => { 19 | expect(doc).to.equal(document); 20 | expect(win).to.equal(window); 21 | done(); 22 | return

{`x=${doc.x},y=${win.y}`}

; 23 | }} 24 |
25 | ); 26 | ReactTestUtils.renderIntoDocument( 27 | 28 | 29 | 30 | ); 31 | }); 32 | 33 | it('exports full context instance to allow accessing via Class.contextType', done => { 34 | const document = { foo: 1 }; 35 | const window = { bar: 2 }; 36 | 37 | class Child extends React.Component { 38 | componentDidMount() { 39 | const { document: doc, window: win } = this.context; 40 | expect(doc).to.deep.equal({ foo: 1 }); 41 | expect(win).to.deep.equal({ bar: 2 }); 42 | done(); 43 | } 44 | render() { 45 | return null; 46 | } 47 | } 48 | Child.contextType = FrameContext; 49 | 50 | ReactTestUtils.renderIntoDocument( 51 | 52 | 53 | 54 | ); 55 | }); 56 | 57 | it('exports full context instance to allow accessing via custom hook', done => { 58 | const document = { foo: 1 }; 59 | const window = { bar: 2 }; 60 | 61 | const Child = () => { 62 | const frame = useFrame(); 63 | 64 | React.useEffect(() => { 65 | expect(frame.document).to.deep.equal(document); 66 | expect(frame.window).to.deep.equal(window); 67 | done(); 68 | }, []); 69 | 70 | return null; 71 | }; 72 | 73 | ReactTestUtils.renderIntoDocument( 74 | 75 | 76 | 77 | ); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/Frame.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import ReactTestUtils from 'react-dom/test-utils'; 5 | import { expect } from 'chai'; 6 | import sinon from 'sinon/pkg/sinon'; 7 | import ForwardedRefFrame, { Frame } from '../src/Frame'; 8 | 9 | describe('The Frame Component', () => { 10 | let div; 11 | 12 | afterEach(() => { 13 | if (div) { 14 | div.remove(); 15 | div = null; 16 | } 17 | }); 18 | 19 | it('should create an empty iFrame', () => { 20 | const frame = ReactTestUtils.renderIntoDocument(); 21 | expect(frame.props.children).to.be.undefined; 22 | expect(ReactDOM.findDOMNode(frame).contentWindow).to.be.defined; 23 | }); 24 | 25 | it('should not pass this.props.children in iframe render', () => { 26 | sinon.spy(React, 'createElement'); 27 | const frame = ReactTestUtils.renderIntoDocument( 28 | 29 |
30 | 31 | ); 32 | 33 | expect(React.createElement.calledWith('iframe', null)); 34 | expect(frame.props.children).to.be.defined; 35 | }); 36 | 37 | it('should create an empty iFrame and apply inline styles', () => { 38 | const frame = ReactTestUtils.renderIntoDocument( 39 | 40 | ); 41 | expect(frame.props.style).to.deep.equal({ border: 0 }); 42 | expect(ReactDOM.findDOMNode(frame).style.border).to.contain('0'); 43 | }); 44 | 45 | it('should pass along all props to underlying iFrame', () => { 46 | const frame = ReactTestUtils.renderIntoDocument( 47 | 53 | ); 54 | const node = ReactDOM.findDOMNode(frame); 55 | expect(frame.props.className).to.equal('test-class-1 test-class-2'); 56 | expect(frame.props.frameBorder).to.equal(0); 57 | expect(frame.props.height).to.equal('100%'); 58 | expect(frame.props.width).to.equal('80%'); 59 | expect(node.className).to.equal('test-class-1 test-class-2'); 60 | expect(node.getAttribute('frameBorder')).to.equal('0'); 61 | expect(node.getAttribute('height')).to.equal('100%'); 62 | expect(node.getAttribute('width')).to.equal('80%'); 63 | }); 64 | 65 | it('should create an iFrame with a tag inside', done => { 66 | div = document.body.appendChild(document.createElement('div')); 67 | const frame = ReactDOM.render( 68 | } 70 | contentDidMount={() => { 71 | const head = ReactDOM.findDOMNode(frame).contentDocument.head; 72 | expect(head.querySelector('link')).to.be.defined; 73 | expect(head.querySelector('link').href).to.contain('styles.css'); 74 | done(); 75 | }} 76 | />, 77 | div 78 | ); 79 | }); 80 | 81 | it('should create an iFrame with a
'; 241 | const renderedContent = 242 | '
'; 243 | const frame = ReactDOM.render( 244 | { 247 | const doc = ReactDOM.findDOMNode(frame).contentDocument; 248 | expect(doc.documentElement.outerHTML).to.equal(renderedContent); 249 | done(); 250 | }} 251 | />, 252 | div 253 | ); 254 | }); 255 | 256 | it('should allow setting mountTarget', done => { 257 | div = document.body.appendChild(document.createElement('div')); 258 | 259 | const initialContent = 260 | "

i was here first

"; 261 | const frame = ReactDOM.render( 262 | { 266 | const doc = ReactDOM.findDOMNode(frame).contentDocument; 267 | expect(doc.querySelectorAll('h1').length).to.equal(2); 268 | done(); 269 | }} 270 | > 271 |

And i am joining you

272 | , 273 | div 274 | ); 275 | }); 276 | 277 | it('should call contentDidMount on initial render', done => { 278 | div = document.body.appendChild(document.createElement('div')); 279 | 280 | const didUpdate = sinon.spy(); 281 | const didMount = sinon.spy(() => { 282 | expect(didMount.callCount).to.equal(1, 'expected 1 didMount'); 283 | expect(didUpdate.callCount).to.equal(0, 'expected 0 didUpdate'); 284 | done(); 285 | }); 286 | ReactDOM.render( 287 | , 288 | div 289 | ); 290 | }); 291 | 292 | it('should call contentDidUpdate on subsequent updates', done => { 293 | div = document.body.appendChild(document.createElement('div')); 294 | const didUpdate = sinon.spy(); 295 | const didMount = sinon.spy(); 296 | const frame = ReactDOM.render( 297 | { 300 | didMount(); 301 | frame.setState({ foo: 'bar' }, () => { 302 | expect(didMount.callCount).to.equal(1, 'expected 1 didMount'); 303 | expect(didUpdate.callCount).to.equal(1, 'expected 1 didUpdate'); 304 | frame.setState({ foo: 'gah' }, () => { 305 | expect(didMount.callCount).to.equal(1, 'expected 1 didMount'); 306 | expect(didUpdate.callCount).to.equal(2, 'expected 2 didUpdate'); 307 | done(); 308 | }); 309 | }); 310 | }} 311 | />, 312 | div 313 | ); 314 | }); 315 | 316 | it('should return first child element of the `body` on call to `this.getMountTarget()` if `props.mountTarget` was not passed in', () => { 317 | div = document.body.appendChild(document.createElement('div')); 318 | 319 | const frame = ReactDOM.render(, div); 320 | const body = ReactDOM.findDOMNode(frame).contentDocument.body; 321 | 322 | expect(Frame.prototype.getMountTarget.call(frame)).to.equal( 323 | body.children[0] 324 | ); 325 | }); 326 | 327 | it('should return resolved `props.mountTarget` node on call to `this.getMountTarget()` if `props.mountTarget` was passed in', () => { 328 | div = document.body.appendChild(document.createElement('div')); 329 | const initialContent = 330 | "
"; 331 | 332 | const frame = ReactDOM.render( 333 | , 334 | div 335 | ); 336 | const body = ReactDOM.findDOMNode(frame).contentDocument.body; 337 | div = document.body.appendChild(document.createElement('div')); 338 | 339 | expect(Frame.prototype.getMountTarget.call(frame)).to.equal( 340 | body.querySelector('#container') 341 | ); 342 | }); 343 | 344 | it("should render null when `this.getMountTarget()` can't resolve", done => { 345 | const didMount = sinon.spy(); 346 | const getMountTarget = sinon 347 | .stub(Frame.prototype, 'getMountTarget') 348 | .returns(null); 349 | 350 | div = document.body.appendChild(document.createElement('div')); 351 | 352 | ReactDOM.render(, div); 353 | 354 | setTimeout(() => { 355 | expect(didMount.callCount).to.equal(0); 356 | done(); 357 | getMountTarget.restore(); 358 | }, 100); 359 | }); 360 | 361 | it('should not error when parent components are reused', done => { 362 | div = document.body.appendChild(document.createElement('div')); 363 | 364 | class Parent extends React.Component { 365 | constructor() { 366 | super(); 367 | this.ulRef = React.createRef(); 368 | this.loaded = 0; 369 | this.state = { 370 | p1: 'Test 1', 371 | p2: 'Test 2' 372 | }; 373 | } 374 | 375 | handleTest = () => { 376 | // wait for both Frames to load 377 | this.loaded = this.loaded + 1; 378 | if (this.loaded !== 2) { 379 | return; 380 | } 381 | 382 | const iframes1 = this.ulRef.current.querySelectorAll('iframe'); 383 | expect( 384 | iframes1[0].contentDocument.body.querySelector('p').textContent 385 | ).to.equal('Test 1'); 386 | expect( 387 | iframes1[1].contentDocument.body.querySelector('p').textContent 388 | ).to.equal('Test 2'); 389 | 390 | this.setState( 391 | { 392 | p1: 'Test 2', 393 | p2: 'Test 1' 394 | }, 395 | () => { 396 | const iframes2 = this.ulRef.current.querySelectorAll('iframe'); 397 | expect( 398 | iframes2[0].contentDocument.body.querySelector('p').textContent 399 | ).to.equal('Test 2'); 400 | expect( 401 | iframes1[1].contentDocument.body.querySelector('p').textContent 402 | ).to.equal('Test 1'); 403 | done(); 404 | } 405 | ); 406 | }; 407 | 408 | render() { 409 | return ( 410 |
    411 |
  • 412 | 413 |

    {this.state.p1}

    414 | 415 |
  • 416 |
  • 417 | 418 |

    {this.state.p2}

    419 | 420 |
  • 421 |
422 | ); 423 | } 424 | } 425 | 426 | ReactDOM.render(, div); 427 | }); 428 | 429 | it('should not error when the root component is removed', () => { 430 | div = document.body.appendChild(document.createElement('div')); 431 | ReactDOM.render(, div); 432 | div.remove(); 433 | ReactDOM.render(, div); 434 | }); 435 | 436 | it('should not error when root component is re-appended', done => { 437 | div = document.body.appendChild(document.createElement('div')); 438 | ReactDOM.render(, div); 439 | ReactDOM.render( 440 | { 442 | const iframes = ReactDOM.findDOMNode(div).querySelectorAll('iframe'); 443 | 444 | expect(iframes[0].contentDocument.body.children.length).to.equal(1); 445 | expect(iframes[0].contentDocument.body.children.length).to.equal(1); 446 | done(); 447 | }} 448 | />, 449 | div 450 | ); 451 | }); 452 | 453 | it('should properly assign ref prop', done => { 454 | div = document.body.appendChild(document.createElement('div')); 455 | 456 | const ref = sinon.spy(iframe => { 457 | expect(iframe instanceof HTMLIFrameElement).to.equal(true); 458 | done(); 459 | }); 460 | 461 | ReactDOM.render(, div); 462 | }); 463 | }); 464 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | const wallabyWebpack = require('wallaby-webpack'); // eslint-disable-line import/no-extraneous-dependencies 2 | 3 | module.exports = function configure(wallaby) { 4 | const wallabyPostprocessor = wallabyWebpack({ 5 | resolve: { 6 | extensions: ['', '.js', '.jsx'] 7 | }, 8 | module: { 9 | noParse: [ 10 | /node_modules\/sinon/ 11 | ], 12 | loaders: [ 13 | { test: /\.json$/, loader: 'json-loader' } 14 | ] 15 | } 16 | }); 17 | 18 | return { 19 | debug: true, 20 | files: [ 21 | { pattern: 'src/**/*.js*', load: false }, 22 | { pattern: 'app/reducers/**/*.js*', load: false }, 23 | { pattern: 'app/components/**/*.js*', load: false } 24 | ], 25 | tests: [ 26 | { pattern: 'test/**/*.spec.js*', load: false } 27 | ], 28 | compilers: { 29 | '**/*.js?(x)': wallaby.compilers.babel() 30 | }, 31 | postprocessor: wallabyPostprocessor, 32 | testFramework: 'mocha', 33 | env: { 34 | kind: 'electron' 35 | }, 36 | setup: function setup() { 37 | window.__moduleBundler.loadTests(); 38 | } 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | context: path.join(__dirname), 7 | entry: { 8 | javascript: './example/app.jsx' 9 | }, 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'appbundle.js' 13 | }, 14 | resolve: { 15 | extensions: ['.js', '.jsx', ''] 16 | }, 17 | plugins: [ 18 | new HtmlWebpackPlugin({ 19 | title: 'React Frame Component Simple Example', 20 | template: './example/index.html' 21 | }) 22 | ], 23 | module: { 24 | loaders: [ 25 | { test: /\.js(x|)/, loaders: ['babel-loader'], exclude: /node_modules/ } 26 | ] 27 | }, 28 | devServer: { 29 | headers: { 30 | 'Cache-Control': 'max-age=10' 31 | } 32 | } 33 | }; 34 | --------------------------------------------------------------------------------