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

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

前回で個別のカードを開いたときの動きを作りました。今回はカード情報の更新などを実装していきます。

### カード更新・削除サービスの実装

まずは、カードの部分更新を行うサービスと削除のサービスを実装します。


```diff:application/modules/kanban/service.py
@@ -84,3 +84,25 @@ def get_card_by_card_id(card_id):
     :return:
     """
     return Card.get_by_id(card_id)
+
+
+def update_card(card_id, title=None, content=None):
+    """
+    :param int card_id:
+    :param str title:
+    :param str content:
+    :return:
+    :rtype Card:
+    """
+    card = Card.get_by_id(card_id)
+    if title:
+        card.title = title
+    if content:
+        card.content = content
+    card.save()
+    return card
+
+
+def delete_card(card_id):
+    card = Card.get_by_id(card_id)
+    card.delete()
```

### viewへの組み込み

作ったサービスを組み込みます。

```diff:application/views/api/boards.py
@@ -82,3 +82,29 @@ class CardGetApi(View):
                 'updated_at': card.updated_at,
             }
         })
+
+    def patch(self, request, board_id, card_id):
+        """
+        カードの内容を更新する
+        """
+        data = json.loads(request.body)
+        title = data.get('title')
+        content = data.get('content')
+        card = kanban_sv.update_card(card_id=card_id, title=title, content=content)
+
+        return JsonResponse({
+            'card_data': {
+                'title': card.title,
+                'content': card.content,
+                'updated_at': card.updated_at,
+            }
+        })
+
+    def delete(self, _, board_id, card_id):
+        """
+        カードを削除する
+        """
+        kanban_sv.delete_card(card_id=card_id)
+        return JsonResponse({
+            'success': True
+        })
```

更新時は更新部分を`patch`メソッドで受け取り、削除時はたんに`delete`メソッドで呼び出します。

### Clientへの組み込み

URLは前回割り当ててるのでそのまま`/boards/:boardId/cards/:cardId`で使えます。メソッドだけ変えてClientに組み込みます。

```diff:application/vuejs/utils/kanbanClient.js
@@ -71,6 +71,23 @@ class KanbanClient extends Client {
     const response = await this._get(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`);
     return response.data.cardData;
   }
+
+  async updateCardData({
+    boardId,
+    cardId,
+    content,
+    title,
+  }) {
+    const response = await this._patch(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`, {
+      content,
+      title,
+    });
+    return response.data.cardData;
+  }
+
+  async deleteCard({ boardId, cardId }) {
+    await this._delete(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`);
+  }
 }

````

Viewに追加した`patch`と`delete`を呼び出すようにしています。

### コンポーネント・Storeへの組み込み

コンポーネントから処理を実行できるようにしていきます。まずはStoreに組み込みます。

```diff:application/vuejs/src/store/pages/board.js
@@ -51,6 +51,32 @@ const actions = {
     const cardData = await kanbanClient.getCardData({ boardId, cardId });
     commit('setFocusedCard', cardData);
   },
+  async updateCardContent({ commit }, { boardId, cardId, content }) {
+    const cardData = await kanbanClient.updateCardData({
+      boardId,
+      cardId,
+      content,
+    });
+    commit('setFocusedCard', cardData);
+  },
+  async updateCardTitle({ commit, dispatch }, { boardId, cardId, title }) {
+    const cardData = await kanbanClient.updateCardData({
+      boardId,
+      cardId,
+      title,
+    });
+    commit('setFocusedCard', cardData);
+    // titleはボード自体に出ているので他のクライアントへの反映を依頼する必要がある
+    dispatch('broadcastBoardData');
+  },
+  async deleteCard({ dispatch }, { boardId, cardId }) {
+    await kanbanClient.deleteCard({
+      boardId,
+      cardId,
+    });
+    // カード自体はボード自体に出ているので他のクライアントへの反映を依頼する必要がある
+    dispatch('broadcastBoardData');
+  },
 };
```

ポイントは更新関連のActionが`updateCardContent`と`updateCardTitle`の2つあるところです。どちらも更新処理自体はClientに追加した`updateCardData`を使うのですが、タイトルはボードの画面にも出ているので更新したら他のクライアントにも通知しなければいけません。

そこで、Titleの更新だけは更新完了後に`dispatch('broadcastBoardData');`を呼び出して他のクライアントにボードデータの再取得を促しています。

では、これをコンポーネントに組み込みます。

```diff:application/vuejs/src/pages/Board/Card/Show.vue
@@ -1,23 +1,32 @@
 <template>
-  <div class="modal" aria-labelledby="modal-title" aria-hidden="true">
-    <div class="modal-dialog" role="document">
+  <div v-if="fetchFocusedCard" class="modal" aria-labelledby="modal-title" aria-hidden="true" @click="close">
+    <div class="modal-dialog" role="document" @click.prevent.stop="">
       <div class="modal-content">
         <div class="modal-header">
           <h5 class="modal-title" id="modal-title">
-            <span>
+            <span v-show="!isTitleEditing" @dblclick="startTitleEdit">
               {{ focusedCard.title }}
             </span>
+            <span v-show="isTitleEditing">
+              <input type="text" v-model="editTitle">
+              <button type="button" class="btn btn-primary" @click="saveTitle">save</button>
+            </span>
           </h5>
-          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="close">
             <span aria-hidden="true">&times;</span>
           </button>
         </div>
-        <div class="modal-body">
-          <p class="card-content">{{ focusedCard.content }}</p>
+        <div class="modal-body" v-show="!isContentEditing">
+          <p v-if="focusedCard.content" @dblclick="startContentEdit" class="card-content">{{ focusedCard.content }}</p>
+          <p v-else @click="startContentEdit" class="empty-content">enter content.</p>
+        </div>
+        <div class="modal-body" v-show="isContentEditing">
+          <textarea class="edit-area" v-model="editContent"></textarea>
+          <button type="button" class="btn btn-primary" @click="saveContent">Save</button>
         </div>
         <div class="modal-footer">
-          <button type="button" class="btn btn-danger">delete</button>
-          <button type="button" class="btn btn-primary">Close</button>
+          <button type="button" class="btn btn-danger" @click="deleteCardAction">delete</button>
+          <button type="button" class="btn btn-primary" @click="close">Close</button>
         </div>
       </div>
     </div>
@@ -31,7 +40,6 @@ const { mapState, mapActions } = createNamespacedHelpers('board');


 export default {
-  name: 'CardShow',
   props: {
     cardId: {
       type: Number,
@@ -42,12 +50,64 @@ export default {
       default: null,
     },
   },
+  name: 'CardShow',
   computed: {
     ...mapState(['focusedCard']),
   },
+  data() {
+    return {
+      isContentEditing: false,
+      editContent: '',
+      isTitleEditing: false,
+      editTitle: '',
+    };
+  },
   methods: {
+    close() {
+      this.$router.push({
+        path: `/boards/${this.boardId}`,
+        query: this.$route.query,
+      });
+    },
+    async deleteCardAction() {
+      await this.deleteCard({
+        boardId: this.boardId,
+        cardId: this.cardId,
+      });
+      window.alert('delete succeeded');
+      this.close();
+    },
+    startContentEdit() {
+      this.isContentEditing = true;
+      this.editContent = this.focusedCard.content;
+    },
+    startTitleEdit() {
+      this.isTitleEditing = true;
+      this.editTitle = this.focusedCard.title;
+    },
+    async saveContent() {
+      this.isContentEditing = false;
+      if (this.editContent === this.focusedCard.content) return;
+      await this.updateCardContent({
+        boardId: this.boardId,
+        cardId: this.cardId,
+        content: this.editContent,
+      });
+    },
+    async saveTitle() {
+      this.isTitleEditing = false;
+      if (this.editTitle === this.focusedCard.title) return;
+      await this.updateCardTitle({
+        boardId: this.boardId,
+        cardId: this.cardId,
+        title: this.editTitle,
+      });
+    },
     ...mapActions([
       'fetchFocusedCard',
+      'updateCardContent',
+      'updateCardTitle',
+      'deleteCard',
     ]),
   },
   watch: {
````

StoreのActionを追加するとともに、タイトルやコンテンツをダブルルクリックすると編集画面になるように変更しました。

![](https://i.gyazo.com/ede05fc16aa9dc4c622657375c3afc23.gif)

地味ですが、タイトル編集時には後ろにでているカード一覧のほうのタイトルも変更されていることがわかります。もちろん他のブラウザで同じボードをみていても同時に変更されています。

これでカードデータの更新・削除が実装できました。

### 次回

リストの追加・削除・更新を作っていきます。

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