leptos_use/
use_infinite_scroll.rs

1use 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
16/// Infinite scrolling of the element.
17///
18/// ## Demo
19///
20/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_infinite_scroll)
21///
22/// ## Usage
23///
24/// ```
25/// # use leptos::prelude::*;
26/// use leptos::html::Div;
27/// # use leptos_use::{use_infinite_scroll_with_options, UseInfiniteScrollOptions};
28/// #
29/// # #[component]
30/// # fn Demo() -> impl IntoView {
31/// let el = NodeRef::<Div>::new();
32///
33/// let (data, set_data) = signal(vec![1, 2, 3, 4, 5, 6]);
34///
35/// let _ = use_infinite_scroll_with_options(
36///     el,
37///     move |_| async move {
38///         let len = data.with(|d| d.len());
39///         set_data.update(|data| *data = (1..len+6).collect());
40///     },
41///     UseInfiniteScrollOptions::default().distance(10.0),
42/// );
43///
44/// view! {
45///     <div node_ref=el>
46///         <For each=move || data.get() key=|i| *i let:item>{ item }</For>
47///     </div>
48/// }
49/// # }
50/// ```
51///
52/// The returned signal is `true` while new data is being loaded.
53pub 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
62/// Version of [`use_infinite_scroll`] that takes a `UseInfiniteScrollOptions`. See [`use_infinite_scroll`] for how to use.
63pub 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/// Options for [`use_infinite_scroll_with_options`].
211#[derive(DefaultBuilder)]
212pub struct UseInfiniteScrollOptions {
213    /// Callback when scrolling is happening.
214    on_scroll: Arc<dyn Fn(web_sys::Event) + Send + Sync>,
215
216    /// Options passed to the `addEventListener("scroll", ...)` call
217    event_listener_options: UseEventListenerOptions,
218
219    /// The minimum distance between the bottom of the element and the bottom of the viewport. Default is 0.0.
220    distance: f64,
221
222    /// The direction in which to listen the scroll. Defaults to `Direction::Bottom`.
223    direction: Direction,
224
225    /// The interval time between two load more (to avoid too many invokes). Default is 100.0.
226    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/// The scroll state being passed into the `on_load_more` callback of [`use_infinite_scroll`].
242#[derive(Copy, Clone)]
243pub struct ScrollState {
244    /// X coordinate of scroll position
245    pub x: Signal<f64>,
246
247    /// Y coordinate of scroll position
248    pub y: Signal<f64>,
249
250    /// Is true while the element is being scrolled.
251    pub is_scrolling: Signal<bool>,
252
253    /// Sets the field that represents a direction to true if the
254    /// element is scrolled all the way to that side.
255    pub arrived_state: Signal<Directions>,
256
257    /// The directions in which the element is being scrolled are set to true.
258    pub directions: Signal<Directions>,
259}