Skip to content

Commit b7b65f0

Browse files
committed
Create the dom element for the portal on mount, after we know what it needs to be.
This was squashed from an experimental branch
1 parent e296b92 commit b7b65f0

File tree

3 files changed

+139
-31
lines changed

3 files changed

+139
-31
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
],
1414
"scripts": {
1515
"build": "rimraf dist/ && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.web.json",
16+
"build:watch": "tsc -p tsconfig.esm.json --watch",
1617
"pretest": "npm run build",
1718
"test": "echo \"It built ok, that'll do for now\"",
1819
"prepare": "npm run build",

src/index.tsx

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
import * as React from 'react';
22
import * as ReactDOM from 'react-dom';
33

4+
console.log('React = ', React)
5+
console.log('ReactDOM = ', ReactDOM)
6+
7+
// These namespaces come from react-dom, which does not export them publicly
8+
// https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/shared/DOMNamespaces.js#L8-L16
9+
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
10+
const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
11+
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
12+
export const Namespaces = {
13+
html: HTML_NAMESPACE,
14+
mathml: MATH_NAMESPACE,
15+
svg: SVG_NAMESPACE,
16+
};
17+
418
type Component<P> = React.Component<P> | React.ComponentType<P>;
519

620
type ComponentProps<C extends Component<any>> = C extends Component<infer P> ? P : never;
721

8-
export interface PortalNode<C extends Component<any> = Component<any>> extends HTMLElement {
22+
type PortalNodeElement = HTMLElement | SVGElement;
23+
24+
export interface PortalNode<C extends Component<any> = Component<any>> {
25+
element: PortalNodeElement,
926
// Used by the out portal to send props back to the real element
1027
// Hooked by InPortal to become a state update (and thus rerender)
1128
setPortalProps(p: ComponentProps<C>): void;
@@ -24,38 +41,61 @@ interface InPortalProps {
2441
children: React.ReactNode;
2542
}
2643

27-
export const createPortalNode = <C extends Component<any>>(tagName: string = 'div'): PortalNode<C> => {
44+
export const createPortalNode = <C extends Component<any>>(): PortalNode<C> => {
2845
let initialProps = {} as ComponentProps<C>;
2946

3047
let parent: Node | undefined;
3148
let lastPlaceholder: Node | undefined;
3249

33-
const portalNode = Object.assign(document.createElement(tagName), {
50+
const portalNode: PortalNode = {
51+
// @ts-ignore
52+
element: null,
53+
// element: document.createElement('div'),
3454
setPortalProps: (props: ComponentProps<C>) => {
3555
initialProps = props;
3656
},
3757
getInitialPortalProps: () => {
3858
return initialProps;
3959
},
40-
mount: (newParent: Node, newPlaceholder: Node) => {
60+
mount: (newParent: PortalNodeElement, newPlaceholder: PortalNodeElement) => {
4161
if (newPlaceholder === lastPlaceholder) {
4262
// Already mounted - noop.
4363
return;
4464
}
4565
portalNode.unmount();
4666

47-
// If either the PortalNode or its content is an SVG element, we need to treat it differently
48-
if (portalNode instanceof SVGElement || newPlaceholder instanceof SVGElement) {
49-
// replaceChild does not work well for SVG elements: it will rearrange the dom
50-
// properly but does not render as expected.
51-
(newPlaceholder as HTMLElement).outerHTML = portalNode.innerHTML;
52-
} else {
53-
newParent.replaceChild(
54-
portalNode,
55-
newPlaceholder
56-
);
67+
console.log('portalNode.mount()', {
68+
newParent, newPlaceholder,
69+
parent, lastPlaceholder,
70+
portalNode,
71+
});
72+
73+
if (!portalNode.element) {
74+
if (newParent instanceof SVGElement) {
75+
portalNode.element = document.createElementNS(SVG_NAMESPACE, newParent.tagName);
76+
} else {
77+
portalNode.element = document.createElement(newParent.tagName);
78+
}
79+
80+
console.log('CREATED portalNode.element!!!', portalNode.element);
81+
82+
} else if (newParent.tagName !== portalNode.element.tagName) {
83+
const oldElement = portalNode.element;
84+
85+
if (newParent instanceof SVGElement) {
86+
portalNode.element = document.createElementNS(SVG_NAMESPACE, newParent.tagName);
87+
} else {
88+
portalNode.element = document.createElement(newParent.tagName);
89+
}
90+
91+
console.log('REPLACED portalNode.element!!!', oldElement, ' -> ', portalNode.element)
5792
}
5893

94+
newParent.replaceChild(
95+
portalNode.element,
96+
newPlaceholder
97+
);
98+
5999
parent = newParent;
60100
lastPlaceholder = newPlaceholder;
61101
},
@@ -67,21 +107,21 @@ export const createPortalNode = <C extends Component<any>>(tagName: string = 'di
67107
}
68108

69109
if (parent && lastPlaceholder) {
70-
// If either the PortalNode or its content is an SVG element, we need to treat it differently
71-
if (portalNode instanceof SVGElement || lastPlaceholder instanceof SVGElement) {
72-
(portalNode as HTMLElement).innerHTML = '';
73-
} else {
110+
if (portalNode.element) {
74111
parent.replaceChild(
75112
lastPlaceholder,
76-
portalNode
113+
portalNode.element
77114
);
78-
}
79115

80-
parent = undefined;
81-
lastPlaceholder = undefined;
116+
parent = undefined;
117+
lastPlaceholder = undefined;
118+
} else {
119+
// Panic!
120+
throw new Error('No element available, in portalNode.mount!');
121+
}
82122
}
83123
}
84-
});
124+
};
85125

86126
return portalNode;
87127
};
@@ -113,14 +153,19 @@ export class InPortal extends React.PureComponent<InPortalProps, { nodeProps: {}
113153
}
114154

115155
render() {
156+
console.log('InPortal.render()', this);
116157
const { children, node } = this.props;
158+
console.log('InPortal.render()...node = ', node);
159+
console.log('InPortal.render()...node.element = ', node.element);
160+
161+
if (!node.element) return null;
117162

118163
return ReactDOM.createPortal(
119164
React.Children.map(children, (child) => {
120165
if (!React.isValidElement(child)) return child;
121166
return React.cloneElement(child, this.state.nodeProps)
122167
}),
123-
node
168+
node.element
124169
);
125170
}
126171
}
@@ -177,12 +222,11 @@ export class OutPortal<C extends Component<any>> extends React.PureComponent<Out
177222
}
178223

179224
render() {
180-
const { tagName } = this.props.node;
181-
const NodeTagName = tagName.toLowerCase();
225+
console.log('OutPortal.render()', this);
182226

183227
// Render a placeholder to the DOM, so we can get a reference into
184228
// our location in the DOM, and swap it out for the portaled node.
185-
return <NodeTagName ref={this.placeholderNode} />;
229+
return <div ref={this.placeholderNode} />;
186230
}
187231

188232
}

stories/index.stories.js

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import ReactDom from 'react-dom';
23

34
import { storiesOf } from '@storybook/react';
45

@@ -11,7 +12,7 @@ const Container = (props) =>
1112

1213
storiesOf('Portals', module)
1314
.add('render things in different places', () => {
14-
const portalNode = portals.createPortalNode('span');
15+
const portalNode = portals.createPortalNode();
1516

1617
return <div>
1718
<div>
@@ -185,7 +186,7 @@ storiesOf('Portals', module)
185186
});
186187
})
187188
.add('renders reliably, even with frequent changes and multiple portals', () => {
188-
const portalNode = portals.createPortalNode('div');
189+
const portalNode = portals.createPortalNode();
189190

190191
const Target = (p) => p.value.toString();
191192

@@ -227,7 +228,7 @@ storiesOf('Portals', module)
227228
</div>;
228229
})
229230
.add('works with SVGs', () => {
230-
const portalNode = portals.createPortalNode('text');
231+
const portalNode = portals.createPortalNode();
231232

232233
// From https://github.com/httptoolkit/react-reverse-portal/issues/2
233234
return <div>
@@ -236,7 +237,7 @@ storiesOf('Portals', module)
236237
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
237238
<svg x={30} y={10}>
238239
<portals.InPortal node={portalNode}>
239-
<text alignmentBaseline="text-before-edge" fill="red">
240+
<text alignmentBaseline="text-before-edge" dominantBaseline="hanging" fill="red">
240241
test
241242
</text>
242243
</portals.InPortal>
@@ -248,6 +249,68 @@ storiesOf('Portals', module)
248249
</div>
249250

250251
})
252+
.add('can move content around within SVGs', () => {
253+
const portalNode = portals.createPortalNode('text');
254+
255+
return React.createElement(() => {
256+
const [inFirstSvg, setSvgToUse] = React.useState(false);
257+
258+
return <div>
259+
<button onClick={() => setSvgToUse(!inFirstSvg)}>
260+
Click to move the OutPortal within the SVG
261+
</button>
262+
263+
<svg>
264+
<portals.InPortal node={portalNode}>
265+
<text alignmentBaseline="text-before-edge" dominantBaseline="hanging" fill="red">
266+
test
267+
</text>
268+
</portals.InPortal>
269+
270+
<rect x={0} y={0} width={300} height={50} fill="gray"></rect>
271+
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
272+
273+
<svg x={30} y={10}>
274+
{ inFirstSvg && <portals.OutPortal node={portalNode} /> }
275+
</svg>
276+
<svg x={30} y={70}>
277+
{ !inFirstSvg && <portals.OutPortal node={portalNode} /> }
278+
</svg>
279+
</svg>
280+
</div>
281+
});
282+
})
283+
.add('persist DOM while moving within SVGs', () => {
284+
const portalNode = portals.createPortalNode('text');
285+
286+
return React.createElement(() => {
287+
const [inFirstSvg, setSvgToUse] = React.useState(false);
288+
289+
return <div>
290+
<button onClick={() => setSvgToUse(!inFirstSvg)}>
291+
Click to move the OutPortal within the SVG
292+
</button>
293+
294+
<svg>
295+
<portals.InPortal node={portalNode}>
296+
<foreignObject width="300" height="50">
297+
<video src="https://media.giphy.com/media/l0HlKghz8IvrQ8TYY/giphy.mp4" controls loop />
298+
</foreignObject>
299+
</portals.InPortal>
300+
301+
<rect x={0} y={0} width={300} height={50} fill="gray"></rect>
302+
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
303+
304+
<svg x={30} y={10}>
305+
{ inFirstSvg && <portals.OutPortal node={portalNode} /> }
306+
</svg>
307+
<svg x={30} y={70}>
308+
{ !inFirstSvg && <portals.OutPortal node={portalNode} /> }
309+
</svg>
310+
</svg>
311+
</div>
312+
})
313+
})
251314
.add('Example from README', () => {
252315
const MyExpensiveComponent = () => 'expensive!';
253316

0 commit comments

Comments
 (0)