<list>

A virtualized scrolling list for long or growing data. Only items near the screen are drawn, and their views are reused as you scroll.

Usage

src/app/contacts.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-contacts',
  imports: [LYNX_ELEMENTS],
  template: `
    <list list-type="single" style="height: 100vh;">
      @for (contact of contacts(); track contact.id) {
        <list-item [attr.item-key]="contact.id">
          <text>{{ contact.name }}</text>
        </list-item>
      }
    </list>
  `,
})
export class Contacts {
  readonly contacts = signal([{ id: '1', name: 'Ada Lovelace' }]);
}

Import LYNX_ELEMENTS in your component (see Elements).

Danger

Every <list-item> needs a unique item-key, or the app crashes. Use the same value as your @for track expression.

Attributes

AttributeTypeDefaultDescription
list-type'single' | 'flow' | 'waterfall''single'Layout: one column, a grid with aligned rows, or a masonry grid where each item goes into the shortest column.
span-countnumber1Columns (or rows, when horizontal) for flow and waterfall.
scroll-orientation'vertical' | 'horizontal''vertical'Scroll and layout direction.
enable-scrollbooleantrueWhether the user can scroll it.
enable-nested-scrollbooleanfalseiOS and Android. Lets it scroll inside another scroll container: the inner one scrolls first.
bouncesbooleantrueiOS only. Bounce at the edges.
scroll-bar-enablebooleantrueiOS only. Shows the scroll bar.
initial-scroll-indexnumber0Item to scroll to after the first render.
upper-threshold-item-countnumber0bindscrolltoupper fires when fewer than this many items remain above the screen.
lower-threshold-item-countnumber0bindscrolltolower fires when fewer than this many items remain below the screen. Use it to load more.
scroll-event-throttlenumber200Minimum milliseconds between bindscroll events.
need-visible-item-infobooleanfalseAdds attachedCells, the items on screen, to the scroll events.
need-layout-complete-infobooleanfalseiOS and Android. Adds item positions and the update's diff to bindlayoutcomplete.
layout-idnumber-1iOS and Android. An id you set with each data update. bindlayoutcomplete reports it back.
preload-buffer-countnumber0iOS and Android. Items to render ahead, outside the screen, so they don't pop in.
item-snap{ factor: number; offset: number; maxSnapCount?: number }—Paging: after each scroll, an item stops at factor (0 top, 1 bottom) plus offset px. single lists only.
stickybooleanfalseAllows items with sticky-top or sticky-bottom to pin.
sticky-offsetnumber0Distance in px from the edge at which sticky items pin.
update-animation'default' | 'none'—'default' animates inserted and removed items. single and flow lists only.
enable-async-listbooleantrue (set by AngularLynx)Required for items to render with AngularLynx's two threads. Leave it on.
enable-insert-platform-view-operationbooleanfalseiOS (Lynx 4.0 and later) and Android. Inserts item views through the native list's own operations.
experimental-search-ref-anchor-strategy0 | 1 | 20Experimental, Lynx 4.0 and later. How the list keeps its place when on-screen items are removed: 0 doesn't, 1 searches toward the start, 2 toward the end.

Plus the common attributes, such as id, class and style.

Item gaps are CSS properties that only <list> understands, not attributes. Set list-main-axis-gap and list-cross-axis-gap in style, e.g. style="list-main-axis-gap: 8px;".

Events

Read the payload from $event.detail (see Handling taps and events).

EventdetailDescription
bindscroll{ scrollTop: number; scrollLeft: number; scrollHeight: number; scrollWidth: number; deltaX: number; deltaY: number; listWidth: number; listHeight: number; eventSource: number; attachedCells?: object[] }Fires while it scrolls, at most every scroll-event-throttle ms. attachedCells needs need-visible-item-info.
bindscrolltoupperSame as bindscrollNear the start, by upper-threshold-item-count.
bindscrolltolowerSame as bindscrollNear the end, by lower-threshold-item-count. Use it to load more.
bindscrollstatechange{ state: 1 | 2 | 3 | 4 }iOS and Android. Scroll state: 1 stopped, 2 dragging, 3 slowing down, 4 animating.
bindlayoutcomplete{ 'layout-id': number; scrollInfo: object; diffResult?: object; visibleItemBeforeUpdate?: object[]; visibleItemAfterUpdate?: object[] }Layout finished, after the first render or an update. The optional fields need need-layout-complete-info.
bindsnap{ position: number; currentScrollTop: number; currentScrollLeft: number; targetScrollTop: number; targetScrollLeft: number }With item-snap: about to snap to the item at position.

Plus the common events, such as bindtap.

Methods

These UI methods (Glossary) scroll it from code. Call them with invoke() on the element (see Call element methods). A call from a handler returns nothing: to read a result, call invoke() in a main-thread script instead.

MethodParametersDescription
scrollToPosition{ index: number; alignTo?: 'top' | 'middle' | 'bottom'; offset?: number; itemKey?: string; smooth?: boolean }Scrolls to the item at index, or the one with itemKey. It aligns the item's top unless you set alignTo.
scrollBy{ offset: number }Scrolls by offset px.
autoScroll{ rate: string; start?: boolean; autoStop?: boolean }Scrolls continuously at rate per second, e.g. '60px'. It stops at the end unless autoStop is false.
getVisibleCells—Returns the items on screen.
getScrollInfo—Returns { scrollX, scrollY, maxScrollOffset } in px.
import { Component, ElementRef, viewChild } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-inbox',
  imports: [LYNX_ELEMENTS],
  template: `
    <list #inbox list-type="single" style="height: 400px;">
      <list-item item-key="welcome"><text>Welcome</text></list-item>
    </list>
    <view (bindtap)="toTop()"><text>Back to top</text></view>
  `,
})
export class Inbox {
  readonly inbox = viewChild.required('inbox', { read: ElementRef });

  toTop(): void {
    this.inbox().nativeElement.invoke?.('scrollToPosition', {
      index: 0,
      smooth: true,
    });
  }
}

Children

Every direct child must be a <list-item>. Don't set z-index on a <list-item>, because it breaks view reuse: set it on the content inside.

<list-item>

AttributeTypeDefaultDescription
item-keystring—Required. A unique, stable key for the item.
full-spanbooleanfalseTakes a whole row (or column) in a flow or waterfall list.
sticky-topbooleanfalsePins to the top while it scrolls. Needs sticky on the list.
sticky-bottombooleanfalsePins to the bottom. Needs sticky on the list.
recyclablebooleantruefalse keeps the item alive off screen, so it isn't rendered again when it comes back.
reuse-identifierstring—Only items with the same value share reused views. Give items with very different layouts different values.
estimated-main-axis-size-pxnumber-1Expected size in px along the scroll direction, used until the item renders. The closer it is, the less the list flickers.