# Listine — Angular Variable-Height Virtual Scroller

[![npm version](https://img.shields.io/npm/v/listine.svg)](https://www.npmjs.com/package/listine)
[![npm downloads](https://img.shields.io/npm/dm/listine.svg)](https://www.npmjs.com/package/listine)
[![license](https://img.shields.io/npm/l/listine.svg)](https://github.com/hrssh11/listine/blob/main/LICENSE)
[![Angular](https://img.shields.io/badge/Angular-21-dd0031.svg)](https://angular.dev)

A lightweight, high-performance virtual scroll component for Angular that renders **only the items currently visible** — and unlike most virtual scrollers, it works with **variable (unknown) item heights** out of the box.

Render lists with **millions of rows** smoothly, without freezing the browser.

---

## Why Listine?

Most virtual scrollers (Angular CDK, PrimeNG, etc.) require a **fixed item size**. Listine measures each row as it renders, so it handles dynamic, content-driven heights without you predefining anything.

- **True dynamic height support** — measures real DOM heights, no fixed `itemSize` required.
- **Built for huge lists** — only visible rows live in the DOM, even with 1,000,000+ items.
- **Signals-based & zoneless-friendly** — modern Angular APIs (`input()`, `output()`, `signal()`, `effect()`), `OnPush` change detection.
- **Standalone component** — no NgModule, drop it straight into any Angular app.
- **Flexible templating** — render each item with your own template, with access to the item and its index.
- **Lazy-load ready** — `scrollToEnd` output makes infinite scroll trivial.

---

## Installation

```bash
npm install listine
```

> Requires **Angular 21+** (`@angular/core` and `@angular/common` `^21.0.0`).

---

## Quick Start

Import the standalone component and use it in your template.

```ts
import { Component } from "@angular/core";
import { VariableVirtualScrollComponent } from "listine";

@Component({
  selector: "app-root",
  standalone: true,
  imports: [VariableVirtualScrollComponent],
  template: `
    <listine-variable-virtual-scroll
      [items]="options"
      [buffer]="10"
      [viewportHeight]="600"
      [itemTemplate]="itemTemplate"
      (scrollToEnd)="loadMore()"
    >
      <ng-template #itemTemplate let-option let-i="index">
        <p>{{ option.label }} (ID: {{ option.value }})</p>
      </ng-template>
    </listine-variable-virtual-scroll>
  `,
})
export class AppComponent {
  options = Array.from({ length: 1_000_000 }, (_, i) => ({
    label: `Item ${i}`,
    value: i.toString(),
  }));

  loadMore() {
    // fetch / append the next page of data
  }
}
```

---

## API

### Inputs

| Input               | Type                | Default | Description                                                                 |
| ------------------- | ------------------- | ------- | --------------------------------------------------------------------------- |
| `items`             | `any[]`             | `[]`    | The full list of data to virtualize.                                        |
| `itemTemplate`      | `TemplateRef<any>`  | —       | **Required.** Template used to render each item.                            |
| `viewportHeight`    | `number`            | `400`   | Height of the scrollable viewport, in pixels.                               |
| `buffer`            | `number`            | `5`     | Extra rows rendered above/below the viewport for smoother scrolling.        |
| `initialItemHeight` | `number`            | `50`    | Estimated row height (px) used before a row is measured.                    |
| `panelOpen`         | `boolean`           | `false` | Recalculates layout when shown inside a panel/overlay (prevents blank view).|
| `scrollToTopTrigger`| `number`            | `0`     | Change this value to reset the list to the top (e.g. after filter/search).  |

### Outputs

| Output          | Payload  | Description                                              |
| --------------- | -------- | ------------------------------------------------------- |
| `scrollEmitter` | `number` | Emits the current scroll position on every scroll.      |
| `scrollToEnd`   | `void`   | Emits when the user reaches the end — ideal for lazy load. |

### Template context

The item template receives:

- `$implicit` — the current item (e.g. `let-option`)
- `index` — the absolute index in `items` (e.g. `let-i="index"`)

### Public methods

| Method          | Description                                            |
| --------------- | ----------------------------------------------------- |
| `scrollToTop()` | Scrolls the list back to the top and recalculates rows. |

---

## Tips

- Put the component in a container with a defined height; the scroll area is driven by `viewportHeight`.
- For filter/search, bump `scrollToTopTrigger` so the user starts from the top of the new results.

---

## Changelog

See [CHANGELOG.md](https://github.com/hrssh11/listine/blob/main/CHANGELOG.md) for the full release history.

## Author

- [@hrssh11](https://www.github.com/hrssh11)

## License

[MIT](LICENSE)
