Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Portal Support #8

Open
weijarz opened this issue Sep 24, 2021 · 7 comments
Open

Portal Support #8

weijarz opened this issue Sep 24, 2021 · 7 comments

Comments

@weijarz
Copy link

weijarz commented Sep 24, 2021

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.

@ryansolid
Copy link
Collaborator

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>

@weijarz
Copy link
Author

weijarz commented Sep 25, 2021

@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.

@neytema
Copy link

neytema commented Sep 2, 2022

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?

@knpwrs
Copy link

knpwrs commented Nov 28, 2022

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 #8 (comment) but I'm rather new to Solid and I'm hitting behavior that I don't quite understand.

@ryansolid
Copy link
Collaborator

Yeah I don't think that would work with a ref. Transition inserts in that part of the tree not where the Portal is.

@knpwrs
Copy link

knpwrs commented Dec 1, 2022

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>;
}

@thetarnav
Copy link
Member

thetarnav commented Mar 18, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants