Infinite scroll

The on_intersect event tells when an element appears in or disappears from the visible part of the page. The event (IntersectionEvent) carries is_intersecting and the intersection_ratio.

To load more content as the user scrolls, put a "sentinel" element after the content and load the next page when it becomes visible:

    use vertigo::{
        bind, dom, render::render_list, AutoJsJson, AutoMap, Computed, IntersectionEvent,
        RequestBuilder, Resource, Value,
    };

    #[derive(AutoJsJson, Clone, PartialEq)]
    pub struct Post {
        id: u32,
        title: String,
    }

    // A cached request for every page
    let pages = AutoMap::new(|_, page: &u32| {
        RequestBuilder::get(format!("/api/posts?page={page}")).lazy_cache(|status, body| {
            if status == 200 {
                Some(body.into::<Vec<Post>>())
            } else {
                None
            }
        })
    });

    let page_count = Value::new(1);
    let page_numbers = page_count.map(|count| (1..=count).collect::<Vec<u32>>());

    // Every page is rendered once, when it's added to the list
    let list = render_list(
        &page_numbers,
        |page| *page,
        bind!(pages, |page| page.render_value(bind!(pages, |page| {
            pages.get(&page).render(|posts| {
                dom! {
                    <div>{..posts.iter().map(|post| dom! { <p>{&post.title}</p> })}</div>
                }
            })
        }))),
    );

    // Number of the last page, once it's loaded and not empty
    let last_loaded = Computed::from(bind!(pages, page_count, |ctx| {
        let count = page_count.get(ctx);
        match pages.get(&count).get(ctx) {
            Resource::Ready(posts) if !posts.is_empty() => Some(count),
            _ => None,
        }
    }));

    // A new sentinel for every loaded page, so it reports itself also when it's still visible
    let sentinel = last_loaded.render_value_option(bind!(page_count, |last_loaded| {
        let count = last_loaded?;
        let load_more = bind!(page_count, |event: IntersectionEvent| {
            if event.is_intersecting {
                page_count.set(count + 1);
            }
        });
        Some(dom! { <p on_intersect={load_more}>"Loading more..."</p> })
    }));

    dom! {
        <div>
            {list}
            {sentinel}
        </div>
    }

The event fires when the element is mounted and later only when its visibility changes. So if the sentinel stayed visible after loading a page (because the page was short), it wouldn't report again. That's why a new sentinel is rendered after every loaded page. And as there's no sentinel while a page is loading, the next page is never requested too early.

Loading only when visible

The same event can postpone loading or rendering of anything until the user actually gets there:

    use vertigo::{bind, dom, AutoJsJson, IntersectionEvent, RequestBuilder, Value};

    #[derive(AutoJsJson, Clone, PartialEq)]
    pub struct Comment {
        author: String,
        text: String,
    }

    let visible = Value::new(false);

    let on_intersect = bind!(visible, |event: IntersectionEvent| {
        if event.is_intersecting {
            visible.set(true);
        }
    });

    // Comments are fetched only when the user scrolls to them
    let comments = visible.render_value(|visible| {
        if !visible {
            return dom! { <p>"Comments"</p> };
        }

        RequestBuilder::get("/api/comments")
            .lazy_cache(|status, body| {
                if status == 200 {
                    Some(body.into::<Vec<Comment>>())
                } else {
                    None
                }
            })
            .render(|comments| {
                dom! {
                    <div>
                        {..comments.iter().map(|comment| dom! {
                            <p><b>{&comment.author}</b> ": " {&comment.text}</p>
                        })}
                    </div>
                }
            })
    });

    dom! {
        <section {on_intersect}>{comments}</section>
    }