leptos_use/
use_infinite_scroll.rs1use crate::core::{Direction, Directions, IntoElementMaybeSignal};
2use crate::{
3 ScrollOffset, UseEventListenerOptions, UseScrollOptions, UseScrollReturn,
4 use_element_visibility, use_scroll_with_options,
5};
6use default_struct_builder::DefaultBuilder;
7use futures_util::join;
8use gloo_timers::future::sleep;
9use leptos::prelude::*;
10use leptos::reactive::wrappers::read::Signal;
11use send_wrapper::SendWrapper;
12use std::future::Future;
13use std::sync::Arc;
14use std::time::Duration;
15use wasm_bindgen::JsCast;
16
17pub fn use_infinite_scroll<El, M, LFn, LFut>(el: El, on_load_more: LFn) -> Signal<bool>
62where
63 El: IntoElementMaybeSignal<web_sys::Element, M> + 'static,
64 LFn: Fn(ScrollState) -> LFut + Send + Sync + 'static,
65 LFut: Future<Output = ()>,
66{
67 use_infinite_scroll_with_options(el, on_load_more, UseInfiniteScrollOptions::default())
68}
69
70pub fn use_infinite_scroll_with_options<El, M, LFn, LFut>(
72 el: El,
73 on_load_more: LFn,
74 options: UseInfiniteScrollOptions,
75) -> Signal<bool>
76where
77 El: IntoElementMaybeSignal<web_sys::Element, M> + 'static,
78 LFn: Fn(ScrollState) -> LFut + Send + Sync + 'static,
79 LFut: Future<Output = ()>,
80{
81 #[cfg(not(feature = "ssr"))]
82 {
83 let UseInfiniteScrollOptions {
84 distance,
85 direction,
86 interval,
87 on_scroll,
88 event_listener_options,
89 } = options;
90
91 let on_load_more = StoredValue::new(on_load_more);
92
93 let el = el.into_element_maybe_signal();
94
95 let UseScrollReturn {
96 x,
97 y,
98 is_scrolling,
99 arrived_state,
100 directions,
101 measure,
102 ..
103 } = use_scroll_with_options(
104 el,
105 UseScrollOptions::default()
106 .on_scroll(move |evt| on_scroll(evt))
107 .event_listener_options(event_listener_options)
108 .offset(ScrollOffset::default().set_direction(direction, distance)),
109 );
110
111 let state = ScrollState {
112 x,
113 y,
114 is_scrolling,
115 arrived_state,
116 directions,
117 };
118
119 let (is_loading, set_loading) = signal(false);
120
121 let observed_element = Signal::derive_local(move || {
122 let el = el.get();
123
124 el.map(|el| {
125 if el.is_instance_of::<web_sys::Window>()
126 || el.is_instance_of::<web_sys::Document>()
127 {
128 SendWrapper::new(
129 document()
130 .document_element()
131 .expect("document element not found"),
132 )
133 } else {
134 el
135 }
136 })
137 });
138
139 let is_element_visible = use_element_visibility(observed_element);
140
141 let check_and_load = StoredValue::new(None::<Arc<dyn Fn() + Send + Sync>>);
142
143 check_and_load.set_value(Some(Arc::new({
144 let measure = measure.clone();
145
146 move || {
147 let observed_element = observed_element.get_untracked();
148
149 if !is_element_visible.get_untracked() {
150 return;
151 }
152
153 if let Some(observed_element) = observed_element {
154 let scroll_height = observed_element.scroll_height();
155 let client_height = observed_element.client_height();
156 let scroll_width = observed_element.scroll_width();
157 let client_width = observed_element.client_width();
158
159 let is_narrower =
160 if direction == Direction::Bottom || direction == Direction::Top {
161 scroll_height <= client_height
162 } else {
163 scroll_width <= client_width
164 };
165
166 if (state.arrived_state.get_untracked().get_direction(direction) || is_narrower)
167 && !is_loading.get_untracked()
168 {
169 set_loading.set(true);
170
171 let measure = measure.clone();
172 leptos::task::spawn_local(async move {
173 #[cfg(debug_assertions)]
174 let zone =
175 leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
176
177 join!(
178 on_load_more.with_value(|f| f(state)),
179 sleep(Duration::from_millis(interval as u64))
180 );
181
182 #[cfg(debug_assertions)]
183 drop(zone);
184
185 set_loading.try_set(false);
186 sleep(Duration::ZERO).await;
187 measure();
188 if let Some(check_and_load) = check_and_load.try_get_value().flatten() {
189 check_and_load();
190 }
191 });
192 }
193 }
194 }
195 })));
196
197 Effect::watch(
198 move || is_element_visible.get(),
199 move |visible, prev_visible, _| {
200 if *visible && !prev_visible.copied().unwrap_or_default() {
201 measure();
202 }
203 },
204 true,
205 );
206
207 Effect::watch(
208 move || state.arrived_state.get().get_direction(direction),
209 move |arrived, prev_arrived, _| {
210 if let Some(prev_arrived) = prev_arrived
211 && prev_arrived == arrived
212 {
213 return;
214 }
215
216 check_and_load
217 .get_value()
218 .expect("check_and_load is set above")()
219 },
220 true,
221 );
222
223 is_loading.into()
224 }
225
226 #[cfg(feature = "ssr")]
227 {
228 let _ = el;
229 let _ = on_load_more;
230 let _ = options;
231
232 Signal::stored(false)
233 }
234}
235
236#[derive(DefaultBuilder)]
238pub struct UseInfiniteScrollOptions {
239 on_scroll: Arc<dyn Fn(web_sys::Event) + Send + Sync>,
241
242 event_listener_options: UseEventListenerOptions,
244
245 distance: f64,
247
248 direction: Direction,
250
251 interval: f64,
253}
254
255impl Default for UseInfiniteScrollOptions {
256 fn default() -> Self {
257 Self {
258 on_scroll: Arc::new(|_| {}),
259 event_listener_options: Default::default(),
260 distance: 0.0,
261 direction: Direction::Bottom,
262 interval: 100.0,
263 }
264 }
265}
266
267#[derive(Copy, Clone)]
269pub struct ScrollState {
270 pub x: Signal<f64>,
272
273 pub y: Signal<f64>,
275
276 pub is_scrolling: Signal<bool>,
278
279 pub arrived_state: Signal<Directions>,
282
283 pub directions: Signal<Directions>,
285}