LiveView live_patch and scroll to top

I’m curious to know, what is the idiomatic way of scrolling to the top after clicking a live_patch link?

The best I could find is this: Window Y scroll position after following live_link is maintained · Issue #481 · phoenixframework/phoenix_live_view · GitHub

1 Like

Not sure if this is idiomatic at all (and if not maybe someone will offer a superior solution), but I have used a JS hook like this:

Hooks.ScrollToTop = {
  mounted() {
    this.el.addEventListener("click", e => {
      window.scrollTo(0, 0);
    })
  }
}

and then include it in your live_patch as such:

<%= live_patch "click me",
      to: Routes.live_path(...),
      class: ...,
      phx_hook: "ScrollToTop" %>

It doesn’t look “perfect” - sometimes you see the page scroll to top a split-second before the patch destination loads. But it’s been “good enough” for my purposes so far.

The last comment on that issue and the linked patch implies to me it should be happening automatically if window.location.href is changing. Is that not the case? Scroll to top on live_link while respecting targets. Closes #517 #481 · phoenixframework/phoenix_live_view@ad96e91 · GitHub

E: Actually the latest code has a few more stipulations:

Note that the scroll only happens for type "redirect" not "patch"

The lastest version of live view requires unique ids for hooks, which makes this solution not ideal.

def link_top(assigns) do
  <.link patch={~p"/pages/#{@page + 1}"} phx-hook="ScrollToTop" id="needs-unique-id">Next</.link>
end

As you can see, a phx-hook needs a unique id. That’s a lot of overhead to keep track of unique ids for every place where I want to use a link_top

I’ve “fixed” this by using #anchors on my links and using the css scroll smooth, so everything looks nice. It’s not completely perfect, but it’s OK!

I’m not sure of the general feeling about this, but for these types of hook where nothing but the hook itself is going to care about the id, I make a component that encapsulates the hook and generates a random id.

def link_top(assigns) do
  ~H"""
  <.link
    patch={~p"/pages/#{@page + 1}"}
    phx-hook="ScrollToTop"
    id={Ecto.UUID.generate()}
  >
    Next
  </.link>
  """
end

You can use System.unique_integer (combined with some string) or whatever instead of a UUID. I dunno, I haven’t been bitten by this before and can’t think of any reason it would be bad, but maybe there is? You can always make it possible to set and id manually if you need it.

It should be noted I haven’t done this recently and never used it in this exact scenario.

Hey @sodapopcan I believe this may cause unintended behavior. Ecto.UUID.generate() is going to generate a new value each time it is called, which will happen each time link_top is re-rendered, but this is is very difficult to determine. Rendering can be elided based on when changes are detected or not.

Basically, the best practice is to avoid non deterministic rendering.

1 Like

I suspected this after posting (sorta rubber ducking my thought process). While aware it could change on re-render, it’s never bitten me before, so I thought I’d throw it out there as I’ve never seen it discussed before. Thanks for the explicit answer!

1 Like

#anchors make sense to me – out of curiosity, what makes it not completely perfect?

Hmm, a more efficient alternative could be adding a single event listener to window that handles the "phoenix.link.click" event triggered by phoenix_html.js as described in the LiveView docs for <.link>:

phoenix_html.js does trigger a custom event phoenix.link.click on the clicked DOM element when a click happened. This allows you to intercept the event on its way bubbling up to window and do your own custom logic…

A data attribute could then be used to distinguish the link elements that should trigger the scroll to top behavior.

<.link patch={~p"/somewhere"} data-scroll="top">Patch to somewhere</.link>

window.addEventListener("phoenix.link.click", function (event) {
  var scroll = event.target.getAttribute("data-scroll")
  if (scroll == "top") { window.scrollTo(0, 0) }
}, false);
5 Likes

Mostly perfect is because of:

  1. you need changes to the actual html page
  2. you might need to fidle around with css things like scroll margin
  3. you need to make sure you add the anchor everywhere you want it

But once you’ve set that up, it works like it should.

Your data attribute looks nice, I’ll try it out later!

1 Like