カンバン方式のタスク管理アプリを作成する

# はじめに
[Recursion](https://recursionist.io/)というサービスのプロジェクト課題の１つであるタスク管理アプリを作成した際に
詰まった箇所が多かったので、頭の整理のためにまとめた。　　



今回は、デザインを良い感じにしてくれるVuetifyとドラッグ＆ドロップで要素を動かせるVue.Draggableを使用した。

* [Vuetify](https://vuetifyjs.com/ja/)
* [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable/blob/master/README.md). 



# 完成品
[GitHub - Task Management App](https://github.com/Takasu-y/task_management_app)
![c6c27ea05632584b1d32cb65cd859560](https://user-images.githubusercontent.com/63044039/139081751-ab45b653-66e8-416f-a293-b91e91f2635f.gif)


**live demo**
https://takasu-y.github.io/task_management_app/

# 開発環境
* Node.js v14.4.0
* npm 6.14.5
* vue/cli 4.5.13


**バージョン確認方法**

```
node -v
npm -v
vue --version
```


# 環境構築
## プロジェクト作成
```
vue create "プロジェクト名"
```

　　
Vue2を選択する。
※Vue2を選択する理由は、現在（2021年10月時点）VuetifyがVue3をサポートしていないため。
 

```
//Vue2を選択
Please pick a preset: (Use arrow keys)
> Default ([Vue 2] babel, eslint) ← こちらを選択
  Default (Vue 3) ([Vue 3] babel, eslint) 
  Manually select features 
```
　　
下記のように表示されたらプロジェクト作成完了。
 
```
🎉  Successfully created project "プロジェクト名".
👉  Get started with the following commands:

 $ cd "プロジェクト名"
 $ npm run serve
```


**ディレクトリ移動**
起動するためにプロジェクトのディレクトリへ移動する。

```
cd "プロジェクト名"
```

**プロジェクト起動**

```cmd
npm run serve
```
  
  
下記のLocal URLをcmd+クリック 

```
 DONE  Compiled successfully in 2720ms                                                 12:35:28


  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://192.168.1.39:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.
```
　　

下記の画面が表示されれば成功。

画面が確認出来たら
^ + cで起動停止する。

![初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693255/09d93614-af65-2482-2faf-b8e5adb75a32.png)



## Vuetifyのインストール
```
vue add vuetify
```
 
Still proceed -> Yes を選択する。

```
 WARN  There are uncommitted changes in the current repository, it's recommended to commit or stash them first.
? Still proceed? Yes
```

Choose a preset -> Defaultを選択する。

```
? Choose a preset: (Use arrow keys)
  Configure (advanced) 
❯ Default (recommended) 
  Vite Preview (Vuetify 3 + Vite) 
  Prototype (rapid development) 
  Vuetify 3 Preview (Vuetify 3) 
```

下記のように表示されたらインストール完了。

```
✔  Successfully invoked generator for plugin: vue-cli-plugin-vuetify
  vuetify  Discord community: https://community.vuetifyjs.com
  vuetify  Github: https://github.com/vuetifyjs/vuetify
  vuetify  Support Vuetify: https://github.com/sponsors/johnleider
```

**Vuetifyが反映されていることを確認**
先程と同様にプロジェクトを起動して、
下記のように画面にVuetifyが表示されていれば成功。
![vuetify初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693255/2ad689da-74bb-2500-8527-345aa3896f02.png)


## Material Design Iconsのインストール
[Vuetify font Iconを追加](https://vuetifyjs.com/ja/features/icon-fonts/)を参考にインストールする。


```
npm install @mdi/font -D
```

CDNの場合は

```
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet">
```


## Vue.Draggableのインストール
```
npm i -S vuedraggable
```

**使い方**

draggableタグで囲んだ要素を動かせるようになる。
ポイントとしては、
* 動かしたい要素に共通のセレクターを指定する
* draggableタグ内のdraggable属性に動かしたい要素のセレクターを指定する
* draggableを複数使う場合はgroup属性を指定する


# 開発
いきなりコーディングに入っても上手くいかないので、
以下の①〜③を明確にしてからコーディングを行った。

## コードを書く前に考えたこと

①要件を整理する
②何のコンポーネントを作成するか決める
③どこに状態を持たせるか決める

## 要件
**task**

* タスクカードはタスクのタイトルと内容を記入できる。
* 編集：🖋アイコンまたはタスクカードをダブルクリック
* お気に入り：★アイコンをクリック
* チェック：✔️アイコンをクリック
* 削除：🗑アイコンをクリック


**Section**

* セクションの右側＋ボタンで新たにセクションを追加。
* セクション名はダブルクリックすることで編集できる。
* 各セクション下部の＋ボタンで新たにタスクを追加。
* タスクカードを並べる。
* 各セクションを横並びにする。


## 作成するコンポーネント
* task.vue
* taskSection.vue
* taskManager.vue



## どこに状態を持たせるか
[ワイ「何で子コンポーネントに状態を持たせたらあかんの？」](https://qiita.com/Yametaro/items/38420265011ee8bc42a0)を参考に。
>"ハスケル子「子コンポーネントは、親から受け取った状態を映し出す単なるディスプレイとして作りましょう」"

ということなので、タスクの状態はtaskSectionコンポーネントに持たせ、
taskコンポーネントはpropsで状態を受け取って、反映させるだけにした。


```js:task.vue
<script>
export default {
    name: "task",
    props: {
        task: {
            type: Object, //データ型
            required: true, //データ渡し必須
        },
        favorite: {
            type: Boolean,
            required: true,
        },
        check: {
            type: Boolean,
            required: true,
        },
    },
};
</script>
```

タスクカードの各アイコンがクリックされたら
$emit(任意のイベント名, タスクカードのオブジェクト)で親コンポーネントへ送る。

```js:task.vue
<template>

    (略)

     <v-card-actions class="d-flex justify-end">
         <v-icon @click="$emit('edit', task)">mdi-pencil</v-icon>
         <v-icon @click="$emit('favorite', task)" :class="{ 'lime--text': task.favorite}">mdi-star</v-icon>
         <v-icon @click="$emit('check', task)" :class="{ 'teal--text': task.check}">mdi-check-bold</v-icon>
         <v-icon @click="$emit('delete', task)">mdi-delete</v-icon>
     </v-card-actions>

　　　　　　　（略）

</template>
```

子コンポーネントから送ったイベントを
親コンポーネントのtemplateへ

```js:taskSection.vue
<template>

    (略)

    <draggable
    v-model="tasks"
    draggable=".item"
    group="items"
    >
        <task
            v-for="task in tasks"
            :key="task.id"
            :task="task"
            class="item"
            @edit="editTask"         //子コンポーネントから送られてきたeditイベント
            @favorite="favoriteTask" //子コンポーネントから送られてきたfavoriteイベント
            @check="checkTask"　　　　　　　　　　　　　//子コンポーネントから送られてきたcheckイベント
            @delete="deleteTask"　　　　　　　　//子コンポーネントから送られてきたdeleteイベント
        ></task>
    </draggable>

　　　　　　　（略）

</template>
```

タスクの状態や変更を加えるメソッドは全てTaskSectionコンポーネントへ記述。

```js:taskSection.vue
<script>
export default {
    
　　　　　　　　（略）

    data() {
        return {
            section: {
                title: "",
                editable: true,
            },
            tasks: [
                 {
                     id: todoId++,
                     title: "朝ごはんを食べる",
                     body: "プロテインも飲む",
                     editable: false,
                     favorite: false,
                     check: false,
                 },
                 {
                     id: todoId++,
                     title: "昼ごはんを食べる",
                     body: "中華かラーメンを食べる",
                     editable: false,
                     favorite: false,
                     check: false,
                 },
                 {
                     id: todoId++,
                     title: "夜ごはんを食べる",
                     body: "サラダのみにする",
                     editable: false,
                     favorite: false,
                     check: false,
                 },
            ]
        }
    },
    methods: {
        addTask(){
            this.tasks.push({
                id: todoId++,
                title: "",
                body: "",
                editable: true,
                favorite: false,
                check: false,
            })
        },
        editTask(taskObject){
            taskObject.editable = !taskObject.editable;
        },
        favoriteTask(taskObject){
            taskObject.favorite = !taskObject.favorite;
        },
        checkTask(taskObject){
            taskObject.check = !taskObject.check;
        },
        deleteTask(taskObject){
            this.tasks = this.tasks.filter(task => task.id !== taskObject.id);
        },
    },
};
</script>
```

最後にtaskManager.vueでセクションを並べたり、追加ボタンを設置して完成です。


# おまけ
GitHub Pagesに公開する場合は、/docs配下にコンパイルしなければいけないので
vue.config.jsにoutputDir: 'docs/',を記述して `npm run build`を行う。




# 参考
* [Vue公式ドキュメント](https://jp.vuejs.org/)
* [【Vue】propsを使いこなす。typeやプロパティの設定方法と注意点](https://qiita.com/shizen-shin/items/ad737fd6b290fe44c0e5)
* [ワイ「何で子コンポーネントに状態を持たせたらあかんの？」](https://qiita.com/Yametaro/items/38420265011ee8bc42a0)
* [【Vue.js】イベントハンドリングをサンプルを作りながら理解する](https://qiita.com/b1san/items/6bc0be17cd6ed687520c)
* [Vue-CLIインストールガイド](https://cli.vuejs.org/guide/installation.html)
* [Vuetifyに入門する](https://qiita.com/azukiazusa/items/16ebffd361af8fa58333)
* [とても簡単にドラッグアンドドロップが実現できる Vue-draggable を使ってみた](https://dev.classmethod.jp/articles/vue-draggable-2/)
* [【Vuetify】max-heightを指定してスクロールバーを表示する](https://into-the-program.com/vuetify-show-scrollbar-max-height/)
* [Vue Cli 4で GitHubPagesに公開する方法](https://qiita.com/kuimac/items/9cfd08b2c3df871a5f56)