最近流行りのフロントエンド構成（実装編）

こんにちわ
[グローバルセンス株式会社](https://gb-sense.com)のskanehiraです。

久しぶりの投稿になってしまいましたが、
以前[こちら](https://qiita.com/skanehira/items/95c6b7bd79b56b6d5102)の記事で紹介した構成を使って、簡単なTodoアプリを作ってみようと思います。

フロントだけで完結できるものなので、結局axiosを使用していないが、
それについては別記事に書く予定です。

# **出来上がったモノ**
[こちら](https://github.com/skanehira/TodoSample)に上げておきますた。
表示・追加・更新・削除は一通り出来ます。
エラー処理はしていないので、変な動きするかもですがサンプルなのでご容赦下さい。

今回はサンプルなのでDB使わず、
データはブラウザ側のLocalStorageに保存するようにしています。

vueのコンポーネントからLocalStorageの操作をしたいので、
[vue-localstorage](https://www.npmjs.com/package/vue-localstorage)を使用しています。

全体の構成はこんな感じになっています。

```shell-session
├── app
│   └── src
│       ├── components
│       │   ├── addTodo
│       │   │   ├── addTodo.html
│       │   │   └── addTodo.vue
│       │   ├── editTodo
│       │   │   ├── editTodo.html
│       │   │   └── editTodo.vue
│       │   ├── header
│       │   │   ├── header.html
│       │   │   └── header.vue
│       │   ├── todoList
│       │   │   ├── todoList.html
│       │   │   └── todoList.vue
│       │   └── top
│       │       └── top.vue
│       └── js
│           ├── app.js
│           └── routes.js
├── package-lock.json
├── package.json
├── public
│   ├── index.html
│   └── js
│       └── bundle.js
└── webpack.config.js
```

# **使い方**
シンプルなので説明するまでもないが一応…

- Todo追加
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/6f606ddf-48bb-71a3-080a-c4d6563de584.png)

タスクの概要と詳細を書いて追加するだけです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/040757ca-4177-c163-3c7e-123544ec393a.png)

追加されたTodoは一覧で見れます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/5692c62d-accd-3289-289b-ac5c9b9c309c.png)

DetailはTodoの詳細を表示します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/4a27d5b6-a6ab-9e57-ac88-388e5e49e460.png)

EditはTodoの更新ができます。
追加画面とほぼ同じです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/03314757-be93-1e83-200f-369a22c47dc0.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/230efa6c-6d30-abda-8245-4496444e2682.png)

Deleteはそのまま削除します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66178/c032500b-e8a4-ea66-068b-4207f368b5cd.png)



# **構成について**
今回作成したコンポーネントは以下の５つになります。

- **addTodo**  
  Todoの追加を行う画面  
  一応簡単なvalidationも軽く実装
- **editTodo**  
  Todoの編集画面  
  追加画面と基本的に同じ  
- **header**  
  ヘッダと書いてありながら実質メニュー
- **todoList**  
  追加したTodoの一覧  
  更新・詳細表示・削除はこの画面から行う  
- **top**  
  一番最初に表示される画面  
  無くてもいいが、とりあえず作った感じ

そして、こちらのファイル達は、重要な役割を担っています。

- **app/src/js/routes.js**  
  v-routerのルーティング処理を実装  
  どのpathでどのコンポーネントを呼び出すかをここで定義  
- **app/src/js/app.js**  
  各種コンポーネント、css、ライブラリを読み込み、vueインスタンスを生成する処理を実装  
  初期化処理などはここにおく
- **public/js/bundle.js**  
  ビルド済みのjsファイル  
  こいつがあれば問題なし
- **public/index.html**  
  ビルド済みのbundle.jsを読み込み  
  あとはvue-routerでコンポーネントの描写に必要なディレクティブを定義するくらい  
  あんまり、やることがない…

ざっくり説明するとこんな感じになります。
これ以上分かりやすい説明が思いつかないので、実際ソースを見ながら説明します。

# **とりあえずindex.html**

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>sample Todo app</title>
</head>

<body>
  <main id="app">
    <headers></headers>
    <router-view></router-view>
  </main>
  <script src="./js/bundle.js"></script>
</body>
</html>
```

スリムところかやせ細って骨くらいしか残っていないですね。
それが良いですが。

mainタグにidを付けています。
これはvueインスタンスを生成する時にどのタグにbindするかを指定する必要があるためです。
詳しくはvueの公式を見て下さい。

`<headers>`はヘッダ（メニュー）の部分になります。
※headerというディレクティブがすでに存在するから複数形にせざるを得ない…

`<router-view>`はvue-routerを使うと時のお作法なので、気にせず覚えれば良いと思います。
これがないと各画面が描写されないのでご注意を。
詳しくは(ry

`<headers>`と`<router-view>`を別々に分けたのはヘッダを固定するためです。
メニューを各画面に実装すると非効率なので、別コンポーネントに切り出しました。

# **app.js**

```javascript:app.js
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import locale from 'element-ui/lib/locale/lang/en';
import VueRouter from 'vue-router';
import VueLocalStorage from 'vue-localstorage';

import header from '../components/header/header.vue';
import routes from './routes';

Vue.use(VueRouter);
Vue.use(ElementUI, { locale });
Vue.use(VueLocalStorage);

const router = new VueRouter({
    routes: routes
});

const app = new Vue({
    el: '#app',
    components: {
        'headers': header,
    },
    router,
});
```

vue-router、ElementUI、vue-localstorageを各コンポーネントで使用したいので、
Vue.useでそれらをVueインスタンスに追加しています。

あとは基本的にライブラリのお作法の処理なので割愛します。

# **routes.js**

```javascript
import TodoList from '../components/todoList/todoList.vue';
import AddTodo from '../components/addTodo/addTodo.vue';
import EditTodo from '../components/editTodo/editTodo.vue';
import Top from '../components/top/top.vue';

export default [
        { path: "/", component: Top },
        { path: "/TodoList", component: TodoList },
        { path: "/AddTodo", component: AddTodo },
        { path: "/EditTodo/:index", component: EditTodo },
]
```

こちらもシンプルで、各コンポーネントをimportして、
ルーティング情報を定義しているだけです。
画面を増やす時はこちらの定義も追加しないと画面遷移しないがよく忘れます。。

# **Todo一覧**
大体の流れが分かるtodo一覧画面に焦点を絞って説明します。
他の画面は、リポジトリを見て頂ければと思います。

```html:todoList.html
<div>
    <el-card shadow="never" style="margin-right: 30%; margin-left: 30%">
        <template>
            <el-table :data="todoList" style="width: 100%">
                <el-table-column prop="title" label="Todo" width="120px"></el-table-column>
                <el-table-column label="Operations" fixed="right" align="center">
                    <template slot-scope="scope">
                        <el-button size="mini" @click="handleDetail(scope.row)">Detail</el-button>
                        <el-button size="mini" @click="handleEdit(scope.$index)">Edit</el-button>
                        <el-button size="mini" type="danger" @click="handleDelete(scope.$index)">Delete</el-button>
                    </template>
                </el-table-column>
            </el-table>
        </template>
    </el-card>

    <el-dialog :title="todo.title" :visible.sync="centerDialogVisible" width="30%" center>
        <pre>{{todo.detail}}</pre>
        <span slot="footer" class="dialog-footer">
            <el-button type="primary" @click="centerDialogVisible = false">OK</el-button>
        </span>
    </el-dialog>
</div>
```

ElementUIをガッツリ使用しています。
ポイントの部分をかい摘んで行きます。

- `<el-table>`
`:data`でtodoの配列を指定すれば、
あとはライブラリ側でうまい具合にテーブルを作ってくれます。

- `<el-table-column>`
`prop`は各オブジェクトのプロパティを抽出して表示します。

- `<template slot-scope="scope">`
各オブジェクトのデータを個別に扱いたいときはscopeを使用します。
`scope.row`がtodoオブジェクトです。

- `<el-button>`
`@click`はボタンを押下した時に呼び出すメソッドを指定します。
scope.rowやらscope.$indedを引数として渡しています。
`@click`についてvueの公式サイトを見て下さい。

- `<el-dialog>`
Detailを押下した時に表示するダイアログです。
`:visible.sync`で指定している変数がtrueなら表示、falseなら非表示という仕組みです。


```vuejs:todoList.vue
<template src="./todoList.html"></template>

<script>
export default {
    data() {
        return {
            todo: {
                title: "",
                detail: "",
            },
            todoList: [],
            centerDialogVisible: false,
        }
    },
    created() {
        this.todoList = this.getTodoListFromStorage();
    },
    methods: {
        getTodoListFromStorage() {
            let data = this.$localStorage.get("todoList");
            // nullの場合空配列を返却
            if (!data) {
                return [];
            }
            // jsonを返却
            return JSON.parse(data);
        },
        setTodoListToStorage(data) {
            this.$localStorage.set("todoList", JSON.stringify(data));
        },
        handleDetail(data) {
            this.todo = data;
            this.centerDialogVisible = true;
        },
        handleEdit(targetIndex) {
            this.$router.push("/EditTodo/" + targetIndex);
        },
        handleDelete(targetIndex) {
            // callbackから直接thisにアクセス出来ないため、ローカル変数経由でアクセスする
            let todoList = this.todoList;

            // 配列から対象レコードを除去
            todoList.some(function(currentData, index) {
                if (index == targetIndex) todoList.splice(index, 1);
            });

            // ストレージ更新
            this.setTodoListToStorage(this.todoList);
        }
    }
}
</script>
```

- `<template>`
相棒のhtmlを指定しています。
vueはあくまでもロジックを実装する場所なので、htmlを切り出しました。

- `data()`
画面で使用するデータをreturnしています。
returnまではお作法なのです。

- `getTodoListFromStorage()`
LocalStorageからデータを取得しています。
LocalStorageに保存できるデータは文字列なので、
保存時はオブジェクトから文字列に変換、
取得時はJSONにパースする必要があります。

- `handleDetail()`
Detailボタンを押下時に呼ばれるメソッド。
単純に表示するデータをセットして、フラグをtrueにするだけです。

- `handleEdit()`
Todo編集画面に遷移するだけです。
app.jsで`Vue.use(VueRouter)`をしたのは`this.$route`を使用できるようにするためです。

- `handleDelete()`
配列から対象データを削除してLocalStorageを更新しているだけです。
1点注意なのが、someで指定しているcallbackからはthisにアクセスできないので、
一度ローカル変数を挟む必要が有るようです。
知らなかった…

# **最後に**
ソースを見つつ、かい摘んで説明しましたが、あんまりぴんと来ない方もいるかと思います。
なので自分で実装してみるのが一番良いと思います。

実装時にぼくが見ていたものはこちらになりますので、
もし改造してみたいとか、作ってみたいとかがあれば参考になればと思います。

- ElementUI
https://element.eleme.io/#/en-US/component/installation

- vue.js
https://jp.vuejs.org/v2/guide/

# **雑感**
簡単なTodoアプリでも意外と時間がかかりましたが、
もっと短時間で作れるように精進します。。

vue.jsは本当に便利なので、これからもメインで使っていきたいと思っています。
UIコンポーネントは…ちょっと[こちら](https://vuetifyjs.com/ja/)が気になっています。
いずれ使ってみようと思います。

次回の記事は、今作っているサービスについて何か書けたらと思います。