-
Notifications
You must be signed in to change notification settings - Fork 41
example infinite list
Every infinite-scroll plugin in the wild reinvents the same five primitives — IntersectionObserver on a sentinel below the last row, an AbortController to cancel in-flight pages on filter change, dedup-by-id so refetches don't render the same row twice, cursor pagination, and a "loading more" indicator separate from the window-level spinner.
wp.os.createInfiniteList() ships every piece of that. Stable.
const list = wp.os.createInfiniteList( {
root: document.getElementById( 'feed-root' ),
fetchPage: async ( cursor, signal ) => {
const res = await wp.os.fetch(
'/wp-json/myplugin/v1/feed?cursor=' + encodeURIComponent( cursor ?? '' ),
{ signal },
);
const json = await res.json();
return { items: json.items, nextCursor: json.next };
},
getId: ( post ) => post.id,
renderItem: ( post ) => {
const li = document.createElement( 'li' );
li.className = 'feed-row';
li.textContent = post.title;
return li;
},
} );That's the whole feed reader. The first page fetches on mount; the sentinel below the rendered rows triggers the next page when it scrolls into view; rows with the same id only render once; the list stops requesting pages when nextCursor is null.
interface InfiniteListOptions< TItem > {
root: HTMLElement;
fetchPage: ( cursor: string | null, signal: AbortSignal )
=> Promise< { items: TItem[]; nextCursor?: string | null } >;
getId: ( item: TItem ) => string | number;
renderItem: ( item: TItem, index: number ) => HTMLElement;
sentinel?: HTMLElement; // override the default 1px sentinel
rootMargin?: string; // IntersectionObserver rootMargin (default '200px')
initialCursor?: string | null; // first call's cursor (default null)
onLoadingChange?: ( loading: boolean ) => void;
onError?: ( err: unknown ) => void;
}interface InfiniteList {
reset(): void; // re-fetch from initialCursor
loadMore(): Promise< void >; // request the next page (sentinel does this for you)
hasMore(): boolean; // false once nextCursor returns null/empty
isLoading(): boolean;
destroy(): void; // disconnect observer, abort in-flight, unmount sentinel
}The single most important thing the helper does for you: when the user changes a filter, call list.reset(). Any in-flight page from the old filter aborts (the AbortController cancels the request the user no longer wants), the rendered rows clear, the dedup set wipes, and a fresh page fetches from initialCursor.
filterInput.addEventListener( 'input', ( e ) => {
currentFilter = e.target.value;
list.reset();
} );If the slow old request resolves AFTER the new one has started, the helper drops it on the floor — no stale rows.
Native windows: pair the call with the new render-ctx.signal so destroy fires on close.
window.openStationNativeWindows[ 'my-feed-inbox' ] = ( body, { signal } ) => {
const root = body.querySelector( '.feed' );
const list = wp.os.createInfiniteList( {
root,
fetchPage: ( cursor, fetchSignal ) =>
// The fetch's own signal is what aborts on filter
// change. The outer `signal` (window close) is wired
// up via the cleanup return below.
myFetchPage( cursor, fetchSignal ),
getId: ( p ) => p.id,
renderItem: buildRow,
} );
signal.addEventListener( 'abort', () => list.destroy() );
return () => list.destroy();
};The shell's window-level spinner is reserved for the first paint. For the "loading more" indicator at the bottom of the feed (the one users see while scrolling), wire onLoadingChange to your own affordance:
const moreSpinner = root.querySelector( '.feed-more-spinner' );
wp.os.createInfiniteList( {
root,
fetchPage,
getId, renderItem,
onLoadingChange: ( loading ) => {
moreSpinner.toggleAttribute( 'hidden', ! loading );
},
} );That keeps the title-bar dot and the loading overlay free to mean what they always mean (wp.os.fetch activity + first-paint readiness) — and the user gets a "loading more" indicator that doesn't conflict with either.
The default sentinel is a 1px <div> appended after the rendered items. To use a "Load more" button (or anything else), pass sentinel:
const moreBar = document.createElement( 'button' );
moreBar.type = 'button';
moreBar.textContent = 'Load more';
root.appendChild( moreBar );
const list = wp.os.createInfiniteList( {
root,
sentinel: moreBar,
fetchPage, getId, renderItem,
} );
moreBar.addEventListener( 'click', () => list.loadMore() );The IntersectionObserver still wires up — your bar auto-loads on scroll AND on click, which is what users expect from "load more" affordances that don't disappear off-screen.
-
render-ctx.md—signalfrom the render ctx aborts on close, perfect forlist.destroy()on the way out. -
window-activity.md—wp.os.fetchso the title-bar dot pulses while pages load. -
keyed-list.md— when the list is small + bounded and you need stable keys for the rows you DO render, not "render more on scroll".
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config