#はじめに
こちらは、AdventCalender2020の9日目の記事です。
Angularを触り始めて半年、というか初のJSフレームワークがAngularの私です。
おそらく多くの方が、フレームワークになれるため最初はTODOアプリを作ったりするのではないでしょうか? そしてDrag&Drop機能(以下D&D)も付けたくなったりするのでは無いでしょうか?
ご存知の通りD&DはMaterialに存在していて、それを確認した私は「なら、そこまで難しくないな」と高を括って、案の定ドハマりしたわけなので、この気にDrag&Dropを通じでAngularの基本的な解説と、RestAPIまで使ったToDoアプリの紹介をしようと思います。
なので今回は初心者向けなAdventCalenderとなりますこと、ご了承いただけたら幸いです。
これはhttpclientを利用して、ドロップ時にサーバー側のデータを更新するようにしています。
が、そこまで行くのに色々と躓いたので例を上げながら解説していきます。これからの方にとって何かのお役に立てたら幸いです。
#ハマり1「JSONデータから配列が作れない」
書くのを迷いましたが、初歩的なところも書こうと思います。
そもそものJavaScript知識が無いとここで躓きます。
- まず先にJavaScripを押さえましょう
- テーブル設計と分類用の項目を考えておきましょう
真面目に、「そんなとこから?」ってところから始めている人間だったので、簡単なことでもネットでどう検索したらいいかもわからず、質問の仕方もわからなかったりして、勉強したての頃ほど簡単なことの壁がプレッシャーにすら感じたりするもんです。それ故にはじめの段階がクリアできなくて挫折したり、その言語やフレームワークを嫌いになったりするわけで。
で、ここで例を上げると、サーバーサイドの設定は割愛しますが例えば
[
{
"id": 1,
"name": "犬の散歩をする",
"status": "4",
},
{
"id": 2,
"name": "理科の宿題をする",
"status": "2",
},
{
"id": 3,
"name": "Angularを11にアップデートする",
"status": "4",
},
---略---
]
このようなJSONデータを取得できるとして、以下のようにデータ取得時にfilterメソッドなどを用いてstatusの値ごとに分類していきましょう。(D&D機能をつかう下準備)
ここでは値が1=計画、2=着手、3=待機、4=完了、5=資料、0=非表示と意図しており、plan,going,wait,done,doc,dropに分けてます。
import { Component, OnInit } from '@angular/core';
import { EventService } from '../event.service'; //httpclient設定済
import { ToDo } from '../modeltypes'; //型指定用
import { Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.css']
})
export class MainViewComponent implements OnInit {
// JSONデータ受取用
toDos: ToDo[] = [];
// D&D用リスト
plan: ToDo[] = [];
going: ToDo[] = [];
wait: ToDo[] = [];
done: ToDo[] = [];
doc: ToDo[] = [];
drop: ToDo[] = [];
todoNum: number;
constructor(
private _eventService: EventService,
private _router: Router,
) { }
ngOnInit(): void {
// ToDo情報を取得&ステータス別に分類してリスト化
this._eventService.getTodo().subscribe(
(res: ToDo[]) => {
this.toDos = res;
this.drop = this.toDos.filter(data => data.status == 0);
this.plan = this.toDos.filter(data => data.status == 1);
this.going = this.toDos.filter(data => data.status == 2);
this.wait = this.toDos.filter(data => data.status == 3);
this.done = this.toDos.filter(data => data.status == 4);
this.doc = this.toDos.filter(data => data.status == 5);
this.todoNum = this.toDos.length - this.todoDrop.length;
},
err => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this._router.navigate(['/login']);
}
}
}
);
}
}
html側はこうしました。[class.common-add-scrollbar]="plan.length > 5"
はTodoのカードが5より多くなったらCSS側でスクロールバーを付与するために書いたものです。
また、todo-cardコンポーネントを作って、そこに@input()でデータを渡しています。
<div class="container">
<div class="row m-0 px-0" cdkDropListGroup>
<!-- planリスト -->
<div class="card-container">
<h2 class="state-planning py-1 mb-2">
計画({{plan.length}})
</h2>
<!-- planカード -->
<div class="list-planning" [class.common-add-scrollbar]="plan.length > 5"
id="Plan" cdkDropList [cdkDropListData]="plan" (cdkDropListDropped)="drop($event)">
<div *ngFor="let todo of plan" cdkDrag>
<app-todo-card [task]="todo"></app-todo-card>
</div>
</div>
</div>
<!-- goingリスト -->
<div class="todo-card-container">
<h2 class="state-sign state-going py-1 mb-2">
着手({{going.length}})
</h2>
<!-- goingカード -->
<div class="list-going" [class.common-add-scrollbar]="going.length > 5"
id="Going" cdkDropList [cdkDropListData]="going"
(cdkDropListDropped)="drop($event)">
<div *ngFor="let todo of going" cdkDrag>
<app-todo-card [task]="todo">
</app-todo-card>
</div>
</div>
</div>
<!-- wait,done,docもほとんど同じ -->
<!-- 右エリア -->
<div class="todo-card-container2">
<div class="row d-flex">
<div class="button-container md-12">
<div matRipple class="create-button">
<span>+新規</span>
</div>
<div matRipple class="clear-button">
<span class="clear">クリア</span>
</div>
<!-- dropエリア -->
<div class="drop-area">
<div class="trashbox">
<div class="area-info"
id="Drop" cdkDropList [cdkDropListData]="drop"
(cdkDropListDropped)="drop($event)">
<h3>Drop</h3>
<div>here</div>
</div>
<mat-icon>delete</mat-icon>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
原因
[cdkDropListData]="hoge" の記述エラー(もしくは書き忘れ)
きちんとtsファイルを確認してデータバインディングさせましょう。なくてもエラーが出ないからといってcdkDropListDataの記述を消してはいけません。
ここでコンテナとデータの紐付けをするので、以降のドロップイベントでデータが反映されなくなり、ドロップが完了しません。
#ハマり3「ドロップ時のstatus値を変更する方法」
ハマったというほどではないのですが、ドロップ時にstatusの値を変更したかったので、さてどうしたものかと考えました。
実際、ドロップできてもサーバー側のデータを更新していないと、カードは常に同じ位置に戻ります。
フロントとしてはサーバーサイドと連携して初めて意味があると思っているので、httpclientまで見越したToDoアプリまでできたら、Angular初学者は抜けたと思って良いのではないでしょうか。(と初学者レベルが言ってみるw)
もっといい方法があるかもしれませんが、以下のように解決できます。
drop(event: CdkDragDrop<string[]>) {
let todoItem; //追加
//Marterialのコードと同じ
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);
//ここまでMarterialのコードと同じ
//以下追加
//drop時に出されるeventを利用して、移動させたカード情報を取得する
//余談だが、console.log(event)で格納されているデータを見ても面白い
todoItem = event.container.data[event.currentIndex];
//カード情報はドロップされた場所情報をevent.container.element.nativeElement.idとして持つので
//main.htmlの各コンテナにid="Plan"など付けておくのがミソ
//しかしstatusの値は以前のコンテナに対応した値のままなので
//以下のように移動先のidで分類して、その際にJSONで持たせていたstatus値を更新する
switch (event.container.element.nativeElement.id) {
case 'Plan':
todoItem.status = "1";
break;
case 'Going':
todoItem.status = "2";
break;
case 'Wait':
todoItem.status = "3";
break;
case 'Done':
todoItem.status = "4";
break;
case 'Document':
todoItem.status = "5";
break;
case 'Drop':
todoItem.status = "0";
break;
};
//更新データをサーバーサイドに送る(httpclientの記述については割愛)
this._eventService.updateToDo(
todoItem.id,
todoItem.name,
todoItem.status
//(※データ項目は一部省略)
).subscribe(
(result: ToDo) => {
//サーバー側のデータ更新成功時にフロント側データも更新
this.todoUpdated(result);
},
error => {
console.log(error);
alert('更新失敗しました');
}
);
}
}
//フロント側データ更新用
todoUpdated(todo: ToDo) {
//toDosは全ToDoデータを格納している変数。前述のほうのsample.ts参照
const indexup = this.toDos.findIndex(todoData => todoData.id === todo.id)
//移動させたToDoカードが全体の配列データ中、何番目か値を取り出す
if (indexup >= 0) {
this.toDos[indexup] = todo;
//toDosの該当箇所(移動させたToDoのデータ)のみ上書きすることで、
//いちいちサーバー側に更新データを取りに行かなくて済む
}
}
}
#まとめ
......と、もう少し説明を加えないと分かりにくいかもしれませんが、D&Dを実用化するには
・データバインドの理解があるか
・JSONデータから配列を分類できるか
・ドロップ時にステータス更新ができるか
・それと同時にサーバー側のデータを更新できるか
といった壁が存在するので、Materialは便利だけれども基礎的な理解は必要だったり、はじめは嫌でも徐々にMaterialのAPIページを読むことも大事になってきます。
初期に自分はどハマリして、Angularはなんて難しいフレームワークなんだ!と思ったりしましたが喉元すぎればなんとやら、だいたい乗り越えられます。
そして今回の記事がこれから触る方にとって、何かしらのヒントになったら幸いです。
次回、Angular Advent Calendar 2020 10日目は@nishiemonさんです。よろしくお願いいたします。