Skip to content

Instantly share code, notes, and snippets.

@nberger
Last active May 6, 2024 16:15
Show Gist options
  • Save nberger/b5e316a43ffc3b7d5e084b228bd83899 to your computer and use it in GitHub Desktop.
Save nberger/b5e316a43ffc3b7d5e084b228bd83899 to your computer and use it in GitHub Desktop.
Reagent infinite scroll
(ns views.infinite-scroll
(:require
[reagent.core :as r]))
(defn- get-scroll-top []
(if (exists? (.-pageYOffset js/window))
(.-pageYOffset js/window)
(.-scrollTop (or (.-documentElement js/document)
(.-parentNode (.-body js/document))
(.-body js/document)))))
(defn- get-el-top-position [node]
(if (not node)
0
(+ (.-offsetTop node) (get-el-top-position (.-offsetParent node)))))
(defn- safe-component-mounted? [component]
(try (boolean (r/dom-node component)) (catch js/Object _ false)))
(defn debounce
"Returns a function that will call f only after threshold has passed without new calls
to the function. Calls prep-fn on the args in a sync way, which can be used for things like
calling .persist on the event object to be able to access the event attributes in f"
([threshold f] (debounce threshold f (constantly nil)))
([threshold f prep-fn]
(let [t (atom nil)]
(fn [& args]
(when @t (js/clearTimeout @t))
(apply prep-fn args)
(reset! t (js/setTimeout #(do
(reset! t nil)
(apply f args))
threshold))))))
(defn infinite-scroll [props]
;; props is a map with :can-show-more? & :load-fn keys
(let [listener-fn (atom nil)
detach-scroll-listener (fn []
(when @listener-fn
(.removeEventListener js/window "scroll" @listener-fn)
(.removeEventListener js/window "resize" @listener-fn)
(reset! listener-fn nil)))
should-load-more? (fn [this]
(let [node (r/dom-node this)
scroll-top (get-scroll-top)
my-top (get-el-top-position node)
threshold 50]
(< (- (+ my-top (.-offsetHeight node))
scroll-top
(.-innerHeight js/window))
threshold)))
scroll-listener (fn [this]
(when (safe-component-mounted? this)
(let [{:keys [load-fn can-show-more?]} (r/props this)]
(when (and can-show-more?
(should-load-more? this))
(println "loading more...")
(detach-scroll-listener)
(load-fn)))))
debounced-scroll-listener (debounce 200 scroll-listener)
attach-scroll-listener (fn [this]
(let [{:keys [can-show-more?]} (r/props this)]
(when can-show-more?
(when-not @listener-fn
(reset! listener-fn (partial debounced-scroll-listener this))
(.addEventListener js/window "scroll" @listener-fn)
(.addEventListener js/window "resize" @listener-fn)))))]
(r/create-class
{:component-did-mount
(fn [this]
(attach-scroll-listener this))
:component-did-update
(fn [this _]
(attach-scroll-listener this))
:component-will-unmount
detach-scroll-listener
:reagent-render
(fn [props]
[:div])})))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment