【Angular】Angular CDK のDrag and Drop使ってみる part2

前回までのあらすじ

前回の記事でAngular CDK の Drag And Drop を使ってみたサンプルを作成しました。

ただ、この記事でやっているのは埋め込んだ静的なタスク群を表示し、Drag And Drop しているだけなので、これではTrelloっぽいとは言えないですよね、、、

動的に列追加できるようにしてみた

ということで、動的に状態の追加ができるように修正してみました!!
こんな感じで動作します。

画質粗くてすいません、、、
なんとなく動的に動いてそうだな、という所をくみ取っていただければ幸いです。
ソースは前回の続きからになります。
最終結果だけ知りたいよって方はこちらにソース上げていますので参考にしてみてください。
Masterブランチが前回までで、Developブランチが今回のソースになります。
計画性なさ過ぎてブランチ名雑ですが気にしないでください。

前回雑に作った部分を整えておく

前回はとりあえずざっくり作ってしまったのでいったんいろいろと整えます。
まずはtask.tsをもう少しきれいに作っておきます。

export class Task {
  id: number;
  title: string;
  description: string;
  constructor(id?: number, title?: string, description?: string) {
    this.id = id ? id : 0;
    this.title = title ? title : '';
    this.description = description ? description : '';
  }
}

export class TaskColumn {
  state: string;
  id: number;
  taskList: Task[];
  constructor(state?: string, id?: number, taskList?: Task[]) {
    this.state = state ? state : '';
    this.id = id ? id : 0;
    this.taskList = taskList ? taskList : [];
  }
}

export class TaskBoard {
  columnList: TaskColumn[];
  constructor(columnList?: TaskColumn[]) {
    this.columnList = columnList ? columnList : [];
  }
}

特に、前回はTaskColumnidstringで作っていた(ドラッグアンドドロップ用のIDとしstring配列が必要だったから)のですが、単にtoString()してあげればいいのでidnumberに修正しました。
後は今回必要にはならないけどとりあえずコンストラクタ用意しておきました。
idstringからnumberになったので、影響箇所修正します。
task-board.component.ts の静的に作成している board 変数の中身を修正します。
idstringで用意されている部分をnumberに変更するだけです。
後は同じ task-board.component.ts ngOnInit()を修正します。

  ngOnInit() {
    for (const column of this.board.columnList) {
      this.connectedTo.push(column.id.toString());
    }
  }

影響箇所はこのくらいだと思います。

タスク追加用の処理作成

前回タスク編集用として、task-item-edit-dialogを作成しましたが、これをそのまま活用して、タスク追加できるようにします。
まずはtask-board.component.tsからdialogを呼び出せるようにimportを追加して、コンストラクタを修正しておきます。

import { MatDialog } from '@angular/material/dialog';
import { TaskItemEditDialogComponent } from '../task-item-edit-dialog/task-item-edit-dialog.component';
・・・
constructor(private dialog: MatDialog) { }
・・・

そしたら、以下のメソッドを追加します。

  addTask(column: TaskColumn) {
    const dialogRef = this.dialog.open(TaskItemEditDialogComponent, {
      data: new Task(),
      disableClose: true
    });
    dialogRef.afterClosed()
      .subscribe(result => {
      if (result) {
        result.id = column.taskList.length + 1;
        column.taskList.push(result);
      }
    });
  }

次に、これを呼び出すためにtask-board.component.htmlにボタンを追加します。

<div class="container" *ngFor="let column of board.columnList">
  <p>{{ column.state }}</p>
  <div
    cdkDropList
    id="{{column.id}}"
    class="list"
    [cdkDropListData]="column.taskList"
    [cdkDropListConnectedTo]="connectedTo"
    (cdkDropListDropped)="drop($event)">
    <div class="box" *ngFor="let task of column.taskList" cdkDrag>
      <app-task-item [item]="task"></app-task-item>
    </div>
    <!-- ここから -->
    <div class="box">
      <button mat-button (click)="addTask(column)">
        +
      </button>
    </div>
    <!-- ここまでが追加 -->
  </div>
</div>

これを作ってるときに気づいたんですが、以前作成したtask-item-edit-dialog側に無駄な処理があったので、これを修正します。

import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Task } from '../models/task';

@Component({
  selector: 'app-task-item-edit-dialog',
  templateUrl: './task-item-edit-dialog.component.html',
  styleUrls: ['./task-item-edit-dialog.component.scss']
})
export class TaskItemEditDialogComponent implements OnInit {
  constructor(
    private dialogRef: MatDialogRef<TaskItemEditDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data?: Task
  ) { }

  ngOnInit() {}

  apply(): void {
    this.dialogRef.close(this.data);
  }

  cancel(): void {
    this.dialogRef.close();
  }
}

以前はキャンセルがクリックされたときに元のデータに戻すために、initItemという変数用意して、初期状態に戻すように書いていたのですが、そもそもデータを渡さなければいい話だったのでそれにまつわる処理を消しています。

これでとりあえず同じ列内にはタスクが追加できるようになりました。
後は新しい列が追加できるようにします。

列追加用のダイアログ作成

MatDialogModuleを使って列追加用のダイアログを作ります。

ng g c task-column-edit-dialog

componentを作成したら、それを呼び出せるようにapp.module.tsへと追加しておきます。

・・・
import { TaskColumnEditDialogComponent } from './task-column-edit-dialog/task-column-edit-dialog.component';

@NgModule({
  declarations: [
    AppComponent,
    TaskBoardComponent,
    TaskItemComponent,
    TaskItemEditDialogComponent,
    TaskColumnEditDailogComponent
    TaskColumnEditDialogComponent
  ],
・・・

次に、task-column-edit-dialog.component.htmlを以下のように編集します。

<div>
  <p>
    状態:<input [(ngModel)]="data.state">
  </p>
</div>

<div>
  <button mat-stroked-button (click)="cancel()">キャンセル</button>
  <button mat-flat-button color="primary" (click)="apply()">適用</button>
</div>

これはタスク追加用のダイアログとほぼ一緒ですね。
task-column-edit-dialog.component.ts側も修正します。

import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { TaskColumn } from '../models/task';

@Component({
  selector: 'app-task-column-edit-dialog',
  templateUrl: './task-column-edit-dialog.component.html',
  styleUrls: ['./task-column-edit-dialog.component.scss']
})
export class TaskColumnEditDialogComponent implements OnInit {
  constructor(
    private dialogRef: MatDialogRef<TaskColumnEditDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data?: TaskColumn
  ) { }

  ngOnInit() { }

  apply(): void {
    this.dialogRef.close(this.data);
  }

  cancel(): void {
    this.dialogRef.close();
  }
}

後は、task-board.component.tsから呼び出せるようにします。
task-board.component.tsを以下のように修正します。

・・・
// importの追加
import { TaskColumnEditDialogComponent } from '../task-column-edit-dialog/task-column-edit-dialog.component';
・・・
// メソッドの追加
  addColumn() {
    this.board.columnList.push(new TaskColumn());
    const dialogRef = this.dialog.open(TaskColumnEditDialogComponent, {
      data: new TaskColumn(),
      disableClose: true
    });
    dialogRef.afterClosed()
      .subscribe(result => {
      if (result) {
        result.id = this.board.columnList.length + 1;
        this.connectedTo.push(result.id.toString());
        this.board.columnList.push(result);
      }
    });
  }
・・・

task-board.component.html に呼び出し用のボタンを追加します。

<div class="container" *ngFor="let column of board.columnList">
  <p>{{ column.state }}</p>
  <div
    cdkDropList
    id="{{column.id}}"
    class="list"
    [cdkDropListData]="column.taskList"
    [cdkDropListConnectedTo]="connectedTo"
    (cdkDropListDropped)="drop($event)">
    <div class="box" *ngFor="let task of column.taskList" cdkDrag>
      <app-task-item [item]="task"></app-task-item>
    </div>
    <div class="box">
      <button mat-button (click)="addTask(column)">
        +
      </button>
    </div>
  </div>
</div>
<!-- ここから -->
<button mat-button (click)="addColumn()">
  +
</button>
<!-- ここまでが追加 -->

これでOKなはずです!
前回に比べて動的な編集ができるようになったため、よりTrelloっぽくなったのではないでしょうか!笑


上記のソースはGitHubにも上げているので、良かったら参考にしてみてください!
https://github.com/kanazawanao/drag-and-drop-sample/tree/develop

一応 StackBlitz でも確認ができますが、ダイアログ用のコンポーネントを entryComponents にコンポーネントを追加する必要があるみたいです、、、
app.module.tsをこんな感じにしていただければ確認できるかと思います。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { DragDropModule } from '@angular/cdk/drag-drop';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TaskBoardComponent } from './task-board/task-board.component';
import { TaskItemComponent } from './task-item/task-item.component';
import { TaskItemEditDialogComponent } from './task-item-edit-dialog/task-item-edit-dialog.component';
import { TaskColumnEditDialogComponent } from './task-column-edit-dialog/task-column-edit-dialog.component';

@NgModule({
  declarations: [
    AppComponent,
    TaskBoardComponent,
    TaskItemComponent,
    TaskItemEditDialogComponent,
    TaskColumnEditDialogComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    NoopAnimationsModule,
    DragDropModule,
    MatButtonModule,
    MatDialogModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents:[TaskItemEditDialogComponent, TaskColumnEditDialogComponent]
})
export class AppModule { }

ちょっと手動での修正が必要ですが、簡単に見たいよって方はこちら参考にしてみてください。
https://stackblitz.com/github/kanazawanao/drag-and-drop-sample/tree/develop