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

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

今回はカンバンのUI部分を作っていきます。

### カンバンUI

大体こんな感じのコンポーネントを作っていきます。

![KANBAN_🔊-3.png](https://qiita-image-store.s3.amazonaws.com/0/44722/40c0e21e-0602-73e0-de55-09bdbb1ec1c1.png)

#### Boardコンポーネントの準備

http://localhost:3000/board/1 といったURLで使うコンポーネントなので、`Board`というディレクトリを作成します。

```bash
$ mkdir application/vuejs/src/pages/Board
```

そして、このページでのエントリーポイントになる`Index.vue`を用意します。

```vue:application/vuejs/src/pages/Board/Index.vue
<template>
  <div class="board">
  </div>
</template>

<script>
export default {
  name: 'Board',
  components: {
  },
};
</script>
<style lang="scss" scoped>
</style>
```

そして、 http://localhost:3000/boards/1 でアクセスしたさいにこの`Index.vue`がレンダリングされるようにVue-routerの設定を変更します。

```diff:application/vuejs/src/router/index.js
@@ -5,6 +5,7 @@ import WebSocketMiddleware from './middlewares/websocket';
 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';


 Vue.use(Router);
@@ -21,6 +22,11 @@ const router = new Router({
           path: '',
           component: Home,
         },
+        {
+          path: 'boards/:boardId',
+          component: Board,
+        },
       ],
     },
     {
```

この時点で http://localhost:3000/boards/1 にアクセスしても404は出なくなっているはずです。

#### MenuBarの実装

とりあえず、MenuBarを作ります。

```vue:application/vuejs/src/pages/Board/components/MenuBar.vue
<template>
  <div class="board-menu-bar navbar navbar-dark bg-dark">
    <span class="navbar-brand mb-0 h1 board-name"></span>
    <nav class="my-2 my-md-0 mr-md-3">
      <form class="form-inline mt-2 mt-md-0" id="search-form">
        <input name="query" class="form-control mr-3"
               type="text" placeholder="Search" aria-label="Search">
        <button type="button" class="btn btn-outline-danger" >delete</button>
      </form>
    </nav>
  </div>
</template>

<script>

export default {
  name: 'MenuBar',
  data() {
    return {
    };
  },
  computed: {
  },
  methods: {
  },
};
</script>

<style scoped>
  .board-menu-bar {
    margin: 1rem 0;
    width: 100%;
  }
  .board-name {
    cursor: pointer;
  }
</style>

```

そして、`Index.vue`に組み込みます。

```diff:application/vuejs/src/pages/Board/Index.vue
@@ -1,12 +1,16 @@
 <template>
   <div class="board">
+    <MenuBar></MenuBar>
   </div>
 </template>

 <script>
+import MenuBar from './components/MenuBar.vue';
+
 export default {
   name: 'Board',
   components: {
+    MenuBar,
   },
 };
 </script>
```

これで、開くと以下のようにメニューバーが表示されます

![KANBAN-4.png](https://qiita-image-store.s3.amazonaws.com/0/44722/a56fc304-d447-3f4c-bd9c-864b8c5e78d3.png)

#### BoardAreaの実装

MenuBarの下はカンバンの内容が表示される領域です。まずはここ全体を表現する`BoardArea`というコンポーネントを作成します。

```vue:application/vuejs/src/pages/Board/components/BoardArea.vue
<template>
  <div class="board-area">
  </div>
</template>

<script>
export default {
  name: 'BoardArea',
};
</script>

<style scoped>
.board-area {
  margin: 1rem 0;
  width: 100%;
}
.board-container {
  display: flex;
}
</style>
```

そして`Index.vue`に組み込みます。


```diff:application/vuejs/src/pages/Board/Index.vue
@@ -1,16 +1,20 @@
 <template>
   <div class="board">
     <MenuBar></MenuBar>
+    <BoardArea></BoardArea>
   </div>
 </template>

 <script>
 import MenuBar from './components/MenuBar.vue';
+import BoardArea from './components/BoardArea.vue';
+

 export default {
   name: 'Board',
   components: {
     MenuBar,
+    BoardArea,
   },
 };
 </script>
```

#### VueでWebSocketを使う準備

このページではWebSocketを使うのでそのあたりを整備していきます。今回はVueでWebSocketを使うため[vue-native-websocket](https://github.com/nathantsoi/vue-native-websocket)を使います。それに合わせて、トップレベルのStoreにmutationとstateを追加します。

```diff:application/vuejs/src/store/index.js
@@ -1,7 +1,6 @@
 import Vue from 'vue';
 import Vuex from 'vuex';
 import createLogger from 'vuex/dist/logger';

 import header from './header';
 import home from './pages/home';

@@ -17,8 +16,33 @@ export default new Vuex.Store({
     home,
   },
   state: {
+    socket: null,  # 接続中のsocketを保持する
   },
   mutations: {
+    SOCKET_ONOPEN(state, event) {
+      console.log('SOCKET_ONOPEN', event);
+      // 接続時にStoreにwebsocketオブジェクトを登録する
+      state.socket = event.target;
+    },
+    SOCKET_ONCLOSE(state, event) {
+      console.log('SOCKET_ONCLOSE', event);
+      // 接続終了にStoreにwebsocketオブジェクトを解除する
+      state.socket = null;
+    },
+    SOCKET_ONERROR(state, event) {
+      console.log('SOCKET_ONERROR', event);
+    },
+    // default handler called for all methods
+    SOCKET_ONMESSAGE(state, message) {
+      console.log('SOCKET_ONMESSAGE', message);
+    },
+    // mutations for reconnect methods
+    SOCKET_RECONNECT(state, count) {
+      console.log('SOCKET_RECONNECT', count);
+    },
+    SOCKET_RECONNECT_ERROR(state) {
+      console.log('SOCKET_RECONNECT_ERROR', state);
+    },
   },
 });
```

実際に必要なmutationは`SOCKET_ONOPEN` と`SOCKET_ONCLOSE`だけですがほかも一応デバッグの意味で入れておきます。`SOCKET_ONOPEN `が呼び出されることで、接続が確立されたWebSocketのセッションオブジェクトが`state.socket`に入るので、実際にメッセージをやり取りする時は`state.socket`を通して行うようにします。

さて、WebSocketとの接続を実際に行うタイミングはいくつか考えられますが今回はrouteの切り替わり時にするようにします。実は今回利用しているテンプレートにはすでにWebSocketへの接続を行うVue-routerのミドルウェアが設定されています(`application/vuejs/src/router/middlewares/websocket.js`)

ここは少し込み入ってるので詳細は省略しますが、Vue-routerのルーティングの定義に`meta.ws`をセットすればルーティングの切替時にWebSocketへ接続が行われます。

```diff:application/vuejs/src/router/index.js
@@ -25,6 +25,9 @@ const router = new Router({
         {
           path: 'boards/:boardId',
           component: Board,
+          meta: {
+            ws: route => route.path,
+          },
         },
       ],
     },
```

これで、`/boards/1`にアクセスしたときには同時に`/ws/boards/1`にWebSocketへ接続します。

#### boardストアの実装

WebSocketへ接続できたので`Board`コンポーネントで使用するストアを用意します。

```javascript:application/vuejs/src/store/pages/board.js
import camelcaseKeys from 'camelcase-keys';


const state = {
  boardData: {
    pipeLineList: [],
  },
};


const getters = {
  getSocket(state, getters, rootState) {
    return rootState.socket;
  },
};

const actions = {
};

const mutations = {
  setBoardData(state, { boardData }) {
    state.boardData = camelcaseKeys(boardData, { deep: true });
  },
};


export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
};

```

WebSocketへの接続の`state.socket`はrootのStoreの属性なので、それにアクセスできるように`getSocket`というgetterを定義しています。また、Consumerから戻されたボードのデータをしまうための`state.boardData`と、値をセットするための`setBoardData` mutationを定義しておきます。ちなみにPythonは基本的にsnake_caseでJavascriptはCamelCaseなので、それを変換してくれる`camelcaseKeys`というライブラリをつかって、Pythonから来たデータをCamelCaseにしています。

このStoreをrootStoreに組み込みます。

```diff:application/vuejs/src/store/index.js
@@ -1,9 +1,9 @@
 import Vue from 'vue';
 import Vuex from 'vuex';
 import createLogger from 'vuex/dist/logger';

 import header from './header';
 import home from './pages/home';
+import board from './pages/board';

 Vue.use(Vuex);

@@ -15,10 +15,36 @@ export default new Vuex.Store({
   modules: {
     header,
     home,
+    board,
   },
```


#### Consumerの修正

Consumerに接続した際にConsumerからボードのデータを送り、それを`setBoardData`をつかってSotreに格納するようにします。`VueNativeWebsocket`では`mutation: xxx`や`action: xxx` というデータが含まれたメッセージがWebsocketに来た場合、対応するmutationやactionを呼び出してくれる機能があるのでそれを利用します。


```diff:application/views/ws/kanban_consumer.py
@@ -12,6 +12,7 @@ class KanbanConsumer(JsonWebsocketConsumer):
         # 後で使う
         self.user = None
         self.board_id = None
+        self.namespace = 'board'

     def connect(self):
         # 認証チェック
@@ -29,7 +30,8 @@ class KanbanConsumer(JsonWebsocketConsumer):
     def send_board_data(self):
         board_data = kanban_sv.get_board_data_by_board_id(self.board_id)
         self.send_json({
-            'boardData': board_data
+            'boardData': board_data,
+            'mutation': 'setBoardData',
+            'namespace': self.namespace,
         })
```

`namespace`はどのStoreのmuationを呼び出すかを`VueNativeWebsocket`に伝えるものです。今回は`board`というStoreの`setBoardData`ですので、その様に指定します。

これで、初回の接続時に渡されるボードのデータがStoreに格納されます。ただ、見た目上は分かりづらいので`MenuBar`にボード名を表示するようにします。

```diff:application/vuejs/src/pages/Board/components/MenuBar.vue
@@ -1,6 +1,6 @@
 <template>
   <div class="board-menu-bar navbar navbar-dark bg-dark">
-    <span class="navbar-brand mb-0 h1 board-name"></span>
+    <span class="navbar-brand mb-0 h1 board-name">{{ boardName }}</span>
     <nav class="my-2 my-md-0 mr-md-3">
       <form class="form-inline mt-2 mt-md-0" id="search-form">
         <input name="query" class="form-control mr-3"
@@ -12,6 +12,9 @@
 </template>

 <script>
+import { createNamespacedHelpers } from 'vuex';
+
+const { mapState } = createNamespacedHelpers('board');

 export default {
   name: 'MenuBar',
@@ -20,6 +23,12 @@ export default {
     };
   },
   computed: {
+    boardName() {
+      return this.boardData.name;
+    },
+    ...mapState([
+      'boardData',
+    ]),
   },
   methods: {
   },
```

`board`ストアの`boardData`をコンポーネントに割当て、その中の`name`を`boardName`として参照できるようにしています。この変更により、ページを開いた際に以下のようにボード名が表示されるようになっているはずです。

![KANBAN-5.png](https://qiita-image-store.s3.amazonaws.com/0/44722/73bb7468-99f2-a3a8-9d45-ba0d25eb1be6.png)

ちゃんとWebSocketから渡ってきたデータが参照できているのでボード名が`MyBoard1`と出てますね。

### 次回

https://qiita.com/denzow/items/b46a15e594a4b484a5de