Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

[✨] View Transition Hook #55

Closed
GrandSchtroumpf opened this issue Aug 26, 2023 · 9 comments
Closed

[✨] View Transition Hook #55

GrandSchtroumpf opened this issue Aug 26, 2023 · 9 comments
Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation

Comments

@GrandSchtroumpf
Copy link

Is your feature request related to a problem?

I'm trying a animate a list of item on page navigation. For that I'm animating with Javascript, but there is no way to hook into the transition from the link.

const animate = $(() => {
  const items = document.querySelectorAll('.item');
  for (const item of items) {
    const transitionName = item.style.getProperty('view-transition-name');
    // Animation is not working because I need to be inside the transition
    document.documentElement.animate({
      transform: [....],
    }, {
      pseudoElement: `::view-transition-old(${transitionName})`,
      duration: 200
    })
  }
})
return <Link onClick$={animate} href="...">Link</Link>

Describe the solution you'd like

Ideally the Link element & useNavigation would expose a callback for that :

const animate = $((transition, element) => {
  ....
})
return <Link onViewTransition$={animate} href="...">Link</Link>

Describe alternatives you've considered

We cannot set the animation inside style because this is a pseudo element of html.
As we cannot update the pseudo element with Javascript and we cannot use css variable like that ::view-transition-old(var(--name)).
We cannot set a <style> on each element because it'll be removed before the view transition is ready.

The only solution is to add <style> inside head with all the ::view-transition-old() pseudo class and remove them:

const viewTransitionName = "...";
useVisibleTask$(() => {
  const style = document.createElement('stlye');
  style.textContent = `::view-transition-old(${viewTransitionName}) { animation: ....; }`
  const child = document.head.appenChild(style);
  return () => setTimeout(() => document.head.removeChild(child), 1000); // wait for transition to be done
});
return <Link href="">...</Link>

Note: I'm not using ::view-transition-old(*) because I want to target specific elements (not all) and I want to add delay.

Additional context

No response

@mhevery
Copy link

mhevery commented Aug 28, 2023

Related QwikDev/qwik#3664 and QwikDev/qwik#2078

@wmertens
Copy link
Member

wmertens commented Mar 6, 2024

I'm going to point the other two issues here since they all want more or less the same thing.

The View Transitions API isn't supported by WebKit yet but according to the tracking issue they are ok with it.

Manu made https://pics-qwik.pages.dev/ last year using the ::view-transition CSS API.
It uses this CSS which assigns the "picture" transition to the images.

So, do we just recommend to use this API or do we implement async callbacks in navigation to allow doing things manually?

@GrandSchtroumpf
Copy link
Author

I think the discussion should go beyond navigation.
The ViewTransition API can be used for any kind of state changes that involves a mutation of the DOM, not only navigation.
Since rendering is async this is very difficult to run the view transition callback at the correct time, especially when animating with WAPI.
An idea would be to have something like track that would add run view transition when the state changes :

useViewTransition(async ({ track, transition }) => {
  if (!transition) return track(state);
  await transition.ready;
  document.documentElement.animate(...);
})

@wmertens
Copy link
Member

wmertens commented Mar 6, 2024

@GrandSchtroumpf so for animations you need to know which old DOM elements will be transformed to which new elements right? And with the ViewTransition API that is done via CSS.

Suppose we decide to only support the ViewTransition API, what is still missing in Qwik?

One thing we could do is put QRL attribures on DOM elements that are awaited before/after performing some DOM manipulation. Using sync$ they would even be instantly available. However, I'm not sure what we would need.

@GrandSchtroumpf
Copy link
Author

GrandSchtroumpf commented Mar 6, 2024

Since this issue gathers all View Transition problems, let's split it into two part

When does rendering happens

The ViewTransition API expect a callback that would work like that :

  1. startViewTransition
  2. Take a screenshot and create a ::view-transition pseudo element on the documentElement
  3. Run callback that returns a promise
  4. When promise resolve: resolve the transition.ready promise & start transition in the next frame

This is an imperative API, while Qwik is reactive, so we don't know when DOM is updated

Here is a an example that illustrate the problem

const hidden = useSignal(true);
useVisibleTask$(({ track }) => {
  track(() => hidden.value);
  // Case 1: flaky since the DOM might not have been updated at this point
  document.startViewTransition(() => waitForNextFrame());
   // Case 2: slow because it waits for 200ms to start the transition
  document.startViewTransition(() => waitFor200ms());
})

In React they suggest to use flushSync inside the callback to force rerendering. It's a ok-ish workaround since the main thread might be already busy, which might create junky transitions.

Solution 1
One solution Qwik could integrate is a onNextRender function that would resolve when DOM has been rerendered.

const hidden = useSignal(true);
useVisibleTask$(({ track }) => {
  track(() => hidden.value);
  document.startViewTransition(() => onNextRender());
})

Note: Here we might not want to startViewTransition when state initializes.

Solution 2
Create a hook into qwik-city that would do that :

useViewTransition(({ track }) => track(() => hidden.value));

The hook would:

  1. runs once to know on what state changes to run
  2. start view transition when the state changes
  3. wait for next rendering to resolve the startViewTransition callback

Note: This solution might be better designed to allow dev to select in which case run or not the transition.

Hook into the transition flow

The API has two faces: CSS & JS.

  • CSS is declarative
  • JS is imperative

CSS :

  • Advantages: we don't have to know when the view transition happens, since it's already declared.
  • Problem: Since all elements flagged with view-transition-name will run for all view transition we tends to add/remove them dynamically.

JS:

  • Advantages: we have more control on the selection
  • Problem: we need to wait for the ViewTransition object to be ready and we don't know when this happen (as mentioned above)

Example in my initial comment

Solution 1:
Provide a way to hook into a transition :

const location = useLocation();
useViewtransition(({ track, before, ready, finished }) => {
  track(() => location.url);
  before(async () => {
    // Do something before `startViewTransition` is called
  });
  ready(async () => {
    // Hooked into the `transition.ready` promise callback
    // run WAPI animation
  });
  finished(async () => {
    // Hooked into the `transition.finished` promise callback
    // cleanup style or classes needed for the transition
  })
});
useViewTransition(({ track }) => track(() => hidden.value));

The benefit of this API is that we can listen on page transition or local state transition with different behaviors.
The issue is that it's not easy to have different behavior depending on the Link

Solution 2:
Maybe we can plug useViewTransition with <Link/> :

const transition = useViewtransition(({ before, ready, finished }) => {
  before(async () => {
    // Do something before `startViewTransition` is called
  });
  ready(async () => {
    // Hooked into the `transition.ready` promise callback
    // run WAPI animation
  });
  finished(async () => {
    // Hooked into the `transition.finished` promise callback
    // cleanup style or classes needed for the transition
  })
});
return <Link transition={transition} />

This could be used in any UI library which wants to leverage the View Transition API power inside Qwik :

const TabGroup = component$(({ transition }) => {
  const selected = useState(null);
  const select = $((e, el) => {
    console.log(transition.id);
    transition.start(() => selected.value = el.ariaControls)
  });
  return (
    <button role="tab" onClick$={select} aria-controls="tab-panel-1">...</button>
    <div role="tabpanel" id="tab-panel-1">...</div
  )
})

Conclusion

We can have something like that, and use it inside the <Link /> component

const useViewtransition = (params) => {
  const initialized = useSignal(false);
  const id = useId();
  const start = $(async (cb = (() => {})) => {
    await params.before();
    const viewTransition = await new Promise((res, rej) => {
      const transition = document.startViewTransition(async () => {
        await cb();
        await nextRendering();
        res(transition);
      })
    });
    viewTransition.read.then(params.ready);
    viewTransition.finished.then(params.finished)
  });

  useTask$(({ track }) => {
    if (!params.track) return;
    track(params.track);
    if (!initialized.value) return initialized.value = true;
    if (!isServer) start();
  })
  return { id, start }
}

@wmertens
Copy link
Member

wmertens commented Mar 6, 2024

Note that we could have a QRL prop on DOM nodes that gets awaited before it gets changed by Qwik. Would that be enough?

@GrandSchtroumpf
Copy link
Author

I'm not sure, like a onViewTransitionstart$, onViewTransitionReady$, and onViewTransitionFinished$ ?

The startViewTransition is based on an action that will change the dom (page navigation, tab, popover, form,...). In a reactive framework like qwik the DOM usually changes with state update. So for me, it makes more sense to me to build around the state changes process and not a DOM element.

@wmertens
Copy link
Member

wmertens commented Mar 6, 2024

No I meant like onBeforeDomNodeWillChange

@gioboa
Copy link
Member

gioboa commented Oct 14, 2024

We moved this issue to qwik-evolution repo to create a RFC discussion for this.
Here is our Qwik RFC process thanks.

@github-project-automation github-project-automation bot moved this to In Progress (STAGE 2) in Qwik Evolution Oct 14, 2024
@gioboa gioboa transferred this issue from QwikDev/qwik Oct 14, 2024
@github-actions github-actions bot added [STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation labels Oct 14, 2024
@QwikDev QwikDev locked and limited conversation to collaborators Oct 14, 2024
@gioboa gioboa converted this issue into discussion #135 Oct 14, 2024
@github-project-automation github-project-automation bot moved this from In Progress (STAGE 2) to Released as Stable (STAGE 5) in Qwik Evolution Oct 14, 2024
@shairez shairez removed this from Qwik Evolution Oct 15, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation
Projects
None yet
Development

No branches or pull requests

4 participants