Utilities

InfiniteScroll

Calls on_more() when the end of its list scrolls into view, with Loading more… and That's all states and a Load more button.

#utility#list#loading#data#interactive

Demo

Preview
  • Entry 1
  • Entry 2
  • Entry 3
  • Entry 4
  • Entry 5
  • Entry 6
  • Entry 7
  • Entry 8
  • Entry 9
  • Entry 10

10 of 40 entries, asked for more 0 times.

Scroll the box to its end: ten more come each time, until forty. The Load more button does the same.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/infinite_scroll.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/infinite_scroll.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/infinite_scroll.qwe
component InfiniteScrollDemoLive() {    state items = range(1, 11)    state asked = 0    let limit = 40    <div.demo-stack>        <div id="infinite-scroll-demo-box" tabindex="0" aria-label="Entries"             style="height: 240px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius)">            <InfiniteScroll scroller="infinite-scroll-demo-box" margin="40px" done={len(items) >= limit}                            on_more={() => {                                asked += 1                                items = items + range(len(items) + 1, min([limit, len(items) + 10]) + 1)                            }}>                <ul style="margin: 0; padding: 0; list-style: none">                    {for i in items => <li key={i} style="padding: 10px 16px; border-bottom: 1px solid var(--ui-border)">Entry {i}</li>}                </ul>            </InfiniteScroll>        </div>        <p.demo-note>{len(items)} of {limit} entries, asked for more {asked} {asked == 1 ? "time" : "times"}.</p>    </div>}view InfiniteScrollDemo() =    <div.demo-stack>        <InfiniteScrollDemoLive/>        <p.demo-note>Scroll the box to its end: ten more come each time, until forty. The Load more button does the same.</p>    </div>

Source

ui/infinite_scroll.qwe
// InfiniteScroll: a list that asks for more as its end comes into view. It// wraps the items; under them is a sentinel that, when scrolled into view (in// the window, or in a scrolling box), calls on_more(). A "Load more" button is// there too, for the keyboard and for when the observer cannot run.////   <InfiniteScroll loading={busy} done={len(items) >= total} on_more={() => { busy = true; items += fetch_page(len(items)); busy = false }}>//       {for it in items => <li key={it.id}>{it.name}</li>}//   </InfiniteScroll>//// children: the items (any markup). on_more(): asked for the next page, with// nothing; it is not asked again while loading is true, nor once done is true.// loading: show "Loading more…" and hold off. done: there is no more; it says// done_text ("That's all") and stops asking. margin: how far before the end it// asks, a CSS length ("200px"). scroller: the id of the scrolling box the list// is in, when it is not the window. loading_text, done_text, button: the words// (button: "Load more"). label: what screen readers call the loading status.// Inside one of your components, since it needs the callback.import ui/theme.qwe use ui_clslet infinite_scroll_css = css`.ui-infscroll { display: grid; gap: 0; }.ui-infscroll-end { display: grid; justify-items: center; gap: 8px; padding: 14px; font: 13px/1.4 var(--ui-font); color: var(--ui-muted); text-align: center; }.ui-infscroll-spin { width: 18px; height: 18px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--ui-accent) 20%, transparent);  border-top-color: var(--ui-accent); animation: ui-infscroll-turn .75s linear infinite; }.ui-infscroll-btn { height: 32px; padding: 0 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  color: var(--ui-accent); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-infscroll-btn:hover { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-infscroll-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }@keyframes ui-infscroll-turn { to { transform: rotate(360deg); } }@media (prefers-reduced-motion: reduce) { .ui-infscroll-spin { animation: none; border-color: var(--ui-accent); } }`component InfiniteScroll(on_more = null, loading: bool = false, done: bool = false, margin: str = "200px", scroller: str? = null,                         loading_text: str = "Loading more…", done_text: str = "That's all", button: str = "Load more",                         label: str = "More items", class: str? = null, children) {    style global infinite_scroll_css    state watcher = null    state sentinel = null    let ask = () => { if !loading && !done && on_more != null { on_more() } }    // Once loading ends (or done is lifted), look again: the end may still be in view.    if watcher != null && sentinel != null && !loading && !done {        watcher.unobserve(sentinel)        watcher.observe(sentinel)    }    <div class={ui_cls(["ui-infscroll", class])}>        {children}        <div.ui-infscroll-end role="status" aria-label={label}             @mount={(el) => {                 sentinel = el                 let root = scroller != null ? js("document.getElementById", scroller) : null                 let seen = (entries, obs) => {                     if any(entries, (en) => en.isIntersecting) {                         ask()                         // Still in view after the new items came? Then ask again.                         js("requestAnimationFrame", (_t = null) => { obs.unobserve(el); obs.observe(el) })                     }                 }                 let w = js_new("IntersectionObserver", seen, { root, rootMargin: margin })                 w.observe(el)                 watcher = w                 () => w.disconnect()             }}>            {done                ? <span>{done_text}</span>                : (loading                    ? [<span.ui-infscroll-spin aria-hidden="true"></span>, <span>{loading_text}</span>]                    : <button.ui-infscroll-btn type="button" @click={ask}>{button}</button>)}        </div>    </div>}
Thread
610Useful? Like it, or add to its thread below.

Thread 0 posts

On InfiniteScroll: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.