Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

※はじめからはこちら

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

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

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

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への組み込み

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

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に組み込みます。

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に追加したpatchdeleteを呼び出すようにしています。

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

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

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がupdateCardContentupdateCardTitleの2つあるところです。どちらも更新処理自体はClientに追加したupdateCardDataを使うのですが、タイトルはボードの画面にも出ているので更新したら他のクライアントにも通知しなければいけません。

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

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

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://qiita.com/denzow/items/1ef4f0ab459a1d0a528b

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away