leptos_pagination/components/
paginated_for.rs

1use std::{marker::PhantomData, sync::Arc};
2
3use leptos::prelude::*;
4use leptos_windowing::{
5    InternalLoader, ItemWindow, cache::CacheStoreFields, item_state::ItemState,
6};
7use reactive_stores::{Store, StoreFieldIterator};
8
9use crate::{PaginationState, UsePaginationOptions, use_pagination};
10
11/// Slot that is rendered when an error occurs.
12#[derive(Clone)]
13#[slot]
14pub struct LoadError {
15    children: Arc<dyn Fn(String) -> AnyView + Send + Sync>,
16}
17
18/// Slot that is rendered when the data is being loaded.
19#[derive(Clone)]
20#[slot]
21pub struct Loading {
22    children: ChildrenFn,
23}
24
25/// Quite similar to Leptos' `<For>` this displays a list of items.
26///
27/// But these items are loaded and cached on-demand using the provided `loader`.
28///
29/// ## Example
30///
31/// ```
32/// # use std::ops::Range;
33/// #
34/// # use leptos::prelude::*;
35/// # use leptos_pagination::{Loading, PaginatedFor, PaginationState, PaginationPrev, PaginationNext, ExactLoader};
36/// #
37/// pub struct Book {
38///     title: String,
39///     author: String,
40/// }
41///
42/// # #[component]
43/// # pub fn App() -> impl IntoView {
44/// let state = PaginationState::new_store();
45///
46/// view! {
47///     <ul>
48///         <PaginatedFor
49///             loader=BookLoader
50///             query=()
51///             state
52///             item_count_per_page=20
53///             let:idx_book
54///         >
55///             // Shown when the data has finished loading.
56///             <li class={if idx_book.0 % 2 == 0 { "even" } else { "odd" }}>
57///                 <h3>{idx_book.1.title.clone()}</h3>
58///                 <p>{idx_book.1.author.clone()}</p>
59///             </li>
60///
61///             // Shown while the data is loading.
62///             <Loading slot>
63///                 <li class="loading">Loading...</li>
64///             </Loading>
65///         </PaginatedFor>
66///     </ul>
67///
68///     <div class="pagination-buttons">
69///         <PaginationPrev state attr:class="pagination-prev">
70///             "Previous"
71///         </PaginationPrev>
72///         <PaginationNext state attr:class="pagination-next">
73///             "Next"
74///         </PaginationNext>
75///     </div>
76/// }
77/// # }
78///
79/// pub struct BookLoader;
80///
81/// impl ExactLoader for BookLoader {
82///     type Item = Book;
83///     type Query = ();
84///     type Error = ();
85///
86///     async fn load_items(&self, range: Range<usize>, query: &Self::Query) -> Result<Vec<Self::Item>, Self::Error> {
87///         todo!()
88///     }
89///
90///     async fn item_count(&self, _query: &Self::Query) -> Result<Option<usize>, Self::Error> {
91///         todo!()
92///     }
93/// }
94///
95/// ```
96///
97/// For more in-depth demonstration, please refer to the example `pagination_rest_api`.
98#[component]
99pub fn PaginatedFor<T, L, Q, CF, V, M>(
100    /// The loader to get the data on-demand.
101    loader: L,
102
103    /// The query to get the data on-demand.
104    #[prop(into)]
105    query: Signal<Q>,
106
107    /// The pagination state.
108    ///
109    /// Used to communicate between the pagination controls and this component.
110    state: Store<PaginationState>,
111
112    /// How many items to display per page.
113    #[prop(into)]
114    item_count_per_page: Signal<usize>,
115
116    /// How many pages to load before and after the current page.
117    ///
118    /// A value of 1 means that the current page as well as the one before and after will be loaded.
119    /// Defaults to 1.
120    #[prop(default = 1)]
121    overscan_page_count: usize,
122
123    /// Slot that is rendered instead of `children` when the data is being loaded.
124    /// This is recommended to be used to show a loading skeleton.
125    #[prop(optional)]
126    loading: Option<Loading>,
127
128    /// Slot that is rendered instead of `children` when an error occurs.
129    #[prop(optional)]
130    load_error: Option<LoadError>,
131
132    /// The normal children are rendered when an item is loaded.
133    /// This would be a normal `<li>` or `<tr>` element for example.
134    children: CF,
135
136    #[prop(optional)] _marker: PhantomData<(M, L)>,
137) -> impl IntoView
138where
139    T: Send + Sync + 'static,
140    L: InternalLoader<M, Item = T, Query = Q> + 'static,
141    L::Error: Send + Sync,
142    Q: Send + Sync + 'static,
143    CF: Fn((usize, Arc<T>)) -> V + Send + Clone + 'static,
144    V: IntoView,
145{
146    let window: ItemWindow<T> = use_pagination(
147        state,
148        loader,
149        query,
150        item_count_per_page,
151        UsePaginationOptions::default().overscan_page_count(overscan_page_count),
152    );
153
154    view! {
155        <For each=move || window.range.get() key=|idx| *idx let:index>
156            {
157                let children = children.clone();
158                let loading = loading.clone();
159                let load_error = load_error.clone();
160                move || match &*window.cache.items().at_unkeyed(index).read() {
161                    ItemState::Loaded(item) => {
162                        children.clone()((index, Arc::clone(item))).into_any()
163                    }
164                    ItemState::Error(error) => {
165                        load_error
166                            .clone()
167                            .map(|e| (e.children)(error.clone()).into_any())
168                            .unwrap_or_else(|| {
169
170                                view! { <div style="color: red;">Error: {error.clone()}</div> }
171                                    .into_any()
172                            })
173                    }
174                    _ => {
175                        loading
176                            .clone()
177                            .map(|l| (l.children)().into_any())
178                            .unwrap_or_else(|| ().into_any())
179                    }
180                }
181            }
182        </For>
183    }
184}