Angular-Leitfaden

So baust du Slider mit @reelkit/angular.

Touch zuerst
Wischen mit Schwung und Einrasten
Tastatur
Pfeiltasten + Escape
Mausrad
Optional mit Debounce
Virtualisiert
10.000+ Elemente, 3 im DOM
Indikatoren
Mitlaufende Punkte wie bei Instagram
API im Code
next(), prev(), goTo() über apiReady
Endlosmodus
Navigation im Kreis ohne Ende
Richtung
Vertikal oder horizontal
Signalbasiert
OnPush mit Angular-Signals

Die Komponente rk-reel

Die Komponente rk-reel kapselt den Slider-Controller aus dem Core. Sie ist standalone und nutzt ChangeDetectionStrategy.OnPush.

app.component.ts
import { Component } from '@angular/core';
import { ReelComponent, ReelIndicatorComponent, RkReelItemDirective } from '@reelkit/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReelComponent, ReelIndicatorComponent, RkReelItemDirective],
  template: `
    <rk-reel
      [count]="items.length"
      style="width: 100%; height: 100dvh"
      direction="vertical"
      [enableWheel]="true"
      (afterChange)="onAfterChange($event)"
    >
      <ng-template rkReelItem let-i let-size="size">
        <div [style.width.px]="size[0]" [style.height.px]="size[1]"
             [style.background]="items[i].color"
             style="display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff">
          <div style="font-size:1.5rem;font-weight:bold">{{ items[i].title }}</div>
          <div style="font-size:0.875rem;opacity:0.8">{{ items[i].subtitle }}</div>
        </div>
      </ng-template>

      <div style="position:absolute;right:12px;top:50%;transform:translateY(-50%);z-index:10">
        <rk-reel-indicator direction="vertical" />
      </div>
    </rk-reel>
  `,
})
export class AppComponent {
  items = [
    { title: 'Virtualized', subtitle: 'Only 3 slides in DOM', color: '#6366f1' },
    { title: 'Touch First', subtitle: 'Native swipe gestures', color: '#8b5cf6' },
    { title: 'Zero Deps', subtitle: 'Tiny bundle size', color: '#7c3aed' },
    { title: 'Keyboard Nav', subtitle: 'Full a11y support', color: '#ec4899' },
    { title: 'SSR Ready', subtitle: 'Works everywhere', color: '#14b8a6' },
    { title: '60fps', subtitle: 'Smooth animations', color: '#f59e0b' },
  ];

  onAfterChange(event: { index: number; indexInRange: number }) {
    console.log('Current index:', event.index);
  }
}

Automatische Größe

Der Input size ist optional. Fehlt er, misst rk-reel seinen Container selbst über ResizeObserver und passt sich an das Layout aus dem CSS an. Die Größe des Containers muss vom Elternelement kommen (zum Beispiel Flex, Grid oder feste Maße im CSS).

html
<!-- Explicit size (fixed) -->
<rk-reel [count]="items.length" [size]="[400, 600]">
  <ng-template rkReelItem let-i let-size="size"> ... </ng-template>
</rk-reel>

<!-- Auto-size (responsive — sized by CSS) -->
<rk-reel [count]="items.length" style="width: 100%; height: 100dvh">
  <ng-template rkReelItem let-i let-size="size"> ... </ng-template>
</rk-reel>

Template-Muster mit rkReelItem

Statt eines Render-Props wie in React nutzt Angular die strukturelle Direktive rkReelItem auf einem ng-template. Das ermöglicht die Virtualisierung: Nur sichtbare Slides werden erzeugt. Der Template-Kontext stellt drei Variablen bereit:

html
<ng-template rkReelItem let-i let-indexInRange="indexInRange" let-size="size">
  <!--
    $implicit (let-i)   : number  — absolute slide index (0 to count-1)
    indexInRange        : number  — position in visible window (0, 1, or 2)
    size                : [number, number] — [width, height] of the container
  -->
  <app-slide [data]="items[i]"
             [style.width.px]="size[0]"
             [style.height.px]="size[1]" />
</ng-template>

Eingebaute Wege der Navigation:

  • Touch/Wischen: Ziehen navigiert mit Schwung und Einrasten
  • Tastatur: Pfeiltasten und Escape
  • Mausrad: Einschalten mit [enableWheel]="true"
  • Im Code: Über den Output (apiReady) bekommst du next(), prev() und goTo()
typescript
import { Component } from '@angular/core';
import { ReelComponent, RkReelItemDirective, type ReelApi } from '@reelkit/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReelComponent, RkReelItemDirective],
  template: `
    <rk-reel
      [count]="10"
      [size]="[400, 600]"
      (apiReady)="api = $event"
    >
      <ng-template rkReelItem let-i let-size="size">
        <app-slide [index]="i" [size]="size" />
      </ng-template>
    </rk-reel>

    <button (click)="api?.prev()">Prev</button>
    <button (click)="api?.next()">Next</button>
    <button (click)="api?.goTo(5)">Go to 5</button>
  `,
})
export class AppComponent {
  api: ReelApi | undefined;
}

URL-State

createOverlayUrlState baut einen URL-State-Controller für ein Overlay und gibt ihn vollständig zurück. Du übergibst ihn dann einer *UrlOverlay-Komponente als Input [controller]. Rufe die Funktion in einem Injection-Kontext auf, also in einem Feld-Initialisierer; sie bindet sich sofort an und gibt sich über DestroyRef wieder frei. Die URL bestimmt, ob es offen ist, also öffnet sich ein gebundenes Overlay selbst, und ein Link ist der übliche Weg, es zu öffnen. Das erste Schreiben eines fehlenden Parameters legt einen Eintrag im Verlauf an, jedes weitere Schreiben ersetzt ihn, so dass Blättern den Zurück-Button nie verschüttet. Behalte den Controller, um value/position zu lesen und ihn im Code zu steuern: set(position) öffnet, set(null) schließt, und set ist dasselbe einfache Schreiben, das das Overlay beim Slide-Wechsel intern nutzt.

typescript
import { Component, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { RkLightboxUrlOverlayComponent } from '@reelkit/angular-lightbox';
import { createOverlayUrlState, urlIndexKey } from '@reelkit/angular';

@Component({
  imports: [RkLightboxUrlOverlayComponent, RouterLink],
  template: `
    @for (image of images(); track image.src; let i = $index) {
      <a [routerLink]="[]" [queryParams]="{ photo: i }">
        <img [src]="image.src" alt="" />
      </a>
    }

    <rk-lightbox-url-overlay [controller]="photo" [items]="images()" />
  `,
})
export class GalleryComponent {
  protected readonly images = signal(photos);

  // Attaches now, releases on destroy.
  protected readonly photo = createOverlayUrlState({
    param: 'photo',
    ...urlIndexKey(() => this.images().length),
  });
}

App mit Router: Übergib einen Adapter, der den Router nutzt. Sonst veraltet die Location des Routers, und seine nächste Navigation verwirft den Parameter:

typescript
import { createRouterUrlAdapter } from '@reelkit/angular/ng-router-url-adapter';

protected readonly photo = createOverlayUrlState({
  param: 'photo',
  adapter: createRouterUrlAdapter(),
  ...urlIndexKey(() => this.images().length),
});

Das Options-Objekt nimmt param, codec und locator (alle drei Pflicht) sowie einen optionalen adapter. codec und locator sind ein zusammengehöriges Paar mit demselben Id. Eine einfache Galerie mit ?photo=3 setzt daher ...urlIndexKey(() => images().length) ein, das beide Hälften auf einmal liefert. urlIndexKey prüft den Index gegen die aktuelle Anzahl, die der Getter liefert. Ein veraltetes ?photo=99 wird also abgelehnt und verschwindet von selbst aus der URL, statt eine Slide zu öffnen, die es nie gab. Ein Feed mit Seiten oder eine Galerie mit Schlüsseln nach Identität liefert stattdessen ihr eigenes Paar aus codec und locator. Die vollständige Tabelle der Optionen steht in der Angular-API-Referenz.

ReelIndicator

Optionale Komponente, die Fortschrittsindikatoren wie bei Instagram anzeigt und die aktuelle Position im Slider zeigt. Steht sie innerhalb eines rk-reel, verbindet sie sich über das Injection-Token RK_REEL_CONTEXT automatisch mit den Werten count und active des Elternelements. Du musst nichts selbst verdrahten.

html
<!-- Auto-connect: count and active are inherited from parent rk-reel -->
<rk-reel [count]="10" [size]="[400, 600]">
  <ng-template rkReelItem let-i let-size="size"> ... </ng-template>
  <rk-reel-indicator direction="vertical" />
</rk-reel>

<!-- Manual usage: pass count and active explicitly (e.g. outside a rk-reel) -->
<rk-reel-indicator [count]="10" [active]="currentIndex" />

Output apiReady — signalbasiertes Muster

Der Output (apiReady) feuert einmal, nachdem die Komponente eingehängt und vermessen ist. Er liefert ein ReelApi-Objekt, das du speichern und für die imperative Navigation nutzen kannst. Mit Angular-Signals für diese Referenz funktioniert das sauber mit der Change Detection OnPush.

typescript
import { Component, signal } from '@angular/core';
import { ReelComponent, RkReelItemDirective, type ReelApi } from '@reelkit/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReelComponent, RkReelItemDirective],
  template: `
    <rk-reel
      [count]="items.length"
      style="width: 100%; height: 100dvh"
      direction="vertical"
      [enableWheel]="true"
      (apiReady)="reelApi.set($event)"
      (afterChange)="currentIndex.set($event.index)"
    >
      <ng-template rkReelItem let-i let-size="size">
        <div [style.width.px]="size[0]" [style.height.px]="size[1]">
          {{ items[i].title }}
        </div>
      </ng-template>
    </rk-reel>

    <div style="position:absolute;bottom:16px;left:50%;transform:translateX(-50%)">
      <button (click)="reelApi()?.prev()"
              [disabled]="currentIndex() === 0">Prev</button>
      <button (click)="reelApi()?.next()"
              [disabled]="currentIndex() === items.length - 1">Next</button>
    </div>
  `,
})
export class AppComponent {
  readonly items = [
    { title: 'Slide 1', color: '#6366f1' },
    { title: 'Slide 2', color: '#8b5cf6' },
    { title: 'Slide 3', color: '#ec4899' },
  ];

  readonly reelApi = signal<ReelApi | undefined>(undefined);
  readonly currentIndex = signal(0);
}

Das Wichtigste

  • Standalone-Komponente

    Importiere ReelComponent, RkReelItemDirective und optional ReelIndicatorComponent direkt in das Array imports deiner Komponente

  • ng-template + rkReelItem

    Das Gegenstück in Angular zum Prop itemBuilder aus React — es ermöglicht die Virtualisierung

  • apiReady

    Ein Output, der einmal mit der API für die imperative Navigation feuert — ohne Abfrage über ViewChild

  • afterChange

    Liefert { index, indexInRange } — so verfolgst du den aktuellen Index für die Oberfläche

  • Standardmäßig OnPush

    Alle Komponenten nutzen ChangeDetectionStrategy.OnPush und Angular-Signals für die beste Performance

Tipps zur Performance

  • Halte die Slide-Templates schlank

    Das Template rkReelItem läuft für jede sichtbare Slide (meist 3 gleichzeitig). Vermeide darin aufwendige Berechnungen oder tief verschachtelte Strukturen.

  • Lade Daten nahe am Ende

    Erkenne mit (afterChange), wann sich der Nutzer dem Ende nähert, und lade den nächsten Schwung, bevor die Slides ausgehen — so entstehen endlos scrollende Feeds.

  • Nutze Signals für den imperativen Zustand

    Speichere die Referenz auf ReelApi und den aktuellen Index in Angular-Signals. So bekommst du feingranulare Reaktivität, ohne die ganze Komponente neu zu rendern.

  • Schalte das Mausrad auf scrollbaren Seiten ab

    Setze [enableWheel]="false", wenn der Slider in einem scrollbaren Layout steckt, damit er das Scrollen der Seite nicht abfängt.

Nächste Schritte