WICG / view-transitions

https://drafts.csswg.org/css-view-transitions-1/
Other
813 stars 51 forks source link

Requirements and usage of the SPA API #189

Open jakearchibald opened 2 years ago

jakearchibald commented 2 years ago

A couple of folks said the API is confusing in parts. Maybe it can be improved. As a basis for the conversation, here are the current assumptions:

The current API:

const transition = document.createTransition({
  async updateDOM() {…}
});

transition.domUpdated.then(…);
transition.ready.then(…);
transition.finished.then(…);
transition.skipTransition();
tbondwilkinson commented 2 years ago

Maybe I'll share a few thoughts, just from my perspective

  • For the feature to work, it needs to capture the state of the DOM before and after the change.

There's an added wrinkle here that "capturing" is as either single snapshot or can be flattened into a series of non-overlapping elements (is that right?). And that the transition between those two states (before/after) can be represented as a transform between those two images.

  • Capturing the 'before' state is async, as it needs to wait for the next render to read back textures.

And that the page is responsible for making sure it is largely static until that snapshot is taken. Existing animations or other UI changes need to be paused, waited, or finished.

  • To cater for frameworks that batch DOM updates, updating the DOM may be async.

I do think it would be good to be more specific about the expectations for how long it should really take to update the DOM. I think there's also some expectation in the API that longer work (like network requests) that are necessary to perform the transition should probably be done before the transition even begins. Making an API async can sometimes invite people to think that it's an opportunity for them to take as long as they need, but really I think the intention with making it async is that your work is resolved within a few microtasks, perhaps.

  • If the DOM change fails, the transition should not happen. An uncaught error during the DOM change indicates a failure.

This I think is perhaps a little controversial, and I would actually expect that most SPA transitions today do NOT abort on an uncaught error. I would suggest relaxing this a bit, or at least make it up to the user what should and shout not abort the transitions as far as errors.

There will be some cases where an Error indicates a complete failure, but there are others that might be completely benign. I think it's really only possible to determine whether a DOM change fails or not from the page itself, I don't think the browser has enough certainty to determine that.

  • If the transition is misconfigured (for example, two elements have the same page-transition-tag), the transition shouldn't happen. This may be detected before the DOM change, but it shouldn't prevent the DOM change.

I think this gets more complicated with MPA where you might not have full control over the next page, but would still want something reasonable for your transition out. I think there will probably be SOME misconfigurations that do not completely abort the transition and are recoverable.

  • Right now, transitions apply to the whole document, and two transitions cannot happen concurrently. Starting one transition before the other has finished causes the earlier transition to 'skip'. However, the DOM update is not skipped, as skipping a transition doesn't necessarily mean the underlying change should be prevented (think of two updates that increment a counter).

Yeah, but just highlighting that it's up to the page whether a new transition should preempt an existing DOM change or do the DOM change immediately. This gets more complicated if DOM changes are async... again encouraging people to make their DOM changes short.

--

I think the API requires that the page knows basically exactly what the page looks like before and after the transition, and just wants to add some easy animation between those states.

I think when people read this proposal, they may think that it aims to be something more than it is. If I had to describe this API, it's "if your page changes quickly from one DOM structure to another, you can add an easy animation to make that transition". It is not "we aim to provide browser support for the full view transition lifecycle."

I think where confusion creeps in is that the API shape seems to imply that it is more concerned with lifecycle than it actually is - I think people who have nuance about transitions (different styles of abort, etc), DOM changes that are long, or any uncertainty about what the page will look like after the transition, should not use this API.

jakearchibald commented 2 years ago

There's an added wrinkle here that "capturing" is as either single snapshot or can be flattened into a series of non-overlapping elements (is that right?).

This issue isn't really dealing with the CSS side of things, it's more about how the developer interacts with the JS API. The explainer has details on the capturing, and there are further details in the spec.

  • Capturing the 'before' state is async, as it needs to wait for the next render to read back textures.

And that the page is responsible for making sure it is largely static until that snapshot is taken. Existing animations or other UI changes need to be paused, waited, or finished.

Maybe? Sites don't generally delay navigations on animations finishing, so an abrupt pause of the outgoing state might be ok in many situations.

  • To cater for frameworks that batch DOM updates, updating the DOM may be async.

I do think it would be good to be more specific about the expectations for how long it should really take to update the DOM. I think there's also some expectation in the API that longer work (like network requests) that are necessary to perform the transition should probably be done before the transition even begins. Making an API async can sometimes invite people to think that it's an opportunity for them to take as long as they need, but really I think the intention with making it async is that your work is resolved within a few microtasks, perhaps.

Correct. There are two parts to this solution:

  • If the DOM change fails, the transition should not happen. An uncaught error during the DOM change indicates a failure.

This I think is perhaps a little controversial, and I would actually expect that most SPA transitions today do NOT abort on an uncaught error. I would suggest relaxing this a bit,

That is controversial! It would clash with other APIs that follow a similar pattern, such as the Navigation API, and the Web Locks API.

The rough steps are:

  1. Capture outgoing state
  2. Change DOM
  3. Capture incoming state
  4. Start animation

Step 2 is given to the developer, via the updateDOM callback. It seems to me like a fundamental of programming, that an uncaught error is a signal that the operation did not complete fully and successfully in a way the developer intended. That's what throwing errors means across the rest of the platform and programming in general.

I think it would be a real mistake to take this "something went badly wrong" signal and assume "ah well it's probably ok".

or at least make it up to the user what should and shout not abort the transitions as far as errors.

That's already possible:

document.createTransition({
  async updateDOM() {
    try {
      await framework.performDOMUpdate();
    } catch (err) {
      // just swallow all errors
    }
  },
});

That means that transitions would apply to potentially broken content, but hopefully it would be clear to the developer that they were intending that by obscuring the "it went wrong" signal.

There will be some cases where an Error indicates a complete failure, but there are others that might be completely benign. I think it's really only possible to determine whether a DOM change fails or not from the page itself, I don't think the browser has enough certainty to determine that.

Right. That's why the API leaves it to the developer to return that signal.

  • If the transition is misconfigured (for example, two elements have the same page-transition-tag), the transition shouldn't happen. This may be detected before the DOM change, but it shouldn't prevent the DOM change.

I think this gets more complicated with MPA where you might not have full control over the next page, but would still want something reasonable for your transition out. I think there will probably be SOME misconfigurations that do not completely abort the transition and are recoverable.

This issue is focusing on the SPA API. For the MPA API I still think we should fail on misconfiguration in either of the captures, but yes it's harder to confirm compatibility between the two. My current thinking is to allow a clonable object to be passed from the outgoing to incoming page, and I'd recommend developers use some sort of versioning scheme to bail the transition if they have a low confidence that the two pages can transition in a meaningful way.

I think when people read this proposal, they may think that it aims to be something more than it is. If I had to describe this API, it's "if your page changes quickly from one DOM structure to another, you can add an easy animation to make that transition". It is not "we aim to provide browser support for the full view transition lifecycle."

I think that's right. The transition 'wraps' a DOM change. But I think developers will frequently make that DOM change without the transition API (either due to a browser not-supporting the transition API, or they want to avoid it due to user preference). So, the transition API should be about transitions only. It shouldn't provide scheduling features beyond that.

I think where confusion creeps in is that the API shape seems to imply that it is more concerned with lifecycle than it actually is

Yeah, the feature is kinda tangled with the DOM change.

A navigation fails if the DOM change fails. A navigation doesn't fail if the transition fails. However, the transition 'wraps' the DOM change, since it should fail if the DOM change fails. The domUpdated promise is an attempt to 'untangle' it a bit. I'm not sure if there's a better way.

  • I think people who have nuance about transitions (different styles of abort, etc), DOM changes that are long, or any uncertainty about what the page will look like after the transition, should not use this API.

I don't think that's the right conclusion. I think the correct conclusion is: Developers shouldn't build transitions that require more certainty about the content than they have. Eg, they shouldn't create a transition that assumes both states have a header, if they're not certain both states have a header. In that case they should either build a transition that can deal with the header existing in both states, one state, or neither state, or don't try to do anything special with the header. Simpler transitions, such as a cross-fade, don't require much certainty between the two states.

"Don't act with certainty if you're not certain" seems like basic sense to me, but maybe I'm too deep in this.

jakearchibald commented 2 years ago

I chatted the API through with @surma, and here's the feedback:

It's mostly intuitive that:

Stuff that was less clear:


In the case of:

const transition = document.createTransition({
  async updateDOM() {
    await coolFramework.setState(stuff);
  }
});

const transition = document.createTransition({
  async updateDOM() {
    await coolFramework.setState(otherStuff);
  }
});

…it wasn't clear whether the transition should be from the state when createTransition was called, or after the change by the first updateDOM.


Some feeling that ready should not reject before domUpdated fulfills, and never resolve if domUpdated never resolves.


skipTransition() might be better named as skipToEnd(), skipToFinish(), finish().


Most of the disagreement with the current API is around .finished:

tbondwilkinson commented 2 years ago

That is controversial! It would clash with other APIs that follow a similar pattern, such as the Navigation API, and the Web Locks API.

This was a misunderstanding on my part of what you meant by an Error being thrown. You mean specifically an Error being thrown in the updateDOM callback? I thought you meant any page level Error at all during a transition. My expectation is that other JS will continue to run outside the updateDOM callback. But yeah, makes sense.

The developer may wish to skip their transition if the previous transition fails to complete successfully and fully.

Is there a way to access the global list of active transitions or do you have to keep track of them yourself?

…it wasn't clear whether the transition should be from the state when createTransition was called, or after the change by the first updateDOM.

My assumption is that by default transitions queue, first come first serve, and the DOM change is uncancellable via the transition API itself?

Some feeling that ready should not reject before domUpdated fulfills, and never resolve if domUpdated never resolves.

Consider as an alternative if people aren't really paying attention to the rejected value of ready to do anything:

const transition = document.createTransition({
  async updateDOM() {…}
  beforeTransition() {}
});

If the only use case for observing ready is to run additional JS animation, perhaps it feels like that work is more like "part of the transition", which side-steps having to consider when and with what to reject. Another alternative:

const transition = document.createTransition({
  async updateDOM() {…}
});

transition.addEventListener('beforeTransition', () => {});

Something else should indicate whether the animation played through. This could be

What's the use case for knowing if an animation ran or not? I agree that finished is most useful as "the dom is changed and the animation is settled". Finished resolving to a value is alright.

noamr commented 2 years ago

Re-iterating some things we discussed in a private chat: I think the confusing thing for me is the distinction between a transition and its underlying animation. Some of the APIs/promises refer to the former and some to the latter. The transition, with its updateDOM, always takes place. It might or might not be animated due to various reasons. If the naming or so reflects that, I think the whole API would be a lot clearer.

Solving it might be a matter of naming bikeshedding, perhaps:

const transition = document.createTransition({ async updateDOM() { ... });

// Rejects if animation cannot be performed
transition.readyToAnimate : Promise<void>

// Rejects if the animation was skipped or not performed for some reason
transition.animationComplete : Promise<void>

// Always resolves once the animation is completed or skipped
transition.finished : Promise<void>

// Rejected only if `updateDOM` throws an exception, otherwise resolved when DOM updates are applied
transition.domUpdated : Promise<void>

// Fast forwards the animation and completes the transition immediately
transition.finish() : void
jakearchibald commented 2 years ago

@tbondwilkinson

The developer may wish to skip their transition if the previous transition fails to complete successfully and fully.

Is there a way to access the global list of active transitions or do you have to keep track of them yourself?

No, but I think we'll need something like that in future. If we end up allowing transitions to be limited to a particular element, it creates a situation where:

…but otherwise, two transitions can happen in parallel.

Given this, the queuing system will need some thought to get right.

Some feeling that ready should not reject before domUpdated fulfills, and never resolve if domUpdated never resolves.

Consider as an alternative if people aren't really paying attention to the rejected value of ready to do anything:

const transition = document.createTransition({
  async updateDOM() {…}
  beforeTransition() {}
});

If the only use case for observing ready is to run additional JS animation, perhaps it feels like that work is more like "part of the transition", which side-steps having to consider when and with what to reject. Another alternative:

Yeah, I've had similar thoughts (but just called it ready in sketches). Although… it really feels like it should be a promise, especially when you also add a callback for the "failed to become ready" state.

Something else should indicate whether the animation played through.

What's the use case for knowing if an animation ran or not? I agree that finished is most useful as "the dom is changed and the animation is settled". Finished resolving to a value is alright.

The use-cases I can think of are logging (are animations unexpectedly not-finishing?), and "I want my transition to run after the current transition, but if the current transition skips, mine should also skip".