Skip to content

Commit b463e9d

Browse files
committed
Virtual scroll backend component with back pressure
1 parent 224477a commit b463e9d

File tree

2 files changed

+130
-57
lines changed

2 files changed

+130
-57
lines changed

examples/virtual_scroll_y/src/app/main.clj

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
(ns app.main
22
(:gen-class)
33
(:require [hyperlith.core :as h :refer [defaction defview]]
4-
[hyperlith.extras.sqlite :as d]))
4+
[hyperlith.extras.sqlite :as d]
5+
[app.virtual-scroll :as vs]))
56

67
(def row-height 20)
7-
(def view-rows 300)
88

99
(def css
1010
(h/static-css
@@ -28,26 +28,8 @@
2828
:gap :5px
2929
:flex-direction :column}]
3030

31-
[:.view
32-
{:overflow-y :scroll
33-
:scroll-behavior :smooth
34-
:overflow-anchor :none
35-
:height "min(100% - 2rem , 60rem)"}]
36-
37-
[:.table
38-
{:background :white
39-
:pointer-events :none}]
40-
41-
[:table-view
42-
;; Using implicit rows here (not specifying the row template) prevents a
43-
;; layout shift.
44-
{:position :relative
45-
:display :grid}]
46-
4731
[:.row
48-
{;; Set row height explicitly
49-
:height (str row-height "px")
50-
:display :grid
32+
{:display :grid
5133
:grid-template-columns (str "repeat(" 4 ", auto)")}]]))
5234

5335
(defn get-session-data [db sid]
@@ -73,40 +55,33 @@
7355
:data ?new-data}]}
7456
{:sid sid :new-data new-data}))))
7557

76-
(defn row->offset [row-cursor]
77-
(max (- row-cursor 100) 0))
78-
7958
(defaction handler-scroll
80-
[{:keys [sid tabid tx-batch!] {:keys [y]} :body}]
59+
[{:keys [sid tabid tx-batch!] {:strs [y]} :query-params}]
8160
;; We don't actually care about the number of rows only their height
8261
;; this makes the maths simpler
83-
(let [row-cursor (int (/ y row-height))]
62+
(when-let [y (int (parse-long y))]
8463
(tx-batch!
8564
(fn [db]
8665
(update-tab-data! db sid tabid
87-
#(assoc % :row-cursor (max (int row-cursor) 0)))))))
66+
#(assoc % :y (max y 0)))))))
8867

8968
(defn Row [id [a b c :as _data]]
90-
(h/html [:div.row
91-
[:div id] [:div a] [:div b] [:div c]]))
92-
93-
(defn UserView [{:keys [row-cursor] :or {row-cursor 0}} db]
94-
(let [current-offset (row->offset row-cursor)]
95-
(h/html
96-
[:div#table-view.table-view
97-
{:style
98-
{:transform (str "translateY(" (* current-offset row-height) "px)")}}
99-
(->> (d/q db
100-
'{select [id data]
101-
from row
102-
offset ?offset
103-
limit ?limit}
104-
{:offset current-offset
105-
:limit view-rows})
106-
(mapv (fn [[id row]] (Row id row))))])))
107-
108-
(def on-scroll-js
109-
(str "$y = el.scrollTop; @post(`" handler-scroll "`)"))
69+
(h/html [:div.row {:id id}
70+
[:div nil id] [:div nil a] [:div nil b] [:div nil c]]))
71+
72+
(defn row-builder [db offset limit]
73+
(->> (d/q db
74+
'{select [id data]
75+
from row
76+
offset ?offset
77+
limit ?limit}
78+
{:offset offset
79+
:limit limit})
80+
(mapv (fn [[id row]] (Row id row)))))
81+
82+
(defn row-count [db]
83+
(-> (d/q db '{select [[[count *]]] from row})
84+
first))
11085

11186
(def shim-headers
11287
(h/html
@@ -117,19 +92,19 @@
11792
(defview handler-root
11893
{:path "/" :shim-headers shim-headers :br-window-size 19}
11994
[{:keys [db sid tabid] :as _req}]
120-
(let [;; TODO: make this dynamic
121-
row-count 200000
122-
tab-data (get-tab-data db sid tabid)
123-
content (UserView (assoc tab-data :row-count row-count) db)]
95+
(let [tab-data (get-tab-data db sid tabid)]
12496
(h/html
12597
[:link#css {:rel "stylesheet" :type "text/css" :href css}]
12698
[:main#morph.main
127-
[:div#view.view
128-
{:data-ref "_view"
129-
:data-on-scroll__throttle.100ms.trail.noleading on-scroll-js}
130-
[:div#table.table
131-
{:style {:height (str (* row-count row-height) "px")}}
132-
content]]])))
99+
[:pre {:data-json-signals true} nil]
100+
[::vs/Virtual#view
101+
{:v/row-height 20
102+
:v/view-height 1000
103+
:v/max-rendered-rows 1000
104+
:v/row-fn (partial row-builder db)
105+
:v/row-count-fn (partial row-count db)
106+
:v/scroll-handler-path handler-scroll
107+
:v/scroll-pos (:y tab-data)}]])))
133108

134109
(defn initial-table-db-state! [db]
135110
(let [number-of-rows 200000
@@ -203,4 +178,16 @@
203178

204179
(def db (-> @app_ :ctx :db))
205180

181+
(user/bench
182+
(html/html->str
183+
(html/html
184+
[::vs/Virtual#view
185+
{:v/row-height 20
186+
:v/view-height 1000
187+
:v/max-rendered-rows 300
188+
:v/row-fn (partial row-builder db)
189+
:v/row-count-fn (partial row-count db)
190+
:v/scroll-handler-path handler-scroll
191+
:v/scroll-pos (:y 10)}])))
192+
206193
,)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
(ns app.virtual-scroll
2+
(:require [hyperlith.core :as h]))
3+
4+
(defn fetch-next-page-js [fired-signal bottom top scroll-handler-path]
5+
(format
6+
"if (($%s !== -1) && (%s > el.scrollTop || %s < el.scrollTop))
7+
{$%s = -1; @post(`%s?y=${Math.floor(el.scrollTop)}`);}"
8+
fired-signal
9+
bottom
10+
top
11+
fired-signal
12+
scroll-handler-path))
13+
14+
;; TODO: get user's initial view size
15+
;; TODO: user view resize
16+
;; TODO: variable item height
17+
;; TODO: WAT?
18+
19+
(defmethod h/html-resolve-alias ::Virtual
20+
[_
21+
{:keys [id]
22+
:v/keys [row-height max-rendered-rows row-fn row-count-fn
23+
scroll-handler-path scroll-pos view-height]
24+
:as attrs}
25+
_]
26+
(let [visible-rows (min (int (/ view-height row-height))
27+
(or max-rendered-rows 1000))
28+
view-height (* visible-rows row-height)
29+
rendered-rows (* 6 visible-rows)
30+
shift (int (- (/ rendered-rows 2) (/ visible-rows 2)))
31+
scroll-pos (or scroll-pos 0)
32+
offset-rows (max (- (int (/ scroll-pos row-height)) shift) 0)
33+
total-row-count (row-count-fn)
34+
table-height (* total-row-count row-height)
35+
threshold (int (/ rendered-rows 6))
36+
fired-signal (str id "-fired")
37+
remaining-rows (- total-row-count offset-rows)
38+
fetch-next-page? (fetch-next-page-js fired-signal
39+
(if (= offset-rows 0)
40+
0
41+
(* (+ offset-rows threshold) row-height))
42+
(if (> remaining-rows rendered-rows)
43+
(* (- (+ offset-rows rendered-rows)
44+
visible-rows threshold)
45+
row-height)
46+
100000000000)
47+
scroll-handler-path)]
48+
(h/html
49+
[:div (assoc attrs
50+
:data-signals (h/edn->json {fired-signal offset-rows})
51+
:data-on-load fetch-next-page?
52+
:data-on-scroll fetch-next-page?
53+
:style {:scroll-behavior :smooth
54+
:overflow-anchor :none
55+
:overflow-y :scroll
56+
:height (str view-height "px")})
57+
[:div
58+
{:id (str id "-virtual-table")
59+
:style {:pointer-events :none
60+
:height (str table-height "px")}}
61+
[:div
62+
{:id (str id "-virtual-table-view")
63+
:style
64+
{:position :relative
65+
:display :grid
66+
:grid-template-rows
67+
(str "repeat(" (if (> remaining-rows rendered-rows)
68+
rendered-rows
69+
remaining-rows) "," row-height "px)")
70+
:transform
71+
(str "translateY(" (* offset-rows row-height) "px)")}}
72+
(row-fn offset-rows rendered-rows)]]])))
73+
74+
(comment
75+
'[::Virtual#view
76+
{:v/row-height 20
77+
:v/view-height 1000
78+
:v/max-rendered-rows 1000
79+
:v/scroll-pos (:y tab-data)}
80+
81+
82+
:v/row-fn (partial row-builder db)
83+
:v/row-count-fn (partial row-count db)
84+
85+
;; Action updates your y state
86+
:v/scroll-handler-path handler-scrolln])

0 commit comments

Comments
 (0)