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

Only update history when Turbo visit is renderable #601

Merged
merged 8 commits into from
Jun 22, 2022
Merged

Only update history when Turbo visit is renderable #601

merged 8 commits into from
Jun 22, 2022

Conversation

manuelpuyol
Copy link
Contributor

@manuelpuyol manuelpuyol commented Jun 14, 2022

Follow up to #571, which ended up just moving around the scroll bug
Should close #387

Context

We've been experiencing some bugs when Turbo visits fail (e.g due to tracked element mismatches or visit-control reload), where Turbo was keeping the previous page scroll position after the page was updated:

Screen.Recording.2022-06-13.at.5.47.27.PM.mov

Problem

From what I see here, Turbo always manually updates the browser state using visit.changeHistory(). This will update the URL before we navigate to the new page. This is fine when Turbo replaces the body, since Turbo will manage everything for us.
The problem lies when a visit is not possible, causing a turbo:reload event. In this case, Turbo was calling window.location.reload() (after the URL was updated), which works like hitting the refresh button in the browser.
Since the reload was called while the previous page was still loaded and scrolled, reload will try to keep the scroll state after the new page loads, causing the bug.

Proposed solution

Turbo should only update the browser history when it knows that it can handle the request. Since we don't actually need the new URL until we replace the body, we can wait for renderPageand renderError to know if the request is renderable or not.
If the request is a success and Turbo can visit, it updates the history like before and continues on.
If the request fails for any reason (404, 500, visit control is reload, element mismatch....), Turbo will delegate to the browser to navigate to the new page, which now is done via window.location.href.

Current state

Now, when we click an element that causes a full reload, the browser will either navigate to the top of the new page or to the location of the targeted element. Notice that when hitting the back button, the previous scroll position is preserved, improving navigation.

Screen.Recording.2022-06-14.at.12.00.30.PM.mov

@manuelpuyol manuelpuyol changed the title Improve scroll position when navigating Only update history when Turbo visit is renderable Jun 14, 2022
@manuelpuyol manuelpuyol marked this pull request as ready for review June 14, 2022 17:02
Copy link
Contributor

@koddsson koddsson left a comment

Choose a reason for hiding this comment

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

I'm not on the Turbo team and have a bias towards getting this merged but this looks good to me 😄

@@ -16,15 +17,18 @@ export class PageView extends View<Element, PageSnapshot, PageViewRenderer, Page
lastRenderedLocation = new URL(location.href)
forceReloaded = false

renderPage(snapshot: PageSnapshot, isPreview = false, willRender = true) {
renderPage(snapshot: PageSnapshot, isPreview = false, willRender = true, visit?: Visit) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably in a follow-up refactor but it's starting to look like this function should take in a "options" object rather than individual arguments.

Comment on lines +16 to +20
<hr class="push-off-screen">

<h2 id="middle">Middle the page</h2>
<hr class="push-off-screen">
<h2>Down the page</h2>
Copy link
Contributor

Choose a reason for hiding this comment

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

Very nit-pick but:

Suggested change
<hr class="push-off-screen">
<h2 id="middle">Middle the page</h2>
<hr class="push-off-screen">
<h2>Down the page</h2>
<hr class="push-off-screen">
<h2 id="middle">Middle the page</h2>
<hr class="push-off-screen">
<h2>Down the page</h2>

@dhh
Copy link
Member

dhh commented Jun 15, 2022

cc @packagethief

@dhh dhh requested a review from packagethief June 15, 2022 12:20
visit.loadCachedSnapshot()
visit.issueRequest()
visit.changeHistory()
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not positive, but moving the history changing to PageView might have implications for the native adapters for iOS and Android. The adapters will still be calling visit.changeHistory themselves, so it will be called twice:

The second should be a no-op because history will have been flagged as changed, but I think we'll still have the same problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how do we avoid calling it twice now?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's only called once by each adapter now. If we move it out of the browser adapter, we may need to move it out of the other adapters as well. Perhaps we don't (it's been a while since I've looked at the native frameworks), but we need to make sure it behaves as expected on iOS and Android.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any tips on how to test that properly? Looking at https://github.com/manuelpuyol/turbo/blob/2eb9ea2c7282f1ec2b41c75705d7e43af7e2076a/src/core/drive/visit.ts#L173-L174 I'd expect it to not do a double update

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right, it will be a no-op because we've already flagged history as having changed. My concern is that we're changing the behavior by moving this out of the adapter, so we just need to ensure it's all good on iOS and Android 👍

Copy link
Member

Choose a reason for hiding this comment

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

@jayohms Can you verify whether this would bring any complication on native?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't foresee any issues on the mobile side, since as mentioned, the second visit.changeHistory call will be a no-op, so the mobile adapters will continue to work as before. It'd be nice to update the mobile adapters to reflect this change, however, we've maintained backwards/forwards compatibility for all iterations of turbo/links API changes.

I don't think we'll be able to reasonably make corresponding changes in the mobile adapters without exposing an additional API, so the adapters can know to conditionally change history (or not). This shouldn't be a problem in the short-term, but it may be a nuance that's difficult for others to understand as time goes on.

@dhh dhh modified the milestones: 7.1.0, 7.2.0 Jun 19, 2022
visit.loadCachedSnapshot()
visit.issueRequest()
visit.changeHistory()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't foresee any issues on the mobile side, since as mentioned, the second visit.changeHistory call will be a no-op, so the mobile adapters will continue to work as before. It'd be nice to update the mobile adapters to reflect this change, however, we've maintained backwards/forwards compatibility for all iterations of turbo/links API changes.

I don't think we'll be able to reasonably make corresponding changes in the mobile adapters without exposing an additional API, so the adapters can know to conditionally change history (or not). This shouldn't be a problem in the short-term, but it may be a nuance that's difficult for others to understand as time goes on.


if (!this.location) return

window.location.href = this.location.toString()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any potential that this causes issues with or prevents reloads for same-page anchors? I'm not clear if that situation is possible here.

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 can't imagine a case where a same-page anchor causes a full page reload

@dhh dhh merged commit 98cdc40 into hotwired:main Jun 22, 2022
dhh added a commit to seanpdoyle/turbo that referenced this pull request Jul 15, 2022
* main:
  Drive Browser tests with `playwright` (hotwired#609)
  Allow Turbo Streams w/ GET via `data-turbo-stream` (hotwired#612)
  Only update history when Turbo visit is renderable (hotwired#601)
  Support development ChromeDriver version overrides (hotwired#606)
  Turbo stream source (hotwired#415)
  Expose Frame load state via `[complete]` attribute (hotwired#487)
dhh added a commit to feliperaul/turbo that referenced this pull request Jul 16, 2022
* main:
  Allow frames to scroll smoothly into view (hotwired#607)
  Export Type declarations for `turbo:` events (hotwired#452)
  Add .php as a valid isHTML extension (hotwired#629)
  Add original click event to 'turbo:click' details (hotwired#611)
  Drive Browser tests with `playwright` (hotwired#609)
  Allow Turbo Streams w/ GET via `data-turbo-stream` (hotwired#612)
  Only update history when Turbo visit is renderable (hotwired#601)
  Support development ChromeDriver version overrides (hotwired#606)
  Turbo stream source (hotwired#415)
  Expose Frame load state via `[complete]` attribute (hotwired#487)
  fix(ie/edge): form.method='delete', raises Invalid argument. (hotwired#586)
  Do not declare global types/constants (hotwired#524)
  Defensively create custom turbo elements (hotwired#483)
  Use `replaceChildren` in StreamActions.update (hotwired#534)
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Oct 13, 2022
Closes hotwired#762

Changes made in [hotwired#601][] had unintended side-effects that
prevented same-page anchor navigations from writing to the history.

This commit adds a call to `Visit.updateHistory()` within
`Visit.goToSamePageAnchor()` along with coverage in
`src/tests/functional/navigation_tests.ts` to guard against future
regressions.

[hotwired#601]: hotwired#601
dhh pushed a commit that referenced this pull request Oct 19, 2022
Closes #762

Changes made in [#601][] had unintended side-effects that
prevented same-page anchor navigations from writing to the history.

This commit adds a call to `Visit.updateHistory()` within
`Visit.goToSamePageAnchor()` along with coverage in
`src/tests/functional/navigation_tests.ts` to guard against future
regressions.

[#601]: #601
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.

Turbo scrolls to Top of current page before rendering new visited page (related to data-turbo-track="reload")
5 participants