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.
54///
55/// ## Server-Side Rendering
56///
57/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
58///
59/// On the server-side, `use_infinite_scroll` will return a signal that is always `false`.
60/// It effectively does nothing on the server, as there are no scroll events to listen to.
61pub 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
70/// Version of [`use_infinite_scroll`] that takes a `UseInfiniteScrollOptions`. See [`use_infinite_scroll`] for how to use.
71pub 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/// Options for [`use_infinite_scroll_with_options`].
237#[derive(DefaultBuilder)]
238pub struct UseInfiniteScrollOptions {
239    /// Callback when scrolling is happening.
240    on_scroll: Arc<dyn Fn(web_sys::Event) + Send + Sync>,
241
242    /// Options passed to the `addEventListener("scroll", ...)` call
243    event_listener_options: UseEventListenerOptions,
244
245    /// The minimum distance between the bottom of the element and the bottom of the viewport. Default is 0.0.
246    distance: f64,
247
248    /// The direction in which to listen the scroll. Defaults to `Direction::Bottom`.
249    direction: Direction,
250
251    /// The interval time between two load more (to avoid too many invokes). Default is 100.0.
252    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/// The scroll state being passed into the `on_load_more` callback of [`use_infinite_scroll`].
268#[derive(Copy, Clone)]
269pub struct ScrollState {
270    /// X coordinate of scroll position
271    pub x: Signal<f64>,
272
273    /// Y coordinate of scroll position
274    pub y: Signal<f64>,
275
276    /// Is true while the element is being scrolled.
277    pub is_scrolling: Signal<bool>,
278
279    /// Sets the field that represents a direction to true if the
280    /// element is scrolled all the way to that side.
281    pub arrived_state: Signal<Directions>,
282
283    /// The directions in which the element is being scrolled are set to true.
284    pub directions: Signal<Directions>,
285}