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
)