How does Portal work?
Source code:
export function Portal<T extends boolean = false, S extends boolean = false>(props: {
mount?: Node;
useShadow?: T;
isSVG?: S;
ref?:
| (S extends true ? SVGGElement : HTMLDivElement)
| ((
el: (T extends true ? { readonly shadowRoot: ShadowRoot } : {}) &
(S extends true ? SVGGElement : HTMLDivElement)
) => void);
children: JSX.Element;
}) {
const { useShadow } = props,
marker = document.createTextNode(""),
mount = () => props.mount || document.body,
owner = getOwner();
let content: undefined | (() => JSX.Element);
let hydrating = !!sharedConfig.context;
createEffect(
() => {
// basically we backdoor into a sort of renderEffect here
if (hydrating) (getOwner() as any).user = hydrating = false;
content || (content = runWithOwner(owner, () => createMemo(() => props.children)));
const el = mount();
if (el instanceof HTMLHeadElement) {
const [clean, setClean] = createSignal(false);
const cleanup = () => setClean(true);
createRoot(dispose => insert(el, () => (!clean() ? content!() : dispose()), null));
onCleanup(cleanup);
} else {
const container = createElement(props.isSVG ? "g" : "div", props.isSVG),
renderRoot =
useShadow && container.attachShadow
? container.attachShadow({ mode: "open" })
: container;
Object.defineProperty(container, "_$host", {
get() {
return marker.parentNode;
},
configurable: true
});
insert(renderRoot, content);
el.appendChild(container);
props.ref && (props as any).ref(container);
onCleanup(() => el.removeChild(container));
}
},
undefined,
{ render: !hydrating }
);
return marker;
}
export function Portal<T extends boolean = false, S extends boolean = false>(props: {
mount?: Node;
useShadow?: T;
isSVG?: S;
ref?:
| (S extends true ? SVGGElement : HTMLDivElement)
| ((
el: (T extends true ? { readonly shadowRoot: ShadowRoot } : {}) &
(S extends true ? SVGGElement : HTMLDivElement)
) => void);
children: JSX.Element;
}) {
const { useShadow } = props,
marker = document.createTextNode(""),
mount = () => props.mount || document.body,
owner = getOwner();
let content: undefined | (() => JSX.Element);
let hydrating = !!sharedConfig.context;
createEffect(
() => {
// basically we backdoor into a sort of renderEffect here
if (hydrating) (getOwner() as any).user = hydrating = false;
content || (content = runWithOwner(owner, () => createMemo(() => props.children)));
const el = mount();
if (el instanceof HTMLHeadElement) {
const [clean, setClean] = createSignal(false);
const cleanup = () => setClean(true);
createRoot(dispose => insert(el, () => (!clean() ? content!() : dispose()), null));
onCleanup(cleanup);
} else {
const container = createElement(props.isSVG ? "g" : "div", props.isSVG),
renderRoot =
useShadow && container.attachShadow
? container.attachShadow({ mode: "open" })
: container;
Object.defineProperty(container, "_$host", {
get() {
return marker.parentNode;
},
configurable: true
});
insert(renderRoot, content);
el.appendChild(container);
props.ref && (props as any).ref(container);
onCleanup(() => el.removeChild(container));
}
},
undefined,
{ render: !hydrating }
);
return marker;
}
1 Reply
And a version I've tried to simplify to remove SVG and HeadElement handling.
what's the thing about $host
and why does it need to set owner? wouldn't the default owner already be correcd
that makes sense, but surely tere'd have to be an event handler on the container to do the custom bubbling
I assume that the $host thing is not just an obscure web api?
thank you this is very helpful
export function Portal<T extends boolean = false>(props: {
mount?: Node;
useShadow?: T;
ref?:
| HTMLDivElement
| ((
el: (T extends true ? { readonly shadowRoot: ShadowRoot } : {}) & HTMLDivElement
) => void);
children: JSX.Element;
}) {
const { useShadow } = props,
marker = document.createTextNode(""),
mount = () => props.mount || document.body,
owner = getOwner();
let content: undefined | (() => JSX.Element);
let hydrating = !!sharedConfig.context;
createEffect(
() => {
// basically we backdoor into a sort of renderEffect here
if (hydrating) (getOwner() as any).user = hydrating = false;
content || (content = runWithOwner(owner, () => createMemo(() => props.children)));
const el = mount();
const container = document.createElement("div"),
renderRoot =
useShadow && container.attachShadow
? container.attachShadow({ mode: "open" })
: container;
Object.defineProperty(container, "_$host", {
get() {
return marker.parentNode;
},
configurable: true
});
insert(renderRoot, content);
el.appendChild(container);
props.ref && (props as any).ref(container);
onCleanup(() => el.removeChild(container));
},
undefined,
{ render: !hydrating }
);
return marker;
}
export function Portal<T extends boolean = false>(props: {
mount?: Node;
useShadow?: T;
ref?:
| HTMLDivElement
| ((
el: (T extends true ? { readonly shadowRoot: ShadowRoot } : {}) & HTMLDivElement
) => void);
children: JSX.Element;
}) {
const { useShadow } = props,
marker = document.createTextNode(""),
mount = () => props.mount || document.body,
owner = getOwner();
let content: undefined | (() => JSX.Element);
let hydrating = !!sharedConfig.context;
createEffect(
() => {
// basically we backdoor into a sort of renderEffect here
if (hydrating) (getOwner() as any).user = hydrating = false;
content || (content = runWithOwner(owner, () => createMemo(() => props.children)));
const el = mount();
const container = document.createElement("div"),
renderRoot =
useShadow && container.attachShadow
? container.attachShadow({ mode: "open" })
: container;
Object.defineProperty(container, "_$host", {
get() {
return marker.parentNode;
},
configurable: true
});
insert(renderRoot, content);
el.appendChild(container);
props.ref && (props as any).ref(container);
onCleanup(() => el.removeChild(container));
},
undefined,
{ render: !hydrating }
);
return marker;
}