余白

https://blog.lacolaco.net/ に移転しました

Angular CDK drag-and-drop の紹介

こんにちは。

この記事ではAngular CDKの次期アップデートで提供される、 drag-and-drop 機能を紹介します。 執筆時点ではまだnpmパッケージとして公開されていないので、一般に利用できるまでにはもうしばらくかかりますが、 もし早く使いたい方は、次のコマンドで開発版ビルドをインストールしましょう。 なお、開発版ビルドですので自己責任でお願いします。

$ yarn add angular/cdk-builds

CDK drag-and-drop

drag-and-dropはその名のとおり、UI上でのドラッグアンドドロップ操作をサポートするものです。

@angular/cdk/drag-drop パッケージから提供される DragDropModule をインポートすると、次の2つのディレクティブ、コンポーネントが利用できます。

cdkDrag ディレクティブ

cdkDragディレクティブは、ドラッグされる要素を指定するディレクティブです。このディレクティブを付けられた要素は画面上で自由に位置を変えられます。

たとえば ng new 直後のテンプレートHTMLで、 li要素に cdkDragディレクティブを付与すると、次のようになります。 (わかりやすさのために li要素にCSSでスタイルを付与しています)

<ul>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
  </li>
</ul>

f:id:lacolaco:20180829200010g:plain

cdkDrag ディレクティブだけを使うと、何の制約もなく自由に移動することができました。

cdk-dropコンポーネント

このままでは動いて面白い以上の意味がないので、cdk-dropコンポーネントを使います。 cdk-dropコンポーネントは、cdkDragディレクティブをグルーピングし、動きに制限をつけて、限られた領域内でだけ移動できるようにします。

たとえば、ul要素の外側に <cdk-drop> コンポーネントを配置すると、ulの内部でだけ移動できるようになり、移動中は並び替えが行われるようになります。

<cdk-drop>
<ul>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
  </li>
</ul>
</cdk-drop>

f:id:lacolaco:20180829200735g:plain

見てのとおり、 <cdk-drop>タグの内側でだけ並べ替えが行われるようになりましたが、ドロップしてしまうともとの状態に戻ります。 これはcdk-dropコンポーネントの仕様で、ドラッグアンドドロップが終了すると、その内部のcdkDragの順序は復元されます。 ただし、ドラッグアンドドロップ終了時にはdroppedイベントが発行されていて、このイベントをもとにコンポーネント側からデータモデルを更新することで、 ドラッグアンドドロップによる並べ替えを実現できます。

並べ替え

並べ替えをおこなうためには、リストをコンポーネント側で管理する必要があります。これまでは適当なli要素を使っていましたが、AppComponentに次のようなlistプロパティをもたせます。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  list = ['まぐろ', 'サーモン', 'えび'];
}

そしてテンプレートを次のように変更し、listプロパティの要素を繰り返し表示します。先程までと同じように、<cdk-drop>タグのなかで繰り返される並べ替えの対象にcdkDragディレクティブを付与します。

<h2>好きなネタ</h2>
<cdk-drop [data]="list" (dropped)="drop($event)">
  <ul>
    <li *ngFor="let item of list" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

ポイントは <cdk-drop [data]="list" (dropped)="drop($event)"> です。[data]プロパティには並べ替えの対象となるデータモデルを渡します。 次に、(dropped)="drop($event)"では、droppedイベントハンドラdropメソッドを呼び出しています。 dropメソッドは次のように記述します。

import {
  CdkDragDrop,
  moveItemInArray,
} from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  list = ['まぐろ', 'サーモン', 'えび'];

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
}

CdkDragDrop<string[]>は、dropイベントの引数の型です。ジェネリックstring[]は並べ替え対象の配列の型を表しています。

moveItemInArray関数は、基本的な配列の並べ替えを行ってくれるCDKの機能です。中身は単なるJavaScriptの配列の並べ替えですが、Angularチームによる実装にまかせておくのが安心だと思います。

export function moveItemInArray<T = any>(array: T[], fromIndex: number, toIndex: number): void {
  const from = clamp(fromIndex, array.length - 1);
  const to = clamp(toIndex, array.length - 1);

  if (from === to) {
    return;
  }

  const target = array[from];
  const delta = to < from ? -1 : 1;

  for (let i = from; i !== to; i += delta) {
    array[i] = array[i + delta];
  }

  array[to] = target;
}

f:id:lacolaco:20180829210528g:plain

これで、dropイベントによって配列を並べ替えられるようになりました。

複数のcdk-dropでグルーピングをおこなう

複数のグループを跨いだ並べ替えも可能です。先程のAppComponentを次のように変更します。listプロパティをlikeプロパティに改名し、新しくunlikeプロパティを追加します。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  like = ['まぐろ', 'サーモン', 'えび'];
  unlike = ['数の子', 'たくあん'];

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
}

テンプレートでは、likeプロパティとunlikeプロパティの両方で同じようにcdk-dropによる並べ替えができるようにします。

<h2>好きなネタ</h2>
<cdk-drop [data]="like" (dropped)="drop($event)">
  <ul>
    <li *ngFor="let item of like" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

<h2>好きじゃないネタ</h2>
<cdk-drop [data]="unlike" (dropped)="drop($event)">
  <ul>
    <li *ngFor="let item of unlike" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

ここまでは先程と変わりません。ここから、この2つのグループを結合します。 並べ替えグループを結合するには、cdk-dropconnectToプロパティを使います。このプロパティに結合の対象となるグループの参照を渡します。

<h2>好きなネタ</h2>
<cdk-drop #dropLike [data]="like" (dropped)="drop($event)" [connectedTo]="[dropUnlike]">
  <ul>
    <li *ngFor="let item of like" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

<h2>好きじゃないネタ</h2>
<cdk-drop #dropUnlike [data]="unlike" (dropped)="drop($event)" [connectedTo]="[dropLike]">
  <ul>
    <li *ngFor="let item of unlike" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

さらに、AppComponentのdropメソッドで、グループを跨いでいた場合の処理を追加します。 この場合も汎用的なグループ移動機能をサポートするtransferArrayItem関数が提供されているので、それを使います。

グループを跨いだ移動かどうかは event.previousContainerevent.container を比較して判定できます。 次のように書けば、一致する場合は配列内での移動を、一致しない場合はグループを越えた移動をおこないます。

  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    }
  }

f:id:lacolaco:20180829212028g:plain

これで複数のグループを跨いだドラッグアンドドロップによる並べ替えができるようになりました。

CSSによるスタイリング

最後に、CDKのdrag-dropが提供するスタイリングのためのCSSクラスを紹介します。

.cdk-drag.placeholder

.cdk-drag.placeholderクラスは、ドラッグされている要素のプレースホルダ部分につけられるCSSクラスです。たとえばここを次のように見えなくすることで自然な挿入を演出できます。

.cdk-drag-placeholder {
  opacity: 0;
}

f:id:lacolaco:20180829212513g:plain

.cdk-drag-preview

.cdk-drag-previewクラスは、ドラッグされている要素のプレビュー部分(動かしている部分)につけられるCSSクラスです。たとえば次のように半透明にすることで自然な挿入を演出できます。

.cdk-drag-preview {
  box-sizing: border-box;
  opacity: 0.5;
}

f:id:lacolaco:20180829212904g:plain

この他にもいくつかCSSクラスがあります。詳しくはスタイリングに関するドキュメントを参照してください。

まとめ

CDKのアップデートはAngular v7のリリースと合わせておこなわれるだろうと見られています。 楽しみに待ちましょう。