Vue2のtodoリストのチュートリアルをVue3で作ってみた

Vueをやることになったので、チュートリアルをやってみました。
どうせなので、Vue３で始めようと思ったのですが、Vue3のチュートリアルが見つからなかったので、「基礎から学ぶ Vue.js」にて公開されている[ToDoリストのチュートリアル](https://cr-vue.mio3io.com/tutorials/todo.html)をVue3で書いてみました。

最終的なコードは[GitHub](https://github.com/hanashima/vue3-tutorial)においてあります。

基本的には以下の手順でおこないました。

1. vue3 + Typescript + vite で環境を構築
1. 上記チュートリアルのコードをなるべくコピペして動作するようにする。
1. composition APIで書き換える。
1. Element-plusで見栄えを整える。
1. vue-routerを入れる。


見栄えをお手軽に良くしたかったので、UIのフレームワークを入れる予定でしたが、
Vue3に対応しているElement-plusを入れてみました。
Element-plusを入れると`npm run dev`では、問題なさそうでしたが、
ビルド時に`vue-router`関係のエラーが出たので、
`vue-router`も入れる予定だったので、入れて簡単なルーティングをつけてみました。

## ファイル構成
最終版のファイル構成は、以下のような感じになっています。


```
src
├── App.vue
├── components
│   ├── HelloWorld.vue
│   └── ToDo.vue         //ToDoリストのメインのコンポーネント
├── libs
│   ├── AddToDo.ts       //AddTodoメソッドを別ファイルに書き出した。
│   └── local_storage.ts  //ローカルストレージとのやり取りも別ファイルに書き出した。
├── main.ts
├── types
│   └── todos.ts         //ToDoリストで使う型の宣言
└── views
    └── Home.vue         //vue-routerで使った仮のホームページ
```

# 環境構築
以下のコマンドでプロジェクトフォルダを作成しました。

```shell
npm init @vitejs/app

✔ Project name: … sample
✔ Select a framework: › vue
✔ Select a variant: › vue-ts
```

エディタをVSCodeを使用しましたが、Vue用の言語ライブラリは`Volar`を使用します。VSCodeのおすすめ通りにインストールすると、`Vetur`を入れてしまうので、機能を切り替えておく。
（参考：[Zenn: Vue3 + TypeScript + Tailwindの環境構築](https://zenn.dev/ymgn____/articles/e7709e88948446))

以下にざっと各ファイルの説明をしてみます。

#App.vue

ソースコードはこちら（[App.vue](https://github.com/hanashima/vue3-tutorial/blob/main/src/App.vue)）

7-13行目のところで、ローカルストレージに初期値を登録しました。

```typescript:App.vue
let todos:ATodo[] = [
  { "id": 0, "comment": "新しいToDo1", "state": 0 },
  { "id": 1, "comment": "新しいToDo2", "state": 0 }
];
todoStorage.save(todos);
todoStorage.uid = todos.length;
```

`<template>`は`vue-router`ようにルーティングのサンプルがおいてあります。


# libs/local_storage.ts

ソースコードはこちら（[libs/local_storage.ts](https://github.com/hanashima/vue3-tutorial/blob/main/src/libs/local_storage.ts)）

基本的にはTodoのページにある内容をそのままコピーしましたが、他の場所で`uid`についてのエラーがでたので、
６行目にプロパティとして追加しました。

```typescript:libs/local_storage.ts
    uid:0,
```

また、後述しますが、`fetch()`の返り値については、reactiveな変数を返すように変更しました。

# libs/AddToDo.ts

ソースコードはこちら（[libs/AddToDo.ts](https://github.com/hanashima/vue3-tutorial/blob/main/src/libs/AddToDo.ts)）

これは、compositionAPIにする際に、コンポーネントから抜き出した関数になります。`ToDo.vue`の`setup()`から呼び出しています。
抜き出す際に以下の２つを引数として設定しました。
- todoの配列となっている`todos`
- `ToDo.vue`で使用している`<form>`で扱うデータの型の変数`form`

```typescript:libs/AddToDo.ts
export function doAddTodo(todos:ATodo[],form:Form){
```

#types/todos.ts

ソースコードはこちら（[types/todos.ts](https://github.com/hanashima/vue3-tutorial/blob/main/src/types/todos.ts)）

このファイルは、型の定義を記述しました。
todoの型となる`AToDo`型（実際のtodoリストの型はAToDoの配列`AToDo[]`としています。）と、`<form>`で使用されるデータ型です。

#components/ToDo.vue

ソースコードはこちら（[components/ToDo.vue](https://github.com/hanashima/vue3-tutorial/blob/main/src/components/ToDo.vue)）

こちらがtodoリストのメインのcomponentになります。

- compositionAPI

compositionAPIはVue３から導入されたコンポーネントの書き方で従来よりも、一連の処理をまとまったところに記述することができるようで、`setup()`にまとめることができます。例えばチュートリアルにある`computed`は以下のようになっていますが、

```typescript
  computed: {

    // ★STEP12
    computedTodos: function () {
      return this.todos.filter(function (el) {
        return this.current < 0 ? true : this.current === el.state
      }, this)
    },

    // ★STEP13 作業中・完了のラベルを表示する
    labels() {
      return this.options.reduce(function (a, b) {
        return Object.assign(a, { [b.value]: b.label })
      }, {})
      // キーから見つけやすいように、次のように加工したデータを作成
      // {0: '作業中', 1: '完了', -1: 'すべて'}
    }
  },
```
以下のように書き換えられます。(57-62行目)

```typescript:components/ToDo.vue
export default defineComponent({
    setup(_, context){
        ....
        const computedTodos = computed(()=> todos.filter((el)=>{
            return current.value < 0 ? true : current.value === el.state
        }));
        const labels = computed(():{[key:number]:string}=> options.reduce((a,b)=>{
            return Object.assign(a, {[b.value]:b.label})
        },{}));
        ....
    }
}
```

他にもチュートリアルのコードでは、`doAdd()`、`doChangeState()`、`doRemove()`の３つのmethodが定義されていますが、これらも`setup()`の中に以下のように記述しました。(64-71行目)

```typescript:components/ToDo.vue
export default defineComponent({
    setup(_, context){
        ....
        const doAdd = function() { doAddTodo(todos, form) };
        const doChangeState = function(item:ATodo) { item.state = item.state ? 0 : 1 };
        // 削除の処理
        const doRemove = function(item:ATodo) {
            let index = todos.indexOf(item)
            todos.splice(index, 1)
        };
        ....
    }
}
```
(doAddは、前述の`libs/AddToDo.ts`ファイルに`doAddTodo()`関数に処理を抜き出しています。)


- Element-plus

実務の方でUIのフレームワークも使用する予定だったので、Vue3に対応している[Element-plus](https://element-plus.org/en-US/)を導入してみました。
以下の5種類のコンポーネントを利用しました。
  - `el-radio`
  - `el-form`
  - `el-button`
  - `el-input`
  - `el-table`

例えば、`el-radio`は以下のようなチュートリアルのコードから

```vue
    <label v-for="label in options">
    <input type="radio"
        v-model="current"
        v-bind:value="label.value">{{ label.label }}
    </label>
```

以下のように書き換えました。

```vue:components/ToDo.vue
    <label v-for="label in options">
        <el-radio v-model="current" v-bind:label="label.value">{{ label.label }}</el-radio>
    </label>
```


いくつかハマった点があるので、以下にまとめておきます。

# reactiveな変数の扱い
Todoリストの内容を格納する変数をリアクティブな変数として宣言したが値の代入によってリアクティブではなくなってしまった。
具体的にはlocal_storageから値をfetchするときに発生した。以下のコードではだめで、

```typescript:local_storage.ts
    fetch() {
        const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
        todos.forEach((todo:any, index:any) => {
            todo.id = index;
        });
        todoStorage.uid = todos.length;
        return todos;
    },
```

以下のように、reactiveな状態として返すことでうまく動いているようです。

```typescript:local_storage.ts
    fetch() {
        const jtodos = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
        jtodos.forEach((todo:any, index:any) => {
            todo.id = index;
        });
        todoStorage.uid = jtodos.length;
        const todos:ATodo[] = reactive<ATodo[]>(jtodos);
        return todos;  //リアクティブな変数が返される
    },
```

# el-form
element-plusの`el-form`はv-modelに変数を設定するような使い方のようだったので、専用の型(前述のtypes/todos.tsファイルにあるForm型)を作成して使用しています。
これにより`libs/AddToDo.ts`にある`doAddTodo()`関数の引数に`Form`型を渡しています。

```typescript:types/todos.ts
export class Form{
    constructor(comment:string){
        this.comment = comment
    }
    comment:string;
}
```

# el-table
element-plusの`el-table`は`:data`にデータをバインドして、`el-table-column`の`prop`属性でバインドしたデータのキーを指定するような使い方になっています。
少し変わった使い方をする場合は、`<template>`を使って、`el-table-column`コンポーネントに注入する必要があるようです。（これを[slot](https://v3.ja.vuejs.org/guide/component-slots.html#%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)と呼ぶみたいです。）
例えば以下のように「コメント」列は、`prop`属性にキーを指定するだけですが、「状態」列は行番号に対応する値を引数にして関数を呼ぶボタンをつける必要があるので、`scope.$index`の値を使って行の番号を使用しています。


```vue:components/ToDo.vue
        <el-table-column label="コメント" prop="comment" />
        <el-table-column label="状態">
            <template #default="scope">
                <el-button type="primary" @click="doChangeState(computedTodos[scope.$index])">
                    {{labels[computedTodos[scope.$index].state]}}
                </el-button>
            </template>
        </el-table-column>
```


# 一応動いているように見えますが、
なにか問題点がありそうなら教えて下さい。