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