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

Communicate Visit direction with html[data-turbo-visit-direction] #1007

Merged
merged 9 commits into from
Dec 4, 2023

Conversation

domchristie
Copy link
Contributor

@domchristie domchristie commented Sep 14, 2023

This pull request adds a direction detail in turbo:visit events data-turbo-visit-direction attributes to the HTML attribute during the lifecycle of a visit. This enables differentiation between back and forward history navigations and is useful for customising animations.

A summary of the changes:

When a history entry is updated (pushState or replaceState):

  • increment the current index if it's a pushState
  • store the current index history's state (as restorationIndex)

When the history is popped (back or forward):

  • determine the direction from and current index and the popstate event state
  • create a Visit with that direction (and dispatch the turbo:visit event with the direction detail apply the data attribute)
  • update the current index to the restored index

direction can be:

  • forward for advance or forward restoration visits
  • back for back restoration visits
  • none for replace visits

@@ -15,6 +16,7 @@ export class History {
if (!this.started) {
addEventListener("popstate", this.onPopState, false)
addEventListener("load", this.onPageLoad, false)
this.currentIndex = history.state?.turbo?.restorationIndex || 0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware of this until today, but browsers persist the state between refreshes (🤯). This means this feature should still work after a reload (e.g. after assets are invalidated and a user taps Back)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, fascinating.

@domchristie
Copy link
Contributor Author

Also /ht @matthewp for the inspiration

shiftyp added a commit to shiftyp/ts-turbo that referenced this pull request Sep 19, 2023
@afcapel
Copy link
Collaborator

afcapel commented Sep 28, 2023

@domchristie nice change 🙌

I'm wondering if we could also add an attribute to the body, similar to data-turbo-preview, with the navigation direction. That way we could customize animations with just css, no js or event handling required.

@domchristie
Copy link
Contributor Author

domchristie commented Sep 28, 2023

I'm wondering if we could also add an attribute to the body

@afcapel good idea. Added in c39c223. It's difficult to test thoroughly because the attributes get added and removed quicker than it's possible to check them—particularly on back/forward (unless we pause rendering :/)

@domchristie
Copy link
Contributor Author

domchristie commented Sep 28, 2023

With this in place, part of me wonders if the event detail is necessary, and whether we just use the data attribute? This might feel more Hotwire-y: where state lives at attributes in the DOM

@domchristie
Copy link
Contributor Author

domchristie commented Sep 28, 2023

With this in place, part of me wonders if the event detail is necessary, and whether we just use the data attribute?

For consistency, I'd also suggest removing the isPreview detail too, as I think this also can be inferred from a data attribute (#926 (comment))

@afcapel
Copy link
Collaborator

afcapel commented Oct 3, 2023

With this in place, part of me wonders if the event detail is necessary, and whether we just use the data attribute? This might feel more Hotwire-y: where state lives at attributes in the DOM

Yes, I think that makes sense. If you can make the change, that'd be great.

I still need to spin the branch for a test, but the changes look 👌

Use html[data-turbo-visit-direction] attribute to communicate visit direction rather than using an event detail
@domchristie domchristie changed the title Add history navigation direction to turbo:visit events Communicate Visit direction with html[data-turbo-visit-direction] Oct 3, 2023
@domchristie
Copy link
Contributor Author

Yes, I think that makes sense. If you can make the change, that'd be great.

👍 @afcapel Done in badfd69

I still need to spin the branch for a test, but the changes look 👌

Demo app with build from this PR and directional View Transitions: https://github.com/domchristie/turbo_view_transition_test

Screen.Recording.2023-10-03.at.18.53.00.mov

@domchristie
Copy link
Contributor Author

(I should add, the test are flaky, but seem to pass, at least after a couple of automated retries)

@domchristie
Copy link
Contributor Author

@afcapel friendly ping, I think this is ready

Copy link
Collaborator

@afcapel afcapel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domchristie looks great, just a few suggestions about the tests.

src/tests/functional/visit_tests.js Outdated Show resolved Hide resolved
waitUntilSelector(page, "[data-turbo-visit-direction='forward']")
.then(() => waitUntilSelector(page, "html:not([data-turbo-visit-direction])")),
page.click("#same-origin-link")
])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can write these tests without relying on Promise.all:

  page.click("#same-origin-link")

  await waitUntilSelector(page, "[data-turbo-visit-direction='forward']")
  await waitUntilNoSelector(page, "[data-turbo-visit-direction]")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I tried this and I don't this it worked because the attribute is added and removed quicker than the assertions can track.

If I recall correctly, in the time it takes for page.click to resolve, the attribute it already added and removed, so waitUntilSelector(page, "[data-turbo-visit-direction='forward']") never passes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also line 226-227 ensures the attribute is added and removed in the correct order

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domchristie what do you think of a660c7b?

The trick is that you don't await for the page.click, so it runs in parallel with the first waitUntilSelector. Then we await for the two assertions to ensure they happen one after the other.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, that works locally but seems very flaky on CI.

Copy link
Collaborator

@afcapel afcapel Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our best option is to use the mutation logs so we are not so dependent on the exact assertion timing. With the mutation log, even if the attribute is no longer present in the page, we can still assert that it was present at some point.

See https://github.com/hotwired/turbo/compare/2bc7c13c66b2a31c4c46c63819d4f7f052538e6f..5d4758969663e4633cf5c61eb9b2bb23cf0a77a9

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our best option is to use the mutation logs

This looks good to me 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, @domchristie if you can push those changes to your branch I'll merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afcapel I will do (I’ve just moved house, and will have a reliable internet connection next week)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you can push those changes to your branch I'll merge

@afcapel Done

src/tests/functional/visit_tests.js Outdated Show resolved Hide resolved
@@ -15,6 +16,7 @@ export class History {
if (!this.started) {
addEventListener("popstate", this.onPopState, false)
addEventListener("load", this.onPageLoad, false)
this.currentIndex = history.state?.turbo?.restorationIndex || 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, fascinating.

@domchristie
Copy link
Contributor Author

@afcapel "test" prefixes and typo fixed, and left a comment about the reason for Promise.all

@brunoprietog
Copy link
Collaborator

brunoprietog commented Nov 17, 2023

With this in place, part of me wonders if the event detail is necessary, and whether we just use the data attribute?

For consistency, I'd also suggest removing the isPreview detail too, as I think this also can be inferred from a data attribute (#926 (comment))

Why remove them? Can't both coexist? Even HEY have a helper to know if it is turboPreview. If it was necessary to make the helper, wouldn't it be better to read the attribute directly from the event as well?

https://app.hey.com/assets/helpers/turbo_helpers-5496333b991567941c18f8dd0485c0d0703e9c67.js

@domchristie
Copy link
Contributor Author

Why remove them? Can't both coexist?

@brunoprietog for me, it's about what feels right for the API. Turbo's event's details are pretty minimal, and as such, isPreview feels out-of-place (particularly since Hotwire encourages state to be stored in the DOM). If event details were to include comprehensive Visit information, then I could see a place for it.

I think there's a good argument for more information in event details, but until that shape is decided upon, it feels preferable to stick with the data attribute approach only. Otherwise, if we decide on a different event API, we'd be introducing a breaking change.

It's less dependent on the timing of the mutation. Even if the attribute
is no longer present, we can still assert that it was present at some
point.
@afcapel afcapel merged commit ac00359 into hotwired:main Dec 4, 2023
1 check passed
@afcapel
Copy link
Collaborator

afcapel commented Dec 4, 2023

Thanks @domchristie!

brunoprietog added a commit to brunoprietog/turbo that referenced this pull request Dec 5, 2023
@brunoprietog
Copy link
Collaborator

With this argument, it should #926 be reverted?

I don't think there is much consistency. Some things are reported via HTML attributes and other things in the event detail. The most recent example is the renderMethod introduced in #1019.

turbo/src/core/session.js

Lines 348 to 350 in ac00359

notifyApplicationAfterRender(isPreview, renderMethod) {
return dispatch("turbo:render", { detail: { isPreview, renderMethod } })
}

Other events that are emitted also provide quite a bit of information in the detail.

As developers, isn't it more ergonomic to receive the information in the detail? Rather than having to make custom helpers to read this information from the HTML.

I feel that exposing the information in the HTML is perfect for the CSS but not ergonomic for the event listeners. What feels wrong with exposing the information both ways?

If something is still undecided, I think it would be good to document the approach and make the API consistent. What euristics will be used to define whether to expose via HTML attributes or via the event detail?

@brunoprietog
Copy link
Collaborator

@domchristie could you document this on turbo-site? If you don't have time I could do it on the weekend, if that's ok with you

@domchristie
Copy link
Contributor Author

@brunoprietog I'm not opposed to including more information in event details, but the implementation of isPreview doesn't feel right to me, and given that there is already a way to retrieve that information from the HTML, I don't think it's currently worth the maintenance overhead. For example, I think the following method signature is a bit smelly:

  allowsImmediateRender(
    { element: newFrame }: Snapshot<FrameElement>,
    _isPreview: boolean,
    options: ViewRenderOptions<FrameElement>
  ) {

Passing through isPreview in the middle of a list of arguments for a method named allowsImmediateRender doesn't feel right to me, particularly as it appears to be unused in this case.

The Turbo event system is reasonably complex, and often requires a chain of method calls to dispatch the event, so to have to include the single isPreview argument, makes me think this will be difficult to change or refactor.

The HTML approach is already implemented, does not require these additional arguments, works with CSS selectors, and it's trivial to write a function to get the same information in JS (which has been the intention since Turbolinks was released).

What euristics will be used to define whether to expose via HTML attributes or via the event detail?

With all that in mind, I'd suggest that if the information might be required in CSS and JavaScript, favour the HTML attribute approach over adding another event detail. In this way, a single API works for both cases without having to maintain the two approaches.

@domchristie could you document this on turbo-site? If you don't have time I could do it on the weekend, if that's ok with you

I'm a bit tight on time at the moment, so if you could do this, that'd be great.

afcapel added a commit to hotwired/turbo-site that referenced this pull request Jan 25, 2024
afcapel added a commit to hotwired/turbo-site that referenced this pull request Jan 25, 2024
afcapel pushed a commit to hotwired/turbo-site that referenced this pull request Feb 7, 2024
* Document view transitions

Introduced in hotwired/turbo#935 and
hotwired/turbo#1007

* Better css example

Co-authored-by: Dom Christie <[email protected]>

* Add data-turbo-visit-direction to attributes reference

* Be more precise

Co-authored-by: Dom Christie <[email protected]>

* Document meta tag

* Clarify it’s optional

Co-authored-by: Kevin McConnell <[email protected]>

---------

Co-authored-by: Dom Christie <[email protected]>
Co-authored-by: Kevin McConnell <[email protected]>
domchristie added a commit to domchristie/turbo that referenced this pull request Jul 20, 2024
…otwired#1007)

* Add direction detail to turbo:visit events

* Test restorationIndex is persisted between reloads

* Add data-turbo-visit-direction to html element

* Versatile Visit direction API.
Use html[data-turbo-visit-direction] attribute to communicate visit direction rather than using an event detail

* Remove "test" prefix from test description

* Fix typos

* Remove "test" prefix from test description

* Clean up assertions

* Read the mutation logs to assert the direction attribute change

It's less dependent on the timing of the mutation. Even if the attribute
is no longer present, we can still assert that it was present at some
point.

---------

Co-authored-by: Alberto Fernández-Capel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants