humanPursuit
11/28/2017 - 8:39 AM

Legacy React Portal

import PropTypes from 'prop-types';
import componentOrElement from 'prop-types-extra/lib/componentOrElement';
import React from 'react';
import ReactDOM from 'react-dom';

import getContainer from './utils/getContainer';
import ownerDocument from './utils/ownerDocument';

/**
 * The `<Portal/>` component renders its children into a new "subtree" outside of current component hierarchy.
 * You can think of it as a declarative `appendChild()`, or jQuery's `$.fn.appendTo()`.
 * The children of `<Portal/>` component will be appended to the `container` specified.
 */
class Portal extends React.Component {
    static displayName = 'Portal';

    static propTypes = {
        /**
         * A Node, Component instance, or function that returns either. The `container` will have the Portal children
         * appended to it.
         */
        container: PropTypes.oneOfType([
            componentOrElement,
            PropTypes.func
        ]),

        onRendered: PropTypes.func,
    };

    componentDidMount() {
        this._isMounted = true;
        this._renderOverlay();
    }

    componentDidUpdate() {
        this._renderOverlay();
    }

    componentWillReceiveProps(nextProps) {
        if (this._overlayTarget && nextProps.container !== this.props.container) {
            this._portalContainerNode.removeChild(this._overlayTarget);
            this._portalContainerNode = getContainer(nextProps.container, ownerDocument(this).body);
            this._portalContainerNode.appendChild(this._overlayTarget);
        }
    }

    componentWillUnmount() {
        this._isMounted = false;
        this._unrenderOverlay();
        this._unmountOverlayTarget();
    }


    _mountOverlayTarget = () => {
        if (!this._overlayTarget) {
            this._overlayTarget = document.createElement('div');
            this._portalContainerNode = getContainer(this.props.container, ownerDocument(this).body);
            this._portalContainerNode.appendChild(this._overlayTarget);
        }
    };

    _unmountOverlayTarget = () => {
        if (this._overlayTarget) {
            this._portalContainerNode.removeChild(this._overlayTarget);
            this._overlayTarget = null;
        }
        this._portalContainerNode = null;
    };

    _renderOverlay = () => {
        let overlay = !this.props.children
            ? null
            : React.Children.only(this.props.children);

        // Save reference for future access.
        if (overlay !== null) {
            this._mountOverlayTarget();

            const initialRender = !this._overlayInstance;

            this._overlayInstance = ReactDOM.unstable_renderSubtreeIntoContainer(
                this, overlay, this._overlayTarget, () => {
                    if (initialRender && this.props.onRendered) {
                        this.props.onRendered();
                    }
                }
            );
        } else {
            // Unrender if the component is null for transitions to null
            this._unrenderOverlay();
            this._unmountOverlayTarget();
        }
    };

    _unrenderOverlay = () => {
        if (this._overlayTarget) {
            ReactDOM.unmountComponentAtNode(this._overlayTarget);
            this._overlayInstance = null;
        }
    };

    render() {
        return null;
    }

    getMountNode = () => {
        return this._overlayTarget;
    };
}

export default Portal;