Skip to content

Instantly share code, notes, and snippets.

@aapoalas
Last active July 9, 2018 20:06
Show Gist options
  • Save aapoalas/b5a49849c458b124d599d3950034e507 to your computer and use it in GitHub Desktop.
Save aapoalas/b5a49849c458b124d599d3950034e507 to your computer and use it in GitHub Desktop.
React Component supporting reparenting through a manual DOM hack. Presume MIT licence.
define([
"react",
"react-dom",
"prop-types"
], function(
React,
ReactDOM,
PropTypes
) {
"use strict";
const { PureComponent } = React;
const ATTRIBUTE_MAP = Object.freeze({
acceptCharset: "accept-charset",
httpEquiv: "http-equiv"
});
const getAttrName = attr => ATTRIBUTE_MAP[attr] || attr;
const store = {};
const getOrCreateMountNode = (uid, type = "div") => {
if (!store[uid]) {
const mountNode = document.createElement(type);
store[uid] = {
mountNode,
inUse: true
};
} else {
store[uid].inUse = true;
if (store[uid].mountNode.nodeName.toLowerCase() !== type) {
const newNode = document.createElement(type);
ReactDOM.unmountComponentAtNode(store[uid].mountNode);
store[uid].mountNode = newNode;
}
}
return store[uid].mountNode;
};
const calculateChangeSet = (prev = {}, next = {}) => {
const set = [];
const del = Object.entries(prev).reduce((acc, [name, value]) => {
const nextValue = next[name];
if (nextValue === undefined || nextValue === null || nextValue === "") {
return acc.concat(name);
} else if (value !== nextValue) {
set.push([name, value]);
}
return acc;
}, []);
Object.entries(next).forEach(entry => {
const [name, value] = entry;
if (prev.hasOwnProperty(name) || value === undefined || value === null || value === "") {
// Already handled or invalid
return;
}
set.push(entry);
});
return {
del,
set
};
};
const updateNodeStyle = (
node,
prevClassName,
nextClassName,
prevStyle,
nextStyle,
prevAttrs,
nextAttrs
) => {
const {
del: delAttrs,
set: setAttrs
} = calculateChangeSet(prevAttrs, nextAttrs);
delAttrs.map(name => node.removeAttribute(name));
setAttrs.map(([name, value]) => node.setAttribute(getAttrName(name), value));
const {
del: delStyle,
set: setStyle
} = calculateChangeSet(prevStyle, nextStyle);
delStyle.map(name => node.style[name] = "");
setStyle.map(([name, value]) => node.style[name] = value);
if (setStyle.length === 0 && node.style.cssText === "") {
// We should remove the empty style attribute
node.removeAttribute("style");
}
if (prevClassName !== nextClassName) {
(nextClassName && nextClassName.length > 0) ?
node.setAttribute("class", nextClassName) :
node.removeAttribute("class");
}
};
// TODO: Diff with existing attributes if any present
const setNodeStyle = (node, className = "", style = {}, attrs = {}) => {
if (node.hasAttributes()) {
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
node.removeAttribute(name);
}
}
if (typeof attrs === "object" && style !== null) {
Object.entries(attrs).map(([name, value]) => node.setAttribute(getAttrName(name), value));
}
if (typeof style === "object" && style !== null) {
Object.entries(style).map(([name, value]) => node.style[name] = value);
}
if (typeof className === "string" && className.length > 0) {
node.setAttribute("class", className);
}
};
const removeMountNode = uid => {
const record = store[uid];
record.inUse = false;
setTimeout(() =>
!store[uid].inUse && (ReactDOM.unmountComponentAtNode(store[uid].mountNode) && delete store[uid]), 100);
};
const Reparentable = class Reparentable extends PureComponent {
componentDidMount() {
const {
className,
el,
style,
type,
uid
} = this.props;
const mountNode = getOrCreateMountNode(uid, type);
setNodeStyle(mountNode, className, style);
this.el = el;
this.el.appendChild(mountNode);
this.renderContentIntoNode(mountNode);
}
componentDidUpdate({
className: prevClassName,
el: prevEl,
style: prevStyle,
type: prevType,
uid: prevUid
}) {
const {
className,
el,
style,
type,
uid
} = this.props;
const mountNode = getOrCreateMountNode(uid, type);
updateNodeStyle(mountNode, prevClassName, className, prevStyle, style);
if (el !== prevEl) {
prevEl.removeChild(mountNode);
this.el = el;
prevEl.appendChild(mountNode);
}
this.renderContentIntoNode(mountNode);
}
renderContentIntoNode(node) {
const { children } = this.props;
ReactDOM.unstable_renderSubtreeIntoContainer(this, children, node);
}
render() {
return null;
}
componentWillUnmount() {
removeMountNode(this.props.uid);
}
};
Reparentable.propTypes = {
className: PropTypes.string,
el: PropTypes.instanceOf(Element).isRequired,
style: PropTypes.object,
type: PropTypes.string.isRequired,
uid: PropTypes.string.isRequired
};
Reparentable.defaultProps = {
className: "",
style: {},
type: "div"
};
return (type, props, children) => h(Reparentable, Object.assign({ type }, props), children);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment