use crate::core::{Direction, Directions, ElementMaybeSignal}; use crate::{ use_element_visibility, use_scroll_with_options, ScrollOffset, UseEventListenerOptions, UseScrollOptions, UseScrollReturn, }; use default_struct_builder::DefaultBuilder; use futures_util::join; use gloo_timers::future::sleep; use leptos::*; use std::future::Future; use std::rc::Rc; use std::time::Duration; use wasm_bindgen::JsCast; /// Infinite scrolling of the element. /// /// ## Demo /// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_infinite_scroll) /// /// ## Usage /// /// ``` /// # use leptos::*; /// use leptos::html::Div; /// # use leptos_use::{use_infinite_scroll_with_options, UseInfiniteScrollOptions}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { /// let el = create_node_ref::
(); /// /// let (data, set_data) = create_signal(vec![1, 2, 3, 4, 5, 6]); /// /// let _ = use_infinite_scroll_with_options( /// el, /// move |_| async move { /// let len = data.with(|d| d.len()); /// set_data.update(|data| *data = (1..len+6).collect()); /// }, /// UseInfiniteScrollOptions::default().distance(10.0), /// ); /// /// view! { ///
/// { item } ///
/// } /// # } /// ``` /// /// The returned signal is `true` while new data is being loaded. pub fn use_infinite_scroll(el: El, on_load_more: LFn) -> Signal where El: Into> + Clone + 'static, T: Into + Clone + 'static, LFn: Fn(ScrollState) -> LFut + 'static, LFut: Future, { use_infinite_scroll_with_options(el, on_load_more, UseInfiniteScrollOptions::default()) } /// Version of [`use_infinite_scroll`] that takes a `UseInfiniteScrollOptions`. See [`use_infinite_scroll`] for how to use. pub fn use_infinite_scroll_with_options( el: El, on_load_more: LFn, options: UseInfiniteScrollOptions, ) -> Signal where El: Into> + Clone + 'static, T: Into + Clone + 'static, LFn: Fn(ScrollState) -> LFut + 'static, LFut: Future, { let UseInfiniteScrollOptions { distance, direction, interval, on_scroll, event_listener_options, } = options; let on_load_more = store_value(on_load_more); let UseScrollReturn { x, y, is_scrolling, arrived_state, directions, measure, .. } = use_scroll_with_options( el.clone(), UseScrollOptions::default() .on_scroll(move |evt| on_scroll(evt)) .event_listener_options(event_listener_options) .offset(ScrollOffset::default().set_direction(direction, distance)), ); let state = ScrollState { x, y, is_scrolling, arrived_state, directions, }; let (is_loading, set_loading) = create_signal(false); let el = el.into(); let observed_element = create_memo(move |_| { let el = el.get(); el.map(|el| { let el = el.into(); if el.is_instance_of::() || el.is_instance_of::() { document() .document_element() .expect("document element not found") } else { el } }) }); let is_element_visible = use_element_visibility(observed_element); let check_and_load = store_value(None::>); check_and_load.set_value(Some(Rc::new({ let measure = measure.clone(); move || { let observed_element = observed_element.get_untracked(); if !is_element_visible.get_untracked() { return; } if let Some(observed_element) = observed_element { let scroll_height = observed_element.scroll_height(); let client_height = observed_element.client_height(); let scroll_width = observed_element.scroll_width(); let client_width = observed_element.client_width(); let is_narrower = if direction == Direction::Bottom || direction == Direction::Top { scroll_height <= client_height } else { scroll_width <= client_width }; if (state.arrived_state.get_untracked().get_direction(direction) || is_narrower) && !is_loading.get_untracked() { set_loading.set(true); let measure = measure.clone(); spawn_local(async move { join!( on_load_more.with_value(|f| f(state)), sleep(Duration::from_millis(interval as u64)) ); set_loading.set(false); sleep(Duration::ZERO).await; measure(); if let Some(check_and_load) = check_and_load.get_value() { check_and_load(); } }); } } } }))); let _ = watch( move || is_element_visible.get(), move |visible, prev_visible, _| { if *visible && !prev_visible.copied().unwrap_or_default() { measure(); } }, true, ); let _ = watch( move || state.arrived_state.get().get_direction(direction), move |_, _, _| { check_and_load .get_value() .expect("check_and_load is set above")() }, true, ); is_loading.into() } /// Options for [`use_infinite_scroll_with_options`]. #[derive(DefaultBuilder)] pub struct UseInfiniteScrollOptions { /// Callback when scrolling is happening. on_scroll: Rc, /// Options passed to the `addEventListener("scroll", ...)` call event_listener_options: UseEventListenerOptions, /// The minimum distance between the bottom of the element and the bottom of the viewport. Default is 0.0. distance: f64, /// The direction in which to listen the scroll. Defaults to `Direction::Bottom`. direction: Direction, /// The interval time between two load more (to avoid too many invokes). Default is 100.0. interval: f64, } impl Default for UseInfiniteScrollOptions { fn default() -> Self { Self { on_scroll: Rc::new(|_| {}), event_listener_options: Default::default(), distance: 0.0, direction: Direction::Bottom, interval: 100.0, } } } /// The scroll state being passed into the `on_load_more` callback of [`use_infinite_scroll`]. #[derive(Copy, Clone)] pub struct ScrollState { /// X coordinate of scroll position pub x: Signal, /// Y coordinate of scroll position pub y: Signal, /// Is true while the element is being scrolled. pub is_scrolling: Signal, /// Sets the field that represents a direction to true if the /// element is scrolled all the way to that side. pub arrived_state: Signal, /// The directions in which the element is being scrolled are set to true. pub directions: Signal, }