Angular ガイド

@reelkit/angular でスライダーを作る方法を説明します。

タッチ優先
慣性とスナップのあるスワイプ
キーボード操作
矢印キーと Escape
ホイールスクロール
任意、デバウンス付き
仮想化
10,000 件以上、DOM には 3 つ
インジケーター
Instagram 風のドットのスクロール
プログラムからの操作
apiReady で next()、prev()、goTo()
ループモード
終わりのない循環ナビゲーション
方向を選べる
縦でも横でも
シグナルベース
Angular のシグナルと OnPush

rk-reel コンポーネント

rk-reel コンポーネントはコアのスライダーコントローラーを包みます。スタンドアロンで、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);
  }
}

サイズの自動計測

size の入力は任意です。省略すると、rk-reel は ResizeObserver でコンテナーを自動計測し、CSS で決まるレイアウトに合わせます。コンテナーのサイズは親要素で決める必要があります(flex、grid、または 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>

rkReelItem テンプレートのパターン

React のレンダープロップの代わりに、Angular では ng-template に構造ディレクティブ rkReelItem を使います。これによって仮想化が可能になり、インスタンス化されるのは表示中のスライドだけです。テンプレートのコンテキストは 3 つの変数を提供します:

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>

組み込みの操作方法:

  • タッチ/スワイプ: ドラッグで移動し、慣性とスナップが効きます
  • キーボード: 矢印キーと Escape
  • マウスホイール: [enableWheel]="true" で有効にします
  • プログラムから: (apiReady) 出力で next()、prev()、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 の状態

createOverlayUrlState はオーバーレイ用の URL 状態コントローラーを作り、そのまま返します。それを *UrlOverlay コンポーネントの [controller] 入力に渡します。インジェクションコンテキスト(フィールドの初期化子)の中で呼んでください。すぐに接続し、DestroyRef で解放されます。開閉状態は URL が持つので、結び付けられたオーバーレイは自分で開き、開く操作は通常リンクになります。存在しないパラメーターへの最初の書き込みは履歴エントリーを 1 つ積み、以降の書き込みはそれを置き換えるので、ページを送っても戻るボタンが埋もれることはありません。コントローラーは手元に残しておき、value/position を読んだり、プログラムから操作したりするのに使います。set(position) で開き、set(null) で閉じます。set は、オーバーレイがスライドの切り替え時に内部で使っているのと同じ低レベルの書き込みです。

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),
  });
}

ルーターを使うアプリでは、Router に連動したアダプターを渡してください。そうしないと Router 自身の位置情報が古くなり、次のナビゲーションでパラメーターが消えます:

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

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

オプションのオブジェクトは param、codec、locator(3 つとも必須)と、任意の adapter を受け取ります。codec と locator は同じ Id を共有する対なので、単純な ?photo=3 のギャラリーでは ...urlIndexKey(() => images().length) を展開すれば、両方が一度に返されます。urlIndexKey は getter が返す現在の件数でインデックスを制限するので、古い ?photo=99 は拒否され、指されていないスライドを開く代わりに URL から自動で取り除かれます。ページングされたフィードや識別子で指すギャラリーは、代わりに独自の codec と locator の対を用意します。すべてのオプションの表は Angular API リファレンス にあります。

ReelIndicator

スライダー内の現在位置を示す、Instagram 風の進捗インジケーターを表示する任意のコンポーネントです。rk-reel の中に置くと、RK_REEL_CONTEXT のインジェクショントークンを通じて親の count と active の値に自動で接続するので、手でつなぐ必要はありません。

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" />

apiReady 出力 — シグナルベースのパターン

(apiReady) 出力は、コンポーネントがマウントされて計測されたあとに 1 回だけ発火します。保持して命令的なナビゲーションに使える ReelApi オブジェクトを出力します。この参照を Angular のシグナルに持たせると、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);
}

ポイント

  • スタンドアロンコンポーネント

    ReelComponent、RkReelItemDirective、必要なら ReelIndicatorComponent を、コンポーネントの imports 配列に直接インポートします

  • ng-template + rkReelItem

    React の itemBuilder プロップに相当する Angular の仕組みで、仮想化を可能にします

  • apiReady

    命令的なナビゲーションの API とともに 1 回だけ発火する出力です。ViewChild で問い合わせる必要はありません

  • afterChange

    { index, indexInRange } を出力します。UI を更新するために現在のインデックスを追跡します

  • デフォルトで OnPush

    すべてのコンポーネントが ChangeDetectionStrategy.OnPush と Angular のシグナルを使い、性能を最大限に引き出します

パフォーマンスのヒント

  • スライドのテンプレートを軽く保つ

    rkReelItem のテンプレートは表示中のスライドごと(通常は同時に 3 枚)に実行されます。中で重い計算や深い入れ子の構造を使わないでください。

  • 端に近づいたらデータを読み込む

    (afterChange) でユーザーが終わりに近づいたことを検知し、スライドが尽きる前に次のバッチを取得します。これで無限スクロールのフィードが作れます。

  • 命令的な状態にはシグナルを使う

    ReelApi の参照と現在のインデックスを Angular のシグナルに保持すると、コンポーネント全体を再描画させずに細かなリアクティビティが得られます。

  • スクロールするページではホイールを無効にする

    スクロールするレイアウトの中にスライダーを埋め込むときは、ページのスクロールを奪わないよう [enableWheel]="false" を指定してください。

次のステップ