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

Multiple independent transitions in SPAs (scoped transitions) #52

Open
jakearchibald opened this issue Aug 20, 2021 · 6 comments
Open
Labels
needs-attention untriaged Feature requests which are pending decision on whether they can be supported.

Comments

@jakearchibald
Copy link
Collaborator

jakearchibald commented Aug 20, 2021

Let's say a page contains two independent components:

<component-a>...</component-a>
<component-b>...</component-b>

Each makes a state change, and wants to perform a transition, so they each prepare and start a transition.

Should this work if both transitions are prepared and started at the same time? It feels like it should. What if the second transition starts half way through the first?

This feels like a likely situation in SPAs.

@mattgperry
Copy link

The imperative prepare/execute API is going to make interruption and synchronisation difficult. Is there a discussion why this isn’t all handled with declarative CSS?

In Framer Motion we have a layout prop. When the layout changes, it animates with the defined transition. Components can optionally be given a layoutId - when a new component with a matching layout ID is added, we crossfade to it. This would all map quite neatly to corresponding hypothetical CSS.

With a completely declarative approach, its easier for us as the implementor to know all the layout transitions happening at any given time, how they affect each other, and if a transition is being interrupted and how best to handle that. Although in terms of implementation interruptions are handled the same as any other layout transition which is what’s leading me to wonder if this ticket is an intrinsic result of an imperative approach.

@jakearchibald
Copy link
Collaborator Author

Is there a discussion why this isn’t all handled with declarative CSS?

Some discussion #2 (comment). I tried to come up with a CSS-only proposal, and ended up with 20+ new properties and still wasn't hitting all the use-cases.

One key difficultly is declaring something about elements that are no longer there (exists in previous page but not in next page). Fundamentally, CSS can only select things that are there.

With a completely declarative approach

I don't think 'declarative' and 'imperative' are useful distinguishing terms here. Is the web animation API declarative or imperative? Just because it's JavaScript doesn't make it imperative. You're still 'declaring' the start and end of the animation and the engine handles the frame by frame stuff.

@mattgperry
Copy link

Just because it's JavaScript doesn't make it imperative

Framer Motion is a declarative JS library so I understand this, I’m just trying to point out that the proposal as it stands is leaving the procedural this-then-that stuff to the user and it’s natural that questions like this will arise as a result of that.

It’s precisely the start and end stuff that’s taken care of, in a consolidated manner that handles all the layout animations together.

The linked proposal looks great. Surely if elements aren’t matched in the subsequent state the resultant animation simply disregards them/keeps them static during the crossfade? That’s what we do in Motion.

@jakearchibald
Copy link
Collaborator Author

The linked proposal looks great. Surely if elements aren’t matched in the subsequent state the resultant animation simply disregards them/keeps them static during the crossfade? That’s what we do in Motion.

You lose some typical use-cases there, like controlling the outgoing animation for the thing going away.

@khushalsagar khushalsagar added the untriaged Feature requests which are pending decision on whether they can be supported. label Sep 8, 2021
@johannesodland
Copy link

This issue highlights one of the things I am left wanting in this proposal.

Sometimes all we need is a transition from one component to an updated version of the same component.

One example is an election result page. As the votes come in the components are updated. The component showing the leader of the popular vote might need to be updated with a new candidate or party. The DOM of the previous visualisation will be replaced with a new DOM for the next visualisation. Right now we need to keep two separate component DOMs alive to transition between them.

Pages like this contain many components that need to update and transition independently. It would be nice if we had a simple way of doing this, echoing the suggested proposal.

Say that in the future we had a function similar to document.createDocumentTransition that supported transitioning an individual component. Lets call it element.createElementTransition.

Components could then be transitioned, similar to pages:

const transition = elm.createElementTransition();
await transition.start(async () => await updateElementDOM(elm))

I know this is not straightforward, and that there are a lot of issues that would need to be sorted out. One of them is that the transition could not be placed in a top layer, or through an uber-root stacking context. It would somehow need to be attached where the element is normally rendered.

I hope that this use case can be addressed in the proposal somehow. We should at least try to make sure that such a use case is not excluded in the future.

@jakearchibald jakearchibald changed the title Multiple independent transitions in SPAs Multiple independent transitions in SPAs (scoped transitions) Sep 16, 2022
@jakearchibald
Copy link
Collaborator Author

jakearchibald commented Sep 16, 2022

A rough sketch for this:

element.createTransition({
  updateDOM() {
    /* … */
  },
});

The above is the same API shape as the whole-document API, except it's called on a particular element.

Capturing and creating pseudo-trees

The element becomes the 'scoped transition root' for the transition. It's the element that will host the pseudo-element tree.

<the element>
└─ ::page-transition

(we will probably need to change the naming, as 'page-transition' doesn't work, but to make it easier to map back to the document transitions, I'll use the current names in this description)

page-transition-container trees will be created for descendants that have a page-transition-tag.

<the element>
└─ ::page-transition
   └─ ::page-transition-container(some-button)
      └─ ::page-transition-image-wrapper(some-button)
         ├─ ::page-transition-outgoing-image(some-button)
         └─ ::page-transition-incoming-image(some-button)

This means, if no element is given a page-transition-tag by the developer, there'll be nothing to transition.

In terms of selectors, these pseudo-elements are addressed from the 'scoped transition root' element (just as they hang off html for document transitions).

The before & after positions of transitioning elements will be calculated relative to the top-left of the 'scoped transition root'.

During the transition, the elements being transitioned (that have a page-transition-tag) will not be rendered. Their visual appearance is left to the pseudo elements.

If the 'scoped transition root' is not renderable during the capture phases, the transition fails.

Why not capture the element itself?

Take this example:

el.style.pageTransitionTag = 'container';
el.style.backgroundColor = 'red';

el.createTransition({
  updateDOM() {
    el.style.backgroundColor = 'green';
  }
});

In this case, el will not transition from 'red' to 'green', it will just swap. This is because only descendants of el can become transitioning parts.

This is because it doesn't really make sense if el has overflow: hidden and a box shadow. It would clip its own capture. Unless we do something weird like say, for the 'scoped transition root', only the inner content is captured and hidden.

Due to this, createTransition seems like a bad name. If we go with this design, we should bikeshed the name.

Concurrent transitions

el.createTransition will cause existing transitions rooted to that element to be skipped.

Otherwise, transitions can happen concurrently, although it may look weird in some cases. We should offer an API to get active transitions occurring on the element, its descendants, its ancestors, and the document.

During updateDOM

element.createTransition({
  await updateDOM() {
    await wait500ms();
    doChanges();
  },
});

During updateDOM, transitioning elements are 'frozen' in terms of rendering. It seems nice to explain this via their pseudo-elements being added early, and their original element being hidden, although that lands us with the issue of animations applying too early. Maybe there's a way to have the best of both without magic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-attention untriaged Feature requests which are pending decision on whether they can be supported.
Projects
None yet
Development

No branches or pull requests

5 participants
@jakearchibald @johannesodland @mattgperry @khushalsagar and others