react-reverse-portal
Version:
Build an element once, move it anywhere
195 lines • 9.35 kB
JavaScript
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
import * as React from 'react';
import * as ReactDOM from 'react-dom';
// Internally, the portalNode must be for either HTML or SVG elements
var ELEMENT_TYPE_HTML = 'html';
var ELEMENT_TYPE_SVG = 'svg';
// ReactDOM can handle several different namespaces, but they're not exported publicly
// https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/shared/DOMNamespaces.js#L8-L10
var SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
var validateElementType = function (domElement, elementType) {
var _a, _b, _c;
var ownerDocument = ((_a = domElement.ownerDocument) !== null && _a !== void 0 ? _a : document);
// Cast document to `any` because Typescript doesn't know about the legacy `Document.parentWindow` field, and also
// doesn't believe `Window.HTMLElement`/`Window.SVGElement` can be used in instanceof tests.
var ownerWindow = (_c = (_b = ownerDocument.defaultView) !== null && _b !== void 0 ? _b : ownerDocument.parentWindow) !== null && _c !== void 0 ? _c : window; // `parentWindow` for IE8 and earlier
switch (elementType) {
case ELEMENT_TYPE_HTML:
return domElement instanceof ownerWindow.HTMLElement;
case ELEMENT_TYPE_SVG:
return domElement instanceof ownerWindow.SVGElement;
default:
throw new Error("Unrecognized element type \"".concat(elementType, "\" for validateElementType."));
}
};
// This is the internal implementation: the public entry points set elementType to an appropriate value
var createPortalNode = function (elementType, options) {
var _a, _b;
var initialProps = {};
var parent;
var lastPlaceholder;
var element;
switch (elementType) {
case ELEMENT_TYPE_HTML:
element = document.createElement((_a = options === null || options === void 0 ? void 0 : options.containerElement) !== null && _a !== void 0 ? _a : 'div');
break;
case ELEMENT_TYPE_SVG:
element = document.createElementNS(SVG_NAMESPACE, (_b = options === null || options === void 0 ? void 0 : options.containerElement) !== null && _b !== void 0 ? _b : 'g');
break;
default:
throw new Error("Invalid element type \"".concat(elementType, "\" for createPortalNode: must be \"html\" or \"svg\"."));
}
if (options && typeof options === "object" && options.attributes) {
for (var _i = 0, _c = Object.entries(options.attributes); _i < _c.length; _i++) {
var _d = _c[_i], key = _d[0], value = _d[1];
element.setAttribute(key, value);
}
}
var portalNode = {
element: element,
elementType: elementType,
setPortalProps: function (props) {
initialProps = props;
},
getInitialPortalProps: function () {
return initialProps;
},
mount: function (newParent, newPlaceholder) {
if (newPlaceholder === lastPlaceholder) {
// Already mounted - noop.
return;
}
portalNode.unmount();
// To support SVG and other non-html elements, the portalNode's elementType needs to match
// the elementType it's being rendered into
if (newParent !== parent) {
if (!validateElementType(newParent, elementType)) {
throw new Error("Invalid element type for portal: \"".concat(elementType, "\" portalNodes must be used with ").concat(elementType, " elements, but OutPortal is within <").concat(newParent.tagName, ">."));
}
}
newParent.replaceChild(portalNode.element, newPlaceholder);
parent = newParent;
lastPlaceholder = newPlaceholder;
},
unmount: function (expectedPlaceholder) {
if (expectedPlaceholder && expectedPlaceholder !== lastPlaceholder) {
// Skip unmounts for placeholders that aren't currently mounted
// They will have been automatically unmounted already by a subsequent mount()
return;
}
if (parent && lastPlaceholder) {
parent.replaceChild(lastPlaceholder, portalNode.element);
parent = undefined;
lastPlaceholder = undefined;
}
}
};
return portalNode;
};
var InPortal = /** @class */ (function (_super) {
__extends(InPortal, _super);
function InPortal(props) {
var _this = _super.call(this, props) || this;
_this.addPropsChannel = function () {
Object.assign(_this.props.node, {
setPortalProps: function (props) {
// Rerender the child node here if/when the out portal props change
_this.setState({ nodeProps: props });
}
});
};
_this.state = {
nodeProps: _this.props.node.getInitialPortalProps(),
};
return _this;
}
InPortal.prototype.componentDidMount = function () {
this.addPropsChannel();
};
InPortal.prototype.componentDidUpdate = function () {
this.addPropsChannel();
};
InPortal.prototype.render = function () {
var _this = this;
var _a = this.props, children = _a.children, node = _a.node;
return ReactDOM.createPortal(React.Children.map(children, function (child) {
if (!React.isValidElement(child))
return child;
return React.cloneElement(child, _this.state.nodeProps);
}), node.element);
};
return InPortal;
}(React.PureComponent));
var OutPortal = /** @class */ (function (_super) {
__extends(OutPortal, _super);
function OutPortal(props) {
var _this = _super.call(this, props) || this;
_this.placeholderNode = React.createRef();
_this.passPropsThroughPortal();
return _this;
}
OutPortal.prototype.passPropsThroughPortal = function () {
var propsForTarget = Object.assign({}, this.props, { node: undefined });
this.props.node.setPortalProps(propsForTarget);
};
OutPortal.prototype.componentDidMount = function () {
var node = this.props.node;
this.currentPortalNode = node;
var placeholder = this.placeholderNode.current;
var parent = placeholder.parentNode;
node.mount(parent, placeholder);
this.passPropsThroughPortal();
};
OutPortal.prototype.componentDidUpdate = function () {
// We re-mount on update, just in case we were unmounted (e.g. by
// a second OutPortal, which has now been removed)
var node = this.props.node;
// If we're switching portal nodes, we need to clean up the current one first.
if (this.currentPortalNode && node !== this.currentPortalNode) {
this.currentPortalNode.unmount(this.placeholderNode.current);
this.currentPortalNode.setPortalProps({});
this.currentPortalNode = node;
}
var placeholder = this.placeholderNode.current;
var parent = placeholder.parentNode;
node.mount(parent, placeholder);
this.passPropsThroughPortal();
};
OutPortal.prototype.componentWillUnmount = function () {
var node = this.props.node;
node.unmount(this.placeholderNode.current);
node.setPortalProps({});
};
OutPortal.prototype.render = function () {
// Render a placeholder to the DOM, so we can get a reference into
// our location in the DOM, and swap it out for the portaled node.
var tagName = this.props.node.element.tagName;
// SVG tagName is lowercase and case sensitive, HTML is uppercase and case insensitive.
// React.createElement expects lowercase first letter to treat as non-component element.
// (Passing uppercase type won't break anything, but React warns otherwise:)
// https://github.com/facebook/react/blob/8039f1b2a05d00437cd29707761aeae098c80adc/CHANGELOG.md?plain=1#L1984
var type = this.props.node.elementType === ELEMENT_TYPE_HTML
? tagName.toLowerCase()
: tagName;
return React.createElement(type, { ref: this.placeholderNode });
};
return OutPortal;
}(React.PureComponent));
var createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML);
var createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG);
export { createHtmlPortalNode, createSvgPortalNode, InPortal, OutPortal, };
//# sourceMappingURL=index.js.map