DjangoとVueでカンバンアプリケーションを作る(13)

※はじめからは[こちら](https://qiita.com/denzow/items/046f3c8b9bd8d3378eb4#_reference-7b3e4a58510793f8a9c7)

前回でカードの追加ができるようになりました。今回はカードをクリックした際の挙動などを作っていきます。

### モーダルコンポーネントの作成

カードをクリックした際にその詳細が開く様にします。そのためには、クリックした際に開くモーダルを準備します。

```vue:application/vuejs/src/pages/Board/Card/Show.vue
<template>
  <div class="modal" aria-labelledby="modal-title" aria-hidden="true">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="modal-title">
            <span>
              Title
            </span>
          </h5>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <p class="card-content">Content</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-danger">delete</button>
          <button type="button" class="btn btn-primary">Close</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CardShow',
  props: {
    cardId: {
      type: Number,
      default: null,
    },
    boardId: {
      type: Number,
      default: null,
    },
  },
};
</script>

<style lang='scss' scoped>
  .modal {
    display: block;
    background-color: rgba(1, 1, 1, 0.5);
  }
  .modal-dialog {
    z-index: 1060;
  }
  .edit-area {
    width: 95%;
    height: 5rem;
  }
  .empty-content {
    text-decoration: underline;
    cursor: pointer;
  }
  .card-content {
    white-space: pre;
  }
</style>
```

Bootstrapのモーダルをベースにしているので少しコード量が多いですが、このコンポーネントがモーダルウィンドウ自体になります。
モーダルですので、もとのカンバン画面上で表示させます。このとき、単に表示・非表示のフラグをStoreに持たせる方法もありますが、素直に`boards/:boradId/cards/:cardId`といったURLでモーダルを開いている状態として表現する方がブックマークできたりと便利なので、その方針で実装します。

まず、モーダルがカンバン画面上に表示されるように`BoardArea`コンポーネントに`<router-view>`を追加します。

```diff:application/vuejs/src/pages/Board/components/BoardArea.vue
@@ -12,6 +12,7 @@
         :key="pipeLine.id"
       />
     </Draggable>
+    <router-view></router-view>
   </div>
 </template>
```

続いて、ルーティングの設定を追加します。

```diff:application/vuejs/src/router/index.js
@@ -6,6 +6,7 @@ import DefaultLayout from '../components/layouts/DefaultLayout.vue';
 import NotFound from '../pages/NotFound.vue';
 import Home from '../pages/Home/Index.vue';
 import Board from '../pages/Board/Index.vue';
+import CardShow from '../pages/Board/Card/Show.vue';


 Vue.use(Router);
@@ -28,6 +29,19 @@ const router = new Router({
           meta: {
             ws: route => route.path,
           },
+          children: [
+            {
+              path: 'cards/:cardId',
+              component: CardShow,
+              props: route => ({
+                cardId: parseInt(route.params.cardId, 10),
+                boardId: parseInt(route.params.boardId, 10),
+              }),
+              meta: {
+                ws: route => route.path.split('/cards')[0],
+              },
+            },
+          ],
         },
       ],
     },
```

これで、`boards/:boardId/cards/:cardId`としてアクセスしたときにモーダルが開くようになります。


![KANBAN-9.png](https://qiita-image-store.s3.amazonaws.com/0/44722/6d5929a0-beba-6de9-d0a7-f48bf9b5d3f8.png)

まだカードの内容を反映したり、閉じたりはできませんがUIの大枠ができました。

### カードクリック時にモーダルがオープンするようにする

カードクリック時に先程のモーダル画面に移動するようにします。これはURLを書き換えるようにするだけで良いです。Cardコンポーネントを変更します。

```diff:application/vuejs/src/pages/Board/components/BoardArea/Card.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="card">
+  <div class="card" @click="openCard">
     <div class="card-body">
       <h5 class="card-title">{{ title }}</h5>
     </div>
@@ -8,6 +8,9 @@

 <script>

+import { createNamespacedHelpers } from 'vuex';
+
+const { mapGetters } = createNamespacedHelpers('board');
 export default {
   name: 'Card',
   props: {
@@ -23,6 +26,16 @@ export default {
     content() {
       return this.card.content;
     },
+    ...mapGetters(['getBoardId']),
+  },
+  methods: {
+    openCard() {
+      console.log('OPEN');
+      this.$router.push({
+        path: `/boards/${this.getBoardId}/cards/${this.card.cardId}`,
+        query: this.$route.query,
+      });
+    },
   },
 };
 </script>
```

click時にVueRouterを利用して(`$router.push`)URLを`boards/:boardId/cards/:cardId`に書き換えています。これで、クリック時に先程のモーダルが開くようになりました。

### モーダルにデータを当てはめる

現時点では開いたモーダルは固定の`Ttile`と`Content`が表示されるだけです。これを実際のデータにしてみます。`vuejs/src/pages/Board/Card/Show.vue`では以下のように`cardId`と`boardId`を`props`経由で受け取っています。

```javascript
export default {
  name: 'CardShow',
  props: {
    cardId: {
      type: Number,
      default: null,
    },
    boardId: {
      type: Number,
      default: null,
    },
  },
};
```

これはVueRouter経由で渡されています。

```javascript:application/vuejs/src/router/index.js
          children: [
            {
              path: 'cards/:cardId',
              component: CardShow,
              props: route => ({
                cardId: parseInt(route.params.cardId, 10),    // これ
                boardId: parseInt(route.params.boardId, 10),  // これ
              }),
              meta: {
                ws: route => route.path.split('/cards')[0],
              },
            },
```

そのため、コンポーネントにはすでに必要なIDが渡されていますのでこれを使ってサーバからカードの詳細を取得するようにします。

#### カードの情報を戻すAPIの実装

まずは。ID指定でカードを取得するサービスメソッドを定義します。

```diff:application/modules/kanban/service.py
@@ -76,3 +76,11 @@ def add_card(pipe_line_id, card_title):
     )
     card.save()
     return card
+
+
+def get_card_by_card_id(card_id):
+    """
+    :param int card_id:
+    :return:
+    """
+    return Card.get_by_id(card_id)

```

続いて、これをViewに組み込みます。

```diff:application/views/api/boards.py
@@ -64,3 +64,21 @@ class CardApi(View):
                 'name': card.title,
             }
         })
+
+
+@method_decorator(csrf_exempt, name='dispatch')
+class CardGetApi(View):
+
+    def get(self, _, board_id, card_id):
+        """
+        カードを取得する
+        """
+        card = kanban_sv.get_card_by_card_id(card_id)
+
+        return JsonResponse({
+            'card_data': {
+                'title': card.title,
+                'content': card.content,
+                'updated_at': card.updated_at,
+            }
+        })
```

最後にURLを割り当てます。

```diff:application/views/urls.py
@@ -7,7 +7,7 @@ from django.views.generic import TemplateView

 from .accounts import signup as signup_view
 from .api.accounts import AccountsApi
-from .api.boards import BoardListApi, CardApi
+from .api.boards import BoardListApi, CardApi, CardGetApi


 urlpatterns = [
@@ -17,6 +17,7 @@ urlpatterns = [

     path('api/accounts/', AccountsApi.as_view()),
     path('api/boards/', login_required(BoardListApi.as_view())),
+    path('api/boards/<int:board_id>/cards/<int:card_id>/', login_required(CardGetApi.as_view())),
     path('api/cards/', login_required(CardApi.as_view())),
 ]
 urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
```

これで`http://localhost:3000/api/boards/1/cards/1/`にアクセスすると以下のようなJSONが戻るはずです。

```json
{"card_data": {"title": "Card1", "content": "content of Card1", "updated_at": "2018-10-15T13:30:42.613Z"}}
```

カードの情報を取得するAPIはこれで完了です。

### APIをStoreに組み込み

作成したAPIを組み込んでいきます。まずはkanbanClientへの組み込みです。

```diff:application/vuejs/utils/kanbanClient.js
@@ -66,6 +66,11 @@ class KanbanClient extends Client {
       pipeLineId,
     });
   }
+
+  async getCardData({ boardId, cardId }) {
+    const response = await this._get(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`);
+    return response.data.cardData;
+  }
 }

``

続いて、`board`ストアに、モーダルを開いているカードの情報を管理・取得できるようにします。

```diff:application/vuejs/src/store/pages/board.js
@@ -6,6 +6,7 @@ const state = {
   boardData: {
     pipeLineList: [],
   },
+  focusedCard: {},  // 開いているカードの情報
 };


@@ -46,6 +47,10 @@ const actions = {
     });
     dispatch('broadcastBoardData');
   },
+  async fetchFocusedCard({ commit }, { boardId, cardId }) {
+    const cardData = await kanbanClient.getCardData({ boardId, cardId });
+    commit('setFocusedCard', cardData);
+  },
 };

 const mutations = {
@@ -56,6 +61,9 @@ const mutations = {
   setBoardData(state, { boardData }) {
     state.boardData = camelcaseKeys(boardData, { deep: true });
   },
+  setFocusedCard(state, cardData) {
+    state.focusedCard = cardData;
+  },
 };
```

これでコンポーネントから`fetchFocusedCard`を呼び出すことで`state.focusedCard`にカードの情報が入るようになります。

ではコンポーネントに組み込みます。

```diff:application/vuejs/src/pages/Board/Card/Show.vue
@@ -5,7 +5,7 @@
         <div class="modal-header">
           <h5 class="modal-title" id="modal-title">
             <span>
-              Title
+              {{ focusedCard.title }}
             </span>
           </h5>
           <button type="button" class="close" data-dismiss="modal" aria-label="Close">
@@ -13,7 +13,7 @@
           </button>
         </div>
         <div class="modal-body">
-          <p class="card-content">Content</p>
+          <p class="card-content">{{ focusedCard.content }}</p>
         </div>
         <div class="modal-footer">
           <button type="button" class="btn btn-danger">delete</button>
@@ -25,6 +25,11 @@
 </template>

 <script>
+import { createNamespacedHelpers } from 'vuex';
+
+const { mapState, mapActions } = createNamespacedHelpers('board');
+
+
 export default {
   name: 'CardShow',
   props: {
@@ -37,6 +42,26 @@ export default {
       default: null,
     },
   },
+  computed: {
+    ...mapState(['focusedCard']),
+  },
+  methods: {
+    ...mapActions([
+      'fetchFocusedCard',
+    ]),
+  },
+  watch: {
+    cardId: {
+      immediate: true,
+      handler(cardId) {
+        console.log(cardId);
+        this.fetchFocusedCard({
+          boardId: this.boardId,
+          cardId,
+        });
+      },
+    },
+  },
 };
 </script>
```

`watch`で`cardId`を`immediate: true`で指定していますので、Propsで渡ってくるCardIdが変更されるごとに先程作成した`fetchFocusedCard`が呼び出され、モーダル内での表示されるカード情報が更新されるようになります。

これでカードを開くと以下のようにちゃんとカードの情報が埋まった状態になります。

![KANBAN-10.png](https://qiita-image-store.s3.amazonaws.com/0/44722/9e986992-1ecb-efdc-3e10-1a908f45b340.png)

### 次回

カードのデータ更新などを実装していきます。

https://qiita.com/denzow/items/1a83ab2cbf1cf9060e13