@angular/cdkのDragDropModuleに感謝した話。


この記事は Angular Advent Calendar 2019 18日目の記事です。
こんにちは、しみきょんです!

今日は@angular/cdkの DragDropModule の話をします。

Ionic / Angularでアプリケーションを作っている際にDraggableなリストコンポネントを作りたくて、Ionicコンポネントや他ライブラリを探して模索していたのですがいまいちはまらず、、
ふと我に返った時に、Angular Material使えばいいじゃん!!となり DragDropModule の恩恵をうけた感謝の話です。

環境

  • @ionic/angular v4.9.0
  • @angular/core v8.1.3
  • @angular/cdk v8.2.3
  • @angular/material v8.2.3
  • ngrx v8.3.0
  • rxjs v6.5.3
  • typescript v3.4.5

やりたかったこと

  • ドラッガブルリストを作りたい
  • リストカラムの数は動的に変えたい
  • Auto scrollが欲しい
  • ステータスによってドラッガブル要素をdisableにしたい
  • リストカラムやドラッガブル要素に特定の値をもたせたい

結論

  • ドラッガブルリストを作りたい → CdkDropList
  • リストカラムは動的に変えたい → CdkDropListGroup
  • Auto scrollが欲しい → CdkDropListcdkDropListAutoScrollDisabled
  • ステータスによってドラッガブル要素をdisableにしたい → CdkDragcdkDragDisabled
  • リストカラムやドラッガブル要素に特定の値をもたせたい → CdkDropListcdkDropListData, CdkDragcdkDragData

DragDropModuleで全てがかないました。
ちなみに、ドラッガブル要素はカード形式にしたかったのでIonic componentにあるion cardを使いました。
実装イメージはこんなでしたが、本家のドキュメントを見れば動作確認もできるし、よりわかりやすいと思います。

<div cdkDropListGroup>
  <div *ngFor="let status of statuses">
    <h2>{{ status.title }}</h2>
    <div cdkDropList (cdkDropListDropped)="drop($event)" [cdkDropListData]="status.key">
      <ion-card cdkDrag *ngFor="let item of items[status.key]"
                [cdkDragDisabled]="status.disable"
                [cdkDragData]="item.id">
        ...
      </ion-card>
    </div>
  </div>
</div>

特にAuto scrollを自作しようか迷っていたので、DragDropModuleで勝手にやってくれて大変助かりました。
そんなAuto scrollをどう実現しているかを、のぞいてみました。

DragDropModuleのAuto scroll

ドラッギング時のAuto scrollの制御はDropListRef._startScrollingIfNecessaryでやっています。
端的にこんなことをやっているようです。
- ドラッギング対象の座標を引数にとる(pointerX pointerY
- 座標を元に、ドラッカブル領域にスクロール可能な方向があるか判定する
- スクロール可能な方向と、実際にスクロールしようとしている方向を比較し、動作可能かを判断(_verticalScrollDirection, _horizontalScrollDirection)
- スクロール可能であった場合は2pxずつスクロールしていく
- スクロール不可であった場合はすでにあるAuto scrollのsubscriptionを破棄、または何もしない

改めて実装を見ると、座標計算やドラッガブル領域の判定など、自分でやったら絶対大変的な部分を任せられるのはありがたいなあ、、DragDropModule感謝だなあ、、、とひしひしと感じました。

さらに個人的に勉強になったのが実際にAuto scrollを動かしている実装です。rxjsのintervalメソッドの引数にSchedulerを指定した実装をしたことなかったので勉強になりました。

angular/componentsの実装から引用
/** Starts the interval that'll auto-scroll the element. */
  private _startScrollInterval = () => {
    this._stopScrolling();

    interval(0, animationFrameScheduler)
      .pipe(takeUntil(this._stopScrollTimers))
      .subscribe(() => {
        const node = this._scrollNode;

        if (this._verticalScrollDirection === AutoScrollVerticalDirection.UP) {
          incrementVerticalScroll(node, -AUTO_SCROLL_STEP);
        } else if (this._verticalScrollDirection === AutoScrollVerticalDirection.DOWN) {
          incrementVerticalScroll(node, AUTO_SCROLL_STEP);
        }

        if (this._horizontalScrollDirection === AutoScrollHorizontalDirection.LEFT) {
          incrementHorizontalScroll(node, -AUTO_SCROLL_STEP);
        } else if (this._horizontalScrollDirection === AutoScrollHorizontalDirection.RIGHT) {
          incrementHorizontalScroll(node, AUTO_SCROLL_STEP);
        }
      });
  }

animationFrameScheduler を指定することにより、ブラウザコンテンツが再描画される直前にタスクがスケジュールされ、スムーズなブラウザアニメーションが実現できているようです。
自作でアニメーション実装したい時に参考にできそうです。

まとめ

今回は、DragDropModule使ってみてよかった話と、その中身をのぞいてみました。
cdkのようなライブラリがあると、自分でやりたいことと、外に任せたいことが切り分けられて楽だなと改めて感じました。
また、本家の実装は実際に自分のソースに活かせる部分がたくさんあるので、よむと楽しいです。

ちなみに、実際に使用しているのは@angular/cdk v8.2.3でしたが、今回みたDragDropModule周りの実装はmasterブランチのものです。
Angular v9のアプデートの際にいくつか修正が入るようでした

最後まで読んでくださりありがとうございます!
明日は @pittanko_ptaさんです!