diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index 65bd2e50b5..3c37d256b8 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -788,8 +788,24 @@ const ComponentsPage = React.createClass({ + {/* Utilities */} +
+

Utilities Portal

+ +

Portal

+

+ A Component that renders its children into a new React "subtree" or container. The Portal component kind of like the React + equivillent to jQuery's .appendTo(), which is helpful for components that need to be appended to a DOM node other than + the component's direct parent. The Modal, and Overlay components use the Portal component internally. +

+

Props

+ + +
+ +
Glyphicons Tables Input + Input Back to top diff --git a/src/OverlayMixin.js b/src/OverlayMixin.js index 76524ed61c..ff9f312e03 100644 --- a/src/OverlayMixin.js +++ b/src/OverlayMixin.js @@ -1,33 +1,42 @@ import React from 'react'; import CustomPropTypes from './utils/CustomPropTypes'; import domUtils from './utils/domUtils'; +import deprecationWarning from './utils/deprecationWarning'; -export default { +export const OverlayMixin = { propTypes: { + container: CustomPropTypes.mountable }, - componentWillUnmount() { - this._unrenderOverlay(); - if (this._overlayTarget) { - this.getContainerDOMNode() - .removeChild(this._overlayTarget); - this._overlayTarget = null; - } + + componentDidMount() { + this._renderOverlay(); }, componentDidUpdate() { this._renderOverlay(); }, - componentDidMount() { - this._renderOverlay(); + componentWillUnmount() { + this._unrenderOverlay(); + this._mountOverlayTarget(); }, _mountOverlayTarget() { - this._overlayTarget = document.createElement('div'); - this.getContainerDOMNode() - .appendChild(this._overlayTarget); + if (!this._overlayTarget) { + this._overlayTarget = document.createElement('div'); + this.getContainerDOMNode() + .appendChild(this._overlayTarget); + } + }, + + _unmountOverlayTarget() { + if (this._overlayTarget) { + this.getContainerDOMNode() + .removeChild(this._overlayTarget); + this._overlayTarget = null; + } }, _renderOverlay() { @@ -39,10 +48,12 @@ export default { // Save reference to help testing if (overlay !== null) { + this._mountOverlayTarget(); this._overlayInstance = React.render(overlay, this._overlayTarget); } else { // Unrender if the component is null for transitions to null this._unrenderOverlay(); + this._unmountOverlayTarget(); } }, @@ -67,3 +78,14 @@ export default { return React.findDOMNode(this.props.container) || domUtils.ownerDocument(this).body; } }; + +export default { + + ...OverlayMixin, + + componentWillMount() { + deprecationWarning( + 'Overlay mixin', 'the `` Component' + , 'http://react-bootstrap.github.io/components.html#modals-custom'); + } +}; diff --git a/src/Portal.js b/src/Portal.js new file mode 100644 index 0000000000..cc8f350701 --- /dev/null +++ b/src/Portal.js @@ -0,0 +1,34 @@ +import React from 'react'; +import CustomPropTypes from './utils/CustomPropTypes'; +import { OverlayMixin } from './OverlayMixin'; + +let Portal = React.createClass({ + + displayName: 'Portal', + + propTypes: { + /** + * The DOM Node that the Component will render it's children into + */ + container: CustomPropTypes.mountable + }, + + // we use the mixin for now, to avoid duplicating a bunch of code. + // when the deprecation is removed we need to move the logic here from OverlayMixin + mixins: [ OverlayMixin ], + + renderOverlay() { + if (!this.props.children) { + return null; + } + + return React.Children.only(this.props.children); + }, + + render() { + return null; + } +}); + + +export default Portal; diff --git a/src/index.js b/src/index.js index 7d2d8e8e85..437008dabb 100644 --- a/src/index.js +++ b/src/index.js @@ -54,6 +54,8 @@ import utils from './utils'; import Well from './Well'; import styleMaps from './styleMaps'; +import Portal from './Portal'; + export default { Accordion, Affix, @@ -98,6 +100,7 @@ export default { Pager, Pagination, Popover, + Portal, ProgressBar, Row, SplitButton, diff --git a/test/PortalSpec.js b/test/PortalSpec.js new file mode 100644 index 0000000000..66a1b49fc0 --- /dev/null +++ b/test/PortalSpec.js @@ -0,0 +1,78 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import Portal from '../src/Portal'; + +describe('Portal', function () { + let instance; + + let Overlay = React.createClass({ + render() { + return ( +
+ {this.props.overlay} +
+ ); + }, + getOverlayDOMNode(){ + return this.refs.p.getOverlayDOMNode(); + } + }); + + afterEach(function() { + if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { + React.unmountComponentAtNode(React.findDOMNode(instance)); + } + }); + + it('Should render overlay into container (DOMNode)', function() { + let container = document.createElement('div'); + + instance = ReactTestUtils.renderIntoDocument( + } /> + ); + + assert.equal(container.querySelectorAll('#test1').length, 1); + }); + + it('Should render overlay into container (ReactComponent)', function() { + let Container = React.createClass({ + render() { + return } />; + } + }); + + instance = ReactTestUtils.renderIntoDocument( + + ); + + assert.equal(React.findDOMNode(instance).querySelectorAll('#test1').length, 1); + }); + + it('Should not render a null overlay', function() { + let Container = React.createClass({ + render() { + return ; + } + }); + + instance = ReactTestUtils.renderIntoDocument( + + ); + + assert.equal(instance.refs.overlay.getOverlayDOMNode(), null); + }); + + it('Should render only an overlay', function() { + let OnlyOverlay = React.createClass({ + render() { + return {this.props.overlay}; + } + }); + + let overlayInstance = ReactTestUtils.renderIntoDocument( + } /> + ); + + assert.equal(overlayInstance.refs.p.getOverlayDOMNode().nodeName, 'DIV'); + }); +});