├── dist └── .gitignore ├── .npmignore ├── src ├── index.js ├── Demo.css ├── Modal.js ├── Modal.css ├── Demo.js └── MountAtSelector.js ├── public └── index.html ├── .gitignore ├── package.json └── README.md /dist/.gitignore: -------------------------------------------------------------------------------- 1 | # empty file to ensure folder /dist exists -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .idea 4 | .gitignore 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Demo from './Demo' 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('app') 8 | ) 9 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MountAtSelector demo 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | dist 4 | 5 | # dependencies 6 | node_modules 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | npm-debug.log 18 | .idea 19 | -------------------------------------------------------------------------------- /src/Demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: lightblue; 3 | font-family: sans-serif; 4 | } 5 | 6 | .content { 7 | background: white; 8 | position: relative; 9 | width: 500px; 10 | padding: 10px; 11 | border: 1px solid black; 12 | height: 300px; 13 | transform: translate(0px, 0px); 14 | margin: 50px; 15 | } 16 | -------------------------------------------------------------------------------- /src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './Modal.css' 3 | import MountAtSelector from './MountAtSelector' 4 | 5 | export default class Modal extends Component { 6 | render() { 7 | return 8 |
9 |
10 | {this.props.children} 11 |
12 |
13 |
14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Modal.css: -------------------------------------------------------------------------------- 1 | 2 | .modal { 3 | background-color: rgba(0, 0, 0, .4); 4 | height: 100%; 5 | overflow: auto; 6 | position: fixed; 7 | left: 0; 8 | top: 0; 9 | width: 100%; 10 | z-index: 10000; 11 | } 12 | 13 | .modal.container { 14 | background-color: white; 15 | position: relative; 16 | border: 1px solid black; 17 | border-radius: 4px; 18 | margin-left: auto; 19 | margin-right: auto; 20 | max-width: 400px; 21 | min-width: 400px; 22 | margin-top: 100px; 23 | padding: 10px; 24 | height: auto; 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mount-at-selector", 3 | "version": "1.0.3", 4 | "description": "Render React components like a Modal outside the main React app", 5 | "keywords": [ 6 | "mount", 7 | "render", 8 | "attach", 9 | "modal", 10 | "root", 11 | "dom" 12 | ], 13 | "main": "./dist/MountAtSelector.js", 14 | "author": "Jos de Jong", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/josdejong/mount-at-selector.git" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "6.18.0", 22 | "babel-preset-react-app": "2.0.1", 23 | "react-scripts": "0.8.4" 24 | }, 25 | "dependencies": { 26 | "react": "15.4.1", 27 | "react-dom": "15.4.1" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "BABEL_ENV=production babel ./src/MountAtSelector.js -o ./dist/MountAtSelector.js --presets=react-app", 32 | "prepublish": "npm run build", 33 | "eject": "react-scripts eject" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Demo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './Demo.css' 3 | import Modal from './Modal' 4 | 5 | export default class Demo extends Component { 6 | state = { 7 | showModal: false 8 | } 9 | 10 | render() { 11 | return
12 |

13 | Content 1 14 |

15 |

16 | 17 |

18 | 19 | { 20 | this.renderModal() 21 | } 22 | 23 |
24 | } 25 | 26 | renderModal () { 27 | if (this.state.showModal) { 28 | return 29 |

30 | I'm a modal. 31 |

32 |

33 | 34 |

35 |
36 | } 37 | else { 38 | return null 39 | } 40 | } 41 | 42 | showModal = () => { 43 | this.setState({ showModal: true }) 44 | } 45 | 46 | hideModal = () => { 47 | this.setState({ showModal: false }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MountAtSelector 2 | 3 | This React component allows rendering components like a Modal outside the main React app. 4 | 5 | Inspired by a blog of Ryan Zec: https://blog.komand.com/how-to-render-components-outside-the-main-react-app 6 | 7 | 8 | # Install 9 | 10 | ``` 11 | npm install --save mount-at-selector 12 | ``` 13 | 14 | 15 | # Use 16 | 17 | Create an extra container like `#myModal` in your HTML: 18 | 19 | 20 | ```html 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | ``` 32 | 33 | Create a `MountAtSelector` component in your React app: 34 | 35 | ```jsx 36 | import React from 'react' 37 | import ReactDOM from 'react-dom' 38 | import MountAtSelector from 'mount-at-selector' 39 | 40 | ReactDOM.render( 41 |
42 |

43 | Content rendered inside the React app... 44 |

45 | 46 | 47 |
48 | I'm rendered inside the #myModal element, 49 | outside of the React app. 50 |
51 |
52 |
, 53 | document.getElementById('app') 54 | ) 55 | ``` 56 | 57 | # Demo 58 | 59 | See the folder `/src` for a full demo. To run the demo, clone the git project 60 | and run `npm install; npm start` in the root of the project. 61 | 62 | 63 | # API 64 | 65 | ``` 66 | 67 | ... 68 | 69 | ``` 70 | 71 | Properties: 72 | 73 | - `selector`
74 | A query selector like `'#myModal'` or a DOM element like 75 | `document.getElementById('myModal)`. You can attach 76 | multiple `MountAtSelector` components at the same selector. 77 | 78 | 79 | # License 80 | 81 | MIT -------------------------------------------------------------------------------- /src/MountAtSelector.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import { render, unmountComponentAtNode } from 'react-dom' 3 | 4 | /** 5 | * Usage: 6 | * 7 | * 8 | *
9 | * I'm rendered inside the #myModal element, 10 | * outside of the current React app. 11 | *
12 | *
13 | * 14 | * Properties: 15 | * 16 | * - `selector` A query selector like '#myModal' or a DOM element like 17 | * document.getElementById('myModal). You can attach 18 | * multiple MountAtSelector components at the same selector. 19 | * 20 | */ 21 | export default class MountAtSelector extends Component { 22 | renderedSelector = null 23 | container = null // the DOM container where the children or MountAtSelector are currently rendered 24 | 25 | render() { 26 | // don't render anything in the parent component, 27 | // we will render the children in a separate root 28 | return null 29 | } 30 | 31 | renderChildren () { 32 | const selector = this.getSelector() 33 | 34 | if (this.renderedSelector && selector !== this.renderedSelector) { 35 | // selector changed. Unmount rendered content at the old selector 36 | this.unrenderChildren() 37 | this.renderedSelector = selector 38 | } 39 | 40 | if (selector) { 41 | if (!this.container) { 42 | // create a container inside the selector and render stuff there, 43 | // so we can create and destroy multiple MountAtSelector components 44 | // in the same selector. 45 | this.container = document.createElement('div') 46 | this.container.className = 'mount-at-selector-container' 47 | selector.appendChild(this.container) 48 | } 49 | 50 | render(this.props.children, this.container) 51 | } 52 | else { 53 | console.error('Selector not found in MountAtSelector component') 54 | } 55 | } 56 | 57 | unrenderChildren () { 58 | if (this.renderedSelector) { 59 | unmountComponentAtNode(this.renderedSelector) 60 | this.renderedSelector = null 61 | } 62 | 63 | if (this.container && this.container.parentNode) { 64 | this.container.parentNode.removeChild(this.container) 65 | this.container = null 66 | } 67 | } 68 | 69 | getSelector() { 70 | // a query selector like '#myModal' 71 | if (typeof this.props.selector === 'string') { 72 | return document.querySelector(this.props.selector) 73 | } 74 | 75 | // a DOM element like document.getElementById('myModal) 76 | if (this.props.selector !== undefined) { 77 | return this.props.selector 78 | } 79 | 80 | // hm, not good 81 | return null 82 | } 83 | 84 | componentDidMount() { 85 | this.renderChildren() 86 | } 87 | 88 | componentDidUpdate() { 89 | this.renderChildren() 90 | } 91 | 92 | componentWillUnmount() { 93 | this.unrenderChildren() 94 | } 95 | } 96 | 97 | MountAtSelector.propTypes = { 98 | selector: function validateSelector (props, propName, componentName) { 99 | if (typeof props[propName] !== 'string' && props[propName] instanceof Element === false) { 100 | return new Error( 101 | 'Invalid prop `' + propName + '` supplied to' + 102 | ' `' + componentName + '`. String or DOM Element expected.' 103 | ) 104 | } 105 | } 106 | } 107 | 108 | --------------------------------------------------------------------------------