12
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AngularAdvent Calendar 2020

Day 9

Angular Material のDrag&Dropで躓いた話(httpclient使用時のAPIデータの更新まで)

Last updated at Posted at 2020-12-08

#はじめに
こちらは、AdventCalender2020の9日目の記事です。

Angularを触り始めて半年、というか初のJSフレームワークがAngularの私です。
おそらく多くの方が、フレームワークになれるため最初はTODOアプリを作ったりするのではないでしょうか? そしてDrag&Drop機能(以下D&D)も付けたくなったりするのでは無いでしょうか?

ご存知の通りD&DはMaterialに存在していて、それを確認した私は「なら、そこまで難しくないな」と高を括って、案の定ドハマりしたわけなので、この気にDrag&Dropを通じでAngularの基本的な解説と、RestAPIまで使ったToDoアプリの紹介をしようと思います。

なので今回は初心者向けなAdventCalenderとなりますこと、ご了承いただけたら幸いです。

#作りたかったもの
イメージはこうです(データは適当)
Peek 2020-12-08 16-29.gif

これはhttpclientを利用して、ドロップ時にサーバー側のデータを更新するようにしています。

が、そこまで行くのに色々と躓いたので例を上げながら解説していきます。これからの方にとって何かのお役に立てたら幸いです。

#ハマり1「JSONデータから配列が作れない」
書くのを迷いましたが、初歩的なところも書こうと思います。
そもそものJavaScript知識が無いとここで躓きます。

  • まず先にJavaScripを押さえましょう
  • テーブル設計と分類用の項目を考えておきましょう

真面目に、「そんなとこから?」ってところから始めている人間だったので、簡単なことでもネットでどう検索したらいいかもわからず、質問の仕方もわからなかったりして、勉強したての頃ほど簡単なことの壁がプレッシャーにすら感じたりするもんです。それ故にはじめの段階がクリアできなくて挫折したり、その言語やフレームワークを嫌いになったりするわけで。

で、ここで例を上げると、サーバーサイドの設定は割愛しますが例えば

example.json
[
    {
        "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に分けてます。

sample.ts
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()でデータを渡しています。

sample.html
<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>

#ハマり2「そもそもドロップが反映されない」
Peek 2020-12-08 20-05.gif

原因

[cdkDropListData]="hoge" の記述エラー(もしくは書き忘れ)

きちんとtsファイルを確認してデータバインディングさせましょう。なくてもエラーが出ないからといってcdkDropListDataの記述を消してはいけません。

ここでコンテナとデータの紐付けをするので、以降のドロップイベントでデータが反映されなくなり、ドロップが完了しません。

#ハマり3「ドロップ時のstatus値を変更する方法」

ハマったというほどではないのですが、ドロップ時にstatusの値を変更したかったので、さてどうしたものかと考えました。
実際、ドロップできてもサーバー側のデータを更新していないと、カードは常に同じ位置に戻ります。
Peek 2020-12-08 20-25.gif

フロントとしてはサーバーサイドと連携して初めて意味があると思っているので、httpclientまで見越したToDoアプリまでできたら、Angular初学者は抜けたと思って良いのではないでしょうか。(と初学者レベルが言ってみるw)

もっといい方法があるかもしれませんが、以下のように解決できます。

sample.ts
  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さんです。よろしくお願いいたします。

12
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?