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

Requirements and usage of the SPA API #189

Open
jakearchibald opened this issue Aug 31, 2022 · 6 comments
Open

Requirements and usage of the SPA API #189

jakearchibald opened this issue Aug 31, 2022 · 6 comments

Comments

@jakearchibald
Copy link
Collaborator

jakearchibald commented Aug 31, 2022

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:

  • For the feature to work, it needs to capture the state of the DOM before and after the change.
  • Capturing the 'before' state is async, as it needs to wait for the next render to read back textures.
  • To cater for frameworks that batch DOM updates, updating the DOM may be async.
  • If the DOM change fails, the transition should not happen. An uncaught error during the DOM change indicates a failure.
  • The transition is an enhancement around the DOM change. If the transition cannot happen, that shouldn't prevent the DOM change.
  • 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.
  • 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).
  • Whether the DOM change succeeded is useful to the developer in other contexts, such as the Navigation API, so the transition API shouldn't make it hard to determine this.
  • More advanced transitions require JS, so the developer needs to know when the transition is about to start, and the transition elements are available.
  • The developer may wish to queue their transition behind another, to prevent the earlier transition skipping.
  • The developer may wish to skip their transition if the previous transition fails to complete successfully and fully.

The current API:

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

transition.domUpdated.then();
transition.ready.then();
transition.finished.then();
transition.skipTransition();
  • updateDOM - this is where the developer changes the DOM. The callback allows the feature to know when the DOM change is complete (via the returned promise) and rejections indicate failure. This callback is always called, as the DOM update is more important than the transition.
  • domUpdated - this exposes the result of updateDOM for use in other APIs, such as the navigation API. It may fulfill even if the transition is skipped, or cannot happen due to misconfiguration.
  • ready - this fulfills when the transition is ready to go, so parts of the animation can be driven with JS. It rejects if the transition fails to become ready, due to skipping, misconfiguration, or a failure in the DOM change.
  • finished - this fulfills when the transition animates to completion. It rejects if the transition fails to fully finish, due to skipping, misconfiguration, or a failure in the DOM change.
  • skipTransition() - causes this transition to end abruptly, and rejects ready and finished if they haven't already resolved.
@tbondwilkinson
Copy link

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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

It's mostly intuitive that:

  • updateDOM will always be called, even if the transition is skipped/failed beforehand.
  • Last-transition-wins if a two start, and both updateDOMs will be called.
  • ready should reject if the transition cannot reach the about-to-start state.
  • .skipTransition() refers to skipping the animation, it doesn't mean skipping updateDOM.
  • domUpdated resolves along with updateDOM.
  • Throwing in updateDOM should cause the transition to fail.

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:

  • It should only reject if updateDOM rejects.
  • It should wait behind domUpdated, and never resolve if domUpdated never resolves.
  • Something else should indicate whether the animation played through. This could be:
    • A value that finished resolves to.
    • A property on transition.
    • Two promises, one which rejects if the animation doesn't play through.

@tbondwilkinson
Copy link

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
Copy link
Collaborator

noamr commented Sep 1, 2022

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
Copy link
Collaborator Author

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

  • A transition on a parent will interrupt a transition on a child (last wins)
  • A transition on a child will interrupt a transition on a parent (last wins)
  • A transition on an element will interrupt a transition on the same element (last win)

…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".

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

3 participants