Skip to main content

leptos_use/
use_infinite_scroll.rs

1use 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
17/// Infinite scrolling of the element.
18///
19/// ## Demo
20///
21/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_infinite_scroll)
22///
23/// ## Usage
24///
25/// ```
26/// # use leptos::prelude::*;
27/// use leptos::html::Div;
28/// # use leptos_use::{use_infinite_scroll_with_options, UseInfiniteScrollOptions};
29/// #
30/// # #[component]
31/// # fn Demo() -> impl IntoView {
32/// let el = NodeRef::<Div>::new();
33///
34/// let (data, set_data) = signal(vec![1, 2, 3, 4, 5, 6]);
35///
36/// let _ = use_infinite_scroll_with_options(
37///     el,
38///     move |_| async move {
39///         let len = data.with(|d| d.len());
40///         set_data.update(|data| *data = (1..len+6).collect());
41///     },
42///     UseInfiniteScrollOptions::default().distance(10.0),
43/// );
44///
45/// view! {
46///     <div node_ref=el>
47///         <For each=move || data.get() key=|i| *i let:item>{ item }</For>
48///     </div>
49/// }
50/// # }
51/// ```
52///
53/// The returned signal is `true` while new data is being loaded.
54pub fn use_infinite_scroll<El, M, LFn, LFut>(el: El, on_load_more: LFn) -> Signal<bool>
55where
56    El: IntoElementMaybeSignal<web_sys::Element, M> + 'static,
57    LFn: Fn(ScrollState) -> LFut + Send + Sync + 'static,
58    LFut: Future<Output = ()>,
59{
60    use_infinite_scroll_with_options(el, on_load_more, UseInfiniteScrollOptions::default())
61}
62
63/// Version of [`use_infinite_scroll`] that takes a `UseInfiniteScrollOptions`. See [`use_infinite_scroll`] for how to use.
64pub fn use_infinite_scroll_with_options<El, M, LFn, LFut>(
65    el: El,
66    on_load_more: LFn,
67    options: UseInfiniteScrollOptions,
68) -> Signal<bool>
69where
70    El: IntoElementMaybeSignal<web_sys::Element, M> + 'static,
71    LFn: Fn(ScrollState) -> LFut + Send + Sync + 'static,
72    LFut: Future<Output = ()>,
73{
74    let UseInfiniteScrollOptions {
75        distance,
76        direction,
77        interval,
78        on_scroll,
79        event_listener_options,
80    } = options;
81
82    let on_load_more = StoredValue::new(on_load_more);
83
84    let el = el.into_element_maybe_signal();
85
86    let UseScrollReturn {
87        x,
88        y,
89        is_scrolling,
90        arrived_state,
91        directions,
92        measure,
93        ..
94    } = use_scroll_with_options(
95        el,
96        UseScrollOptions::default()
97            .on_scroll(move |evt| on_scroll(evt))
98            .event_listener_options(event_listener_options)
99            .offset(ScrollOffset::default().set_direction(direction, distance)),
100    );
101
102    let state = ScrollState {
103        x,
104        y,
105        is_scrolling,
106        arrived_state,
107        directions,
108    };
109
110    let (is_loading, set_loading) = signal(false);
111
112    let observed_element = Signal::derive_local(move || {
113        let el = el.get();
114
115        el.map(|el| {
116            if el.is_instance_of::<web_sys::Window>() || el.is_instance_of::<web_sys::Document>() {
117                SendWrapper::new(
118                    document()
119                        .document_element()
120                        .expect("document element not found"),
121                )
122            } else {
123                el
124            }
125        })
126    });
127
128    let is_element_visible = use_element_visibility(observed_element);
129
130    let check_and_load = StoredValue::new(None::<Arc<dyn Fn() + Send + Sync>>);
131
132    check_and_load.set_value(Some(Arc::new({
133        let measure = measure.clone();
134
135        move || {
136            let observed_element = observed_element.get_untracked();
137
138            if !is_element_visible.get_untracked() {
139                return;
140            }
141
142            if let Some(observed_element) = observed_element {
143                let scroll_height = observed_element.scroll_height();
144                let client_height = observed_element.client_height();
145                let scroll_width = observed_element.scroll_width();
146                let client_width = observed_element.client_width();
147
148                let is_narrower = if direction == Direction::Bottom || direction == Direction::Top {
149                    scroll_height <= client_height
150                } else {
151                    scroll_width <= client_width
152                };
153
154                if (state.arrived_state.get_untracked().get_direction(direction) || is_narrower)
155                    && !is_loading.get_untracked()
156                {
157                    set_loading.set(true);
158
159                    let measure = measure.clone();
160                    leptos::task::spawn_local(async move {
161                        #[cfg(debug_assertions)]
162                        let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
163
164                        join!(
165                            on_load_more.with_value(|f| f(state)),
166                            sleep(Duration::from_millis(interval as u64))
167                        );
168
169                        #[cfg(debug_assertions)]
170                        drop(zone);
171
172                        set_loading.try_set(false);
173                        sleep(Duration::ZERO).await;
174                        measure();
175                        if let Some(check_and_load) = check_and_load.try_get_value().flatten() {
176                            check_and_load();
177                        }
178                    });
179                }
180            }
181        }
182    })));
183
184    Effect::watch(
185        move || is_element_visible.get(),
186        move |visible, prev_visible, _| {
187            if *visible && !prev_visible.copied().unwrap_or_default() {
188                measure();
189            }
190        },
191        true,
192    );
193
194    Effect::watch(
195        move || state.arrived_state.get().get_direction(direction),
196        move |arrived, prev_arrived, _| {
197            if let Some(prev_arrived) = prev_arrived
198                && prev_arrived == arrived
199            {
200                return;
201            }
202
203            check_and_load
204                .get_value()
205                .expect("check_and_load is set above")()
206        },
207        true,
208    );
209
210    is_loading.into()
211}
212
213/// Options for [`use_infinite_scroll_with_options`].
214#[derive(DefaultBuilder)]
215pub struct UseInfiniteScrollOptions {
216    /// Callback when scrolling is happening.
217    on_scroll: Arc<dyn Fn(web_sys::Event) + Send + Sync>,
218
219    /// Options passed to the `addEventListener("scroll", ...)` call
220    event_listener_options: UseEventListenerOptions,
221
222    /// The minimum distance between the bottom of the element and the bottom of the viewport. Default is 0.0.
223    distance: f64,
224
225    /// The direction in which to listen the scroll. Defaults to `Direction::Bottom`.
226    direction: Direction,
227
228    /// The interval time between two load more (to avoid too many invokes). Default is 100.0.
229    interval: f64,
230}
231
232impl Default for UseInfiniteScrollOptions {
233    fn default() -> Self {
234        Self {
235            on_scroll: Arc::new(|_| {}),
236            event_listener_options: Default::default(),
237            distance: 0.0,
238            direction: Direction::Bottom,
239            interval: 100.0,
240        }
241    }
242}
243
244/// The scroll state being passed into the `on_load_more` callback of [`use_infinite_scroll`].
245#[derive(Copy, Clone)]
246pub struct ScrollState {
247    /// X coordinate of scroll position
248    pub x: Signal<f64>,
249
250    /// Y coordinate of scroll position
251    pub y: Signal<f64>,
252
253    /// Is true while the element is being scrolled.
254    pub is_scrolling: Signal<bool>,
255
256    /// Sets the field that represents a direction to true if the
257    /// element is scrolled all the way to that side.
258    pub arrived_state: Signal<Directions>,
259
260    /// The directions in which the element is being scrolled are set to true.
261    pub directions: Signal<Directions>,
262}