Angular7 drag and dropを使ってToDoアプリを作る


## はじめに 
Angular7のdrag and dropを触ってみたかったため、とりあえずToDoアプリで試してみました。drag and dropはCDKにて提供されおり、そちらを使用します。[公式ドキュメント:https://material.angular.io/](https://material.angular.io/)

## 動作環境
- Angular7   
- Angular6以前の場合はupdateしてください

## この記事の内容
1. プロジェクトの作成
2. 基本構成
3. ToDo機能の実装
4. まとめ
5. 参考資料

## プロジェクトの作成
Angular CLIでプロジェクトを作成します。
今回は最小限構成で良いのでオプションにminimalをつけます。
またアプリ名は`todo-app`とします。

```
ng new todo-app --minimal
```

### Angular Material  CDK & Animationのインストール

```
npm install --save @angular/material @angular/cdk @angular/animations
```


### ng addでAngularMaterialのセットアップ
`ng add <packge>`でパケージを指定し、Schematics機能でテンプレートの展開を行います。

```
ng add @angular/material
```

とりあえず起動確認 

```
ng serve -o
```

お決まりの画面が表示されれば成功
<img width="1280" alt="スクリーンショット 2018-12-09 21.30.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/320055/ee86f115-178a-9c9b-d287-e7e439836677.png">



## 基本構成
今回は小規模なアプリのため、`app.component.ts`と`app.component.ts`に処理を直書きします。そのため、`app.component.ts`に`OnInit`を追加します。

```app.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor() { }
  ngOnInit() {
  }
}

```

## ToDo機能の実装
`app.module.ts`に`material`モジュールをインポート
ついでにToDoの入力のために`ReactiveFormsModule`もインポートしておきます。

``` app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

import { ReactiveFormsModule } from '@angular/forms'; // 入力フォーム用のモジュール

/* 今回使うmaterialのモジュール */
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // 追加
import { MatButtonModule } from '@angular/material'; // 追加
import { MatInputModule } from '@angular/material/input'; // 追加
import { DragDropModule } from '@angular/cdk/drag-drop'; // 追加
import { MatCardModule } from '@angular/material/card'; // 追加

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatInputModule,
    ReactiveFormsModule,
    MatCardModule,
    DragDropModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

```

### todoリストの入力欄を作成します

`app.component.html`内を下記のソースコードに置き換えます。

```app.component.html
<div>
  <form class="example-form" [formGroup]="myForm">
    <mat-form-field class="example-full-width">
      <input formControlName="title" matInput placeholder="title">
    </mat-form-field>

    <mat-form-field class="example-full-width">
      <textarea formControlName="description" matInput placeholder="description"></textarea>
    </mat-form-field>
    <button mat-raised-button color="primary">登録</button>
  </form>
</div>
```

`app.component.ts`に入力フォームの処理を記述します。

```app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(private formBuilder: FormBuilder, ) { }
  myForm: FormGroup;
  todo = [];
  done = [];
  ngOnInit() {
    this.myForm = this.formBuilder.group({
      title: ['', [Validators.required]],
      description: ['', [Validators.required]]
    });
  }
  taskRegist() {
    console.log(this.myForm.value);
    this.todo.push({
      title: this.myForm.controls.title.value,
      description: this.myForm.controls.description.value,
    });
  }
}

```

ここまでの結果をアプリを起動し確認します。

```
ng serve -o
```
<img width="316" alt="スクリーンショット 2018-12-10 21.27.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/320055/f0ed477e-34fc-f856-7898-7304e0091756.png">
これで入力欄はできました。
ここまで完了したら次はDrag and Dropで入力欄を自由に動かせるようにします。


### Drag and Dropを使用し入力フォームを自由に動かせるようにする
先程作成した入力フォームを`cdkDrag`ディレクティブが付加された`div`で囲みます。これだけで、入力フォームが画面内を自由に動かせるようになります。

```app.component.html
<div class="input-box" cdkDrag> <!-- ←追加部分 -->
    <form class="input-form" [formGroup]="myForm">
      <mat-form-field class="example-full-width">
        <input formControlName="title" matInput placeholder="タイトル">
      </mat-form-field>

      <mat-form-field class="example-full-width">
        <textarea formControlName="description" matInput placeholder="説明を入力"></textarea>
      </mat-form-field>
      <button mat-raised-button color="primary" (click)="taskRegist()" [disabled]="myForm.invalid">登録</button>
    </form>
</div> <!-- ←追加部分 -->
```

- ドラッグを可能にするには`cdkDrag`ディレクティブをタグに追加します。これだけでドラッグすることができます。
- `cdkDrag`ディレクティブのみ付加した場合はページ内を自由に動かすことができます。


デザインを整えたら実行します。
cssは[Angular Materialの公式ドキュメント](https://material.angular.io/cdk/drag-drop/overview)を参考にしました。

```app.component.css
.input-form {
    min-width: 300px;
    max-width: 300px;
    width: 90%;
  }
  
.example-full-width {
    width: 90%;
}
.input-box {
    width: 300px;
    height: 300px;
    border: solid 1px #ccc;
    color: rgba(0, 0, 0, 0.87);
    cursor: move;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
    background: #fff;
    border-radius: 4px;
    position: relative;
    z-index: 1;
    transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
    box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
                0 2px 2px 0 rgba(0, 0, 0, 0.14),
                0 1px 5px 0 rgba(0, 0, 0, 0.12);
  }
```

アプリを起動し確認 

```
ng serve -o
```

入力フォームがドラッグできるのを確認できると思います。


![drag.gif](https://qiita-image-store.s3.amazonaws.com/0/320055/327c9f73-1422-837f-0dc8-09ca199c26b7.gif)







### todoリストを作成する
先ほど作成した入力フォームの下に追加します

```app.component.html
<div class="example-container">
  <h2>やること</h2>

  <div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList]" class="example-list"
    (cdkDropListDropped)="drop($event)">
    <div class="example-box" [cdkDragData]="item" *ngFor="let item of todo" cdkDrag>
      {{item.title}}
      <div *cdkDragPreview>
        <mat-card class="example-card">
          <mat-card-content>
            <div>{{item.title}}</div>
            <hr>
            <p>{{item.description}}</p>
          </mat-card-content>
        </mat-card>
      </div>
    </div>
  </div>
</div>

<div class="example-container">
  <h2>完了</h2>
  <div cdkDropList #doneList="cdkDropList" [cdkDropListData]="done" [cdkDropListConnectedTo]="[todoList]" class="example-list"
    (cdkDropListDropped)="drop($event)">
    <div class="example-box" [cdkDragData]="item" *ngFor="let item of done" cdkDrag>{{item.title}}</div>
  </div>
</div>
```

入力フォームは画面内を自由に動かせるようにしましたが、ToDoリストは整列させたいです。そこで`cdkDropList`を使用します。
`cdkDropList`はドロップする場所を制限することができます。これにより`cdkDropList`内の要素は自動で整列されます。

```
#<id名>="cdkDropList"
```

今回は他にも`cdkDropListData`や`cdkDropListDropped`、`cdkDropListConnectedTo`を使用しています。

- **cdkDragData　、cdkDropListData**   
`cdkDragData`に値を設定することで要素にデータをもたせることができます。
今回のアプリの場合はdropイベントの`event.item.data`でデータにアクセスできます。
またデータを指定することで、ドロップ操作の発生元を特定するのに役立ちます。

- **cdkDropListDropped**
ユーザーがドラッグを終了するとイベントが発生しデータモデルを更新します。またドラッグ中はデータモデルを更新しません。

- **cdkDropListConnectedTo**
`[cdkDropListConnectedTo]="[id名]"`でドロップ先のコンテナを設定できます。ドロップ先を制限したいときに使用する感じですね。
また`cdkDropListGroup`配下で`cdkDropListConnectedTo`を設定しない場合は好きなコンテナにドロップすることができます。

- **cdkDragPreview**
`cdkDragPreview`を付加することでドラッグ時のプレビューを作成することができます。






app.component.tsにメソッドを追加します

```app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; //　<------------追加
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(private formBuilder: FormBuilder, ) { }
  todo = [];
  done = [];
  myForm: FormGroup;

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      title: ['', [Validators.required]],
      description: ['', [Validators.required]]
    });
  }
/*--------------以下追加分----------------------*/
/*
 * ドロップしたcdkDropListが同じなら場所を入れ替えます
 * cdkDropListが違う場合はアイテムを移します
 */
  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);
    }
  }
/*--------------------------------------*/　

/*タスク登録用のメソッド*/
  taskRegist() {
    console.log(this.myForm.value);
    this.todo.push({
      title: this.myForm.controls.title.value,
      description: this.myForm.controls.description.value,
    });
  }

}

```

- **moveItemInArray**
同じコンテナ内で要素の場所を入れ替えるメソッドです

- **transferArrayItem**
ターゲットのコンテナに要素を移動するメソッドです

- **copyArrayItem**
今回は使用していませんが、ターゲットのコンテナに要素をコピーするメソッドもあるそうです


ここまでで、機能の実装は完了しました。あとはレイアウトを整えます。
先程作成したcssに追記します。

```app.component.css
.example-card {
  max-width: 400px;
}
.example-container {
    width: 400px;
    max-width: 100%;
    margin: 0 25px 25px 0;
    display: inline-block;
    vertical-align: top;
  }
  
  .example-list {
    border: solid 1px #ccc;
    min-height: 60px;
    background: white;
    border-radius: 4px;
    overflow: hidden;
    display: block;
  }
  
  .example-box {
    padding: 20px 10px;
    border-bottom: solid 1px #ccc;
    color: rgba(0, 0, 0, 0.87);
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    box-sizing: border-box;
    cursor: move;
    background: white;
    font-size: 14px;
  }
  
  .cdk-drag-preview {
    box-sizing: border-box;
    border-radius: 4px;
    box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
                0 8px 10px 1px rgba(0, 0, 0, 0.14),
                0 3px 14px 2px rgba(0, 0, 0, 0.12);
  }
  
  .cdk-drag-placeholder {
    opacity: 0;
  }
  
  .cdk-drag-animating {
    transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
  }
  
  .example-box:last-child {
    border: none;
  }
  
  .example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
    transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
  }
```


アプリを起動し確認 

```
ng serve -o
```


![マイムービー.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/320055/5e0b033e-cbea-4589-8a03-540578413eeb.gif)



## まとめ
思ったより簡単にdrag and dropの機能を実装でき驚きました。
Anguler７へのバージョンアップで他にも機能が追加されているので、そちらも触ってみたいと思います。
最後まで読んでいただき、ありがとうございました。

## 参考文献
[Angular Material](https://material.angular.io/)