Portal Support
https://codesandbox.io/s/solid-transition-group-demo-yo0zv?file=/src/index.js
How to use this in Portal?
<Transition name="dialog"
>
{show() && <Portal><Dialog /></Portal>}
</Transition>
Transition's children is a Portal, not a HTMLElement, so Transition can not add classes to inner dialog element.
Yeah that's tricky. Since the element returned from the portal isn't actually the inserted element, which is actually being inserted elsewhere. The approach here will never work like that. You're better off putting the transition and show logic in the portal.
<Portal>
<Transition name="dialog">
{show() && <Dialog />}
</Transition>
</Portal>
@ryansolid It works. But it’s not very perfect. For example, I have 100 tooltip components on my page, because the Portal always rendered whether it shows up or not. so it will creates 100 empty div elements in Portal mount container.
May I suggest a feature that introduces the ability to pass a ref call-back as a child?
It should solve similar issues with deeply nested elements.
<Transition name="dialog">
{ref => show() && <Portal><Dialog ref={ref} /></Portal>}
</Transition>
This API might introduce new mount/unmount edge cases to consider but might be worth it?
A solution for transitioning portaled elements in and out without having to mount portal elements ahead of time would be great. I've been attempting to implement something similar to https://github.com/solidjs/solid-transition-group/issues/8#issuecomment-1235280601 but I'm rather new to Solid and I'm hitting behavior that I don't quite understand.
Yeah I don't think that would work with a ref. Transition inserts in that part of the tree not where the Portal is.
I wound up with this wrapper around Show which appears to work for my use case:
Code Snippet
import {
createEffect,
createSignal,
Show,
children,
type JSX,
type Setter,
} from 'solid-js';
function nextFrame(fn: () => unknown) {
requestAnimationFrame(() => {
requestAnimationFrame(fn);
});
}
export type Props = {
when: boolean;
children: (ref: Setter<HTMLElement>) => JSX.Element;
classEnterBase: string;
classEnterFrom: string;
classEnterTo: string;
classExitBase: string;
classExitFrom: string;
classExitTo: string;
};
export default function ShowTransition(props: Props) {
const [ref, setRef] = createSignal<HTMLElement | null>(null);
const [renderChildren, setRenderChildren] = createSignal(false);
const resolved = children(() => renderChildren() && props.children(setRef));
function doTransition(
el: HTMLElement,
entering: boolean,
fromClasses: Array<string>,
activeClasses: Array<string>,
toClasses: Array<string>,
) {
function endTransition(e?: Event) {
if (!e || e.target === el) {
el.removeEventListener('transitionend', endTransition);
el.removeEventListener('animationend', endTransition);
el.classList.remove(...activeClasses);
el.classList.remove(...toClasses);
}
setRenderChildren(entering);
if (!entering) {
setRef(null);
}
}
el.addEventListener('transitionend', endTransition);
el.addEventListener('animationend', endTransition);
el.classList.add(...fromClasses);
el.classList.add(...activeClasses);
nextFrame(() => {
el.classList.remove(...fromClasses);
el.classList.add(...toClasses);
});
}
createEffect(() => {
if (props.when && !ref()) {
setRenderChildren(true);
}
});
createEffect(() => {
const el = ref();
const when = props.when;
if (el) {
doTransition(
el,
when,
...(when
? ([
props.classEnterFrom.split(' '),
props.classEnterBase.split(' '),
props.classEnterTo.split(' '),
] as const)
: ([
props.classExitFrom.split(' '),
props.classExitBase.split(' '),
props.classExitTo.split(' '),
] as const)),
);
}
});
return <Show when={renderChildren()}>{resolved()}</Show>;
}
I'm pretty sure that this is solvable in userland just by changing the approach.
<Portal> needs to be on the outside as @ryansolid suggested because we want to transition the element rendered by the Portal, not the Portal itself.
But to solve the issue of Portal injecting a mount point to the document regardless if there is a modal to display or not, we could wrap it in another <Show>. (Although this feels like something that should be considered in Portal design)
const content = children(() => (
<Transition>
<MyDialogComponent />
</Transition>
));
<Show when={content.toArray().length}>
<Portal>
{content()}
</Portal>
</Show>
demo: https://stackblitz.com/edit/transition-with-portal