My JS hook stopped working after i added phx-viewport-top

Another option is to make a window-level listener for your scroll-down, and then let the phx-viewport-bottom/phx-viewport-top do it’s own thing.

# app.js

  // Scroll to target selector from event
  window.addEventListener("js:scroll-into-view", e => {
    e.target.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
  })

# my_component.js

  def js_jump_to_top(js \\ %JS{}) do
    js
    |> JS.dispatch("js:scroll-into-view", to: "#my-search")
    |> JS.push_focus(to: "#my-combobox-input")
  end

  def js_reset(js \\ %JS{}) do
    js
    |> JS.push("reset-search")
    |> js_jump_to_top()
  end

I use placeholders, either “You’ve reached the end”, or just empty divs, at the beginning or ends of streams or scroll-able sections, and fire a JS.dispatch to them when I need to jump to the bottom/top.

I like this flexibility because the event doesn’t need to care where the item is. It could be a list of items and a graph, and when you click a dot on the graph, it scrolls the list to the corresponding list item, regardless of where it is


EDIT:

Because the above is only half the answer if you’re using <table> and <tbody> tags.

If someone searches this later and has issues with them, note that <tbody> tags have no height, so your phx-viewport-top/phx-viewport-bottom will mess up, or just not work at all.
If you can, switch to <ul> or <ol>, but if not easily possible, you can follow the IntersectionObserver examples from the docs, and write a custom hook. Here’s mine that uses keyset pagination aka cursor pagination:

# js/hooks/intersection_observing_dispatch.js

export const IntersectionObservingDispatch = {
  maybeDispatch(entries) {
    const target = entries[0];
    if (target.isIntersecting && this.el.dataset.intersect) {
      this.liveSocket.execJS(this.el, this.el.dataset.intersect)
    }
  },
  mounted() {
    this.observer = new IntersectionObserver(
      (entries) => this.maybeDispatch(entries),
      {
        root: null, // window by default
        rootMargin: "0px",
        threshold: 1.0,
      }
    );
    this.observer.observe(this.el);
  },
  beforeDestroy() {
    this.observer.unobserve(this.el);
  }
};
# after the `<table>`
# where `metadata` is from using `Paginator` for keyset-pagination

<div :if={@metadata} class="flex flex-col items-center m-6">
    <span :if={@end_of_timeline?} class="font-bold text-md md:text-lg">Looks like you've reached the end 🎉</span>
    <.button :if={!@end_of_timeline?} phx-click="load-more" phx-target={@myself}>
      Load More
    </.button>
    <div
      id="load-more-observed-element"
      phx-hook="IntersectionObservingDispatch"
      data-intersect={!@end_of_timeline? && JS.push("load-more", target: @myself)}
      data-intersect-key={@metadata.after}
    >
    </div>
  </div>

I don’t use the data-intersect-key anymore, but if your “load_more” is particularly slow, you might want to pass it so you don’t waste time on in-flight queries (especially if using async assigns, or start_async)

3 Likes