Infinite Scroll

A long list that loads more rows as you near the end, built on the virtualized <list> element.

There's no live preview for this example. To run it, paste the code into a new app's src/app/app.ts.

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

type Contact = { id: string; name: string };

const PAGE_SIZE = 20;

const makeContacts = (offset: number): Contact[] =>
  Array.from({ length: PAGE_SIZE }, (_, i) => ({
    id: String(offset + i + 1),
    name: `Contact ${offset + i + 1}`,
  }));

@Component({
  selector: 'app-root',
  imports: [LYNX_ELEMENTS],
  template: `
    <view class="screen">
      <text class="status">
        {{ contacts().length }} loaded{{ loading() ? ', loading more…' : '' }}
      </text>
      <list
        class="contacts"
        list-type="single"
        scroll-orientation="vertical"
        [attr.lower-threshold-item-count]="5"
        (bindscrolltolower)="loadMore()"
      >
        @for (contact of contacts(); track contact.id) {
          <list-item [attr.item-key]="contact.id">
            <view class="row">
              <text class="name">{{ contact.name }}</text>
            </view>
          </list-item>
        }
      </list>
    </view>
  `,
  styles: `
    .screen {
      display: flex;
      flex-direction: column;
      height: 100vh;
    }
    .status {
      padding: 12px 16px;
      font-size: 13px;
      color: #71717a;
    }
    .contacts {
      flex: 1;
    }
    .row {
      padding: 16px;
      border-bottom: 1px solid #e4e4e7;
    }
    .name {
      font-size: 16px;
    }
  `,
})
export class App {
  readonly contacts = signal(makeContacts(0));
  readonly loading = signal(false);

  loadMore(): void {
    // The list can report the end again before the last batch arrives.
    if (this.loading()) return;
    this.loading.set(true);

    // Stands in for loading the next batch from a data source.
    setTimeout(() => {
      this.contacts.update((list) => [...list, ...makeContacts(list.length)]);
      this.loading.set(false);
    }, 800);
  }
}

What it shows

  • <list> — a virtualized list: it renders the rows near the screen and recycles them as you scroll. See <list>.
  • item-key — each <list-item> needs a unique, stable key so the list can recycle rows correctly.
  • (bindscrolltolower) — fires as you near the end. lower-threshold-item-count set to 5 fires it five items before the last one.
  • A loading guard — loading() stops a second batch from starting while one is on its way.
  • setTimeout as a stand-in — fetching data from a component isn't supported yet. See Network requests.