Vue.js初学者が書籍片手にtodoアプリをつくる　後篇：実装

## はじめに
todoアプリ開発の続きです。
前回は環境構築してVITEでローカルサーバを起動するところまで進めました。

https://qiita.com/yorocovich/private/8e12fad0f6d89b480b5f

今回はいよいよ実装を進めます。

### 参考書籍

『Vue.js 超入門 v3.4+: 最初に手にしてよかった本』

https://www.amazon.co.jp/dp/B0CW1BZ6PG?ref_=cm_sw_r_cp_ud_dp_79Q82QWRT76X0TFT1ZP6


## 実際にアプリを作っていく
### 実現する機能
画面イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296985/99b1603c-7bc7-925d-0e15-1db191bf27d6.png)

- TODOを追加できるテキスト入力欄
- 残TODOが一覧で見える一覧表示欄
- 内容編集する機能
- 完了チェックで打消線がつく機能
- TODOを削除する機能

### 実装内容
ローカルサーバを起動してindex.htmlを呼び出すところから実装を見ていきます。

#### index.html
```html:index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```
ここでは、`main.ts`を呼び出しているだけです。
次は`main.ts`を見てみます。

#### main.ts
```typescript:main.ts
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
```
このコードでは、createApp関数を使ってVueアプリケーションのインスタンスを作成し、App.vueというルートコンポーネントを指定して、HTMLの#app要素にマウントします。

:::note 
ルートコンポーネントは他のすべてのコンポーネントを包含する「土台」として考えることができます。これにより、アプリケーションの各部分が独立して機能しつつ、全体として統合された動作を実現します。Vue3では、ルートコンポーネントを通じて、状態管理やルーティングなどの機能を一元的に管理することが可能です。[^1]
:::

App.vueは[単一ファイルコンポーネント](https://ja.vuejs.org/guide/introduction.html#single-file-components)と呼ばれるscript, HTML, CSS を1つのファイルに書く構造となっています。

`<script>`タグで制御を記載する。
`<template>`タグで部品配置などHTMLに相当する部分を記載する。
`<style>`タグでデザインを記載する。

今回のアプリでは具体的に以下のように記載しました。
#### App.vue
```vue:App.vue
<script setup lang="ts">
import MainTodo from '@/components/MainTodo.vue' // メイン部の呼び出し
import TheFooter from '@/components/TheFooter.vue' // ヘッダー部の呼び出し
import TheHeader from '@/components/TheHeader.vue' // フッター部の呼び出し
</script>

<template>
  <div class="warp">
    <TheHeader /> <!-- ヘッダー部 -->
    <main class="main"><MainTodo /></main> <!-- メイン部 -->
    <TheFooter /> <!-- フッター部 -->
  </div>
</template>
```

`<sytle>`はTheHeader、MainTodo、TheFooterでそれぞれ記載しているので、ここでは書いていません。

ヘッダー部、フッター部はそれぞれ次のように記載しています。

#### TheHeader.vue
```vue:TheHeader.vue
<template>
  <h1 class="title">TODO</h1>
</template>

<style scoped>
.title {
  width: 100%;
  padding: 16px 0;
  margin: 0;
  text-align: center;
  background-color: #fff;
}
</style>
```

#### TheFooter.vue
```vue:TheFooter.vue
<template>
  <footer class="footer">&copy; yorocovich</footer>
</template>

<style scoped>
.footer {
  width: 100%;
  height: 30px;
  text-align: center;
  box-shadow: 0 1px 1px #ddd;
}
</style>
```
`<style scoped>`のようにscopedを記載することで、スタイルの適用範囲を同vueファイル内に限定することができます。

メイン部のMainTodoは次のようになっています。

#### MainTodo.vue
```vue:MainTodo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useTodoList } from '@/composables/useTodoList'
const { todoList, add, edit, check } = useTodoList()
const todo = ref<string | undefined>()

const addTodo = () => {
  if (!todo.value) return
  add(todo.value)
  todo.value = ''
}
const editTodo = (_id: number) => {
  edit(_id)
}
const changeCheck = (_id: number) => {
  check(_id)
}
</script>

<template>
  <div>
    <input
      type="text"
      class="todo_input"
      v-model="todo"
      @keyup.enter="addTodo"
      placeholder="TODOを入力"
    />
  </div>
  <div class="box_list">
    <div class="todo_list" v-for="todo in todoList" :key="todo.id">
      <div class="todo" :class="{ fin: todo.checked }">
        <input
          type="checkbox"
          class="check"
          @change="changeCheck(todo.id)"
          :checked="todo.checked"
        />
        <input
          v-show="!todo.checked"
          type="text"
          class="task"
          @keyup.enter="editTodo(todo.id)"
          @blur="editTodo(todo.id)"
          v-model="todo.task"
        />
        <label v-show="todo.checked">{{ todo.task }}</label>
      </div>
    </div>
  </div>
</template>

<style scoped>
.todo_input {
  width: 250px;
  padding: 6px 8px;
  margin-right: 8px;
  font-size: 18px;
  border: 1px solid #aaa;
  border-radius: 6px;
}

.box_list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 4px;
}

.todo_list {
  display: flex;
  gap: 5px;
  align-items: center;
}

.todo {
  width: 300px;
  padding: 4px 1px;
}

.check {
  margin-right: 12px;
  transform: scale(1.6);
}

.task {
  width: 120px;
  font-size: 15px;
  border: 0px;
}

.fin {
  font-size: 15px;
  color: #777;
  text-decoration: line-through;
  background-color: #ddd;
}
</style>
```
以下軽く解説
`import { ref } from 'vue'` ref は Vue3 の Composition API で導入された関数で、リアクティブな値を作成するために使用されます。更新を画面をリアルタイムで反映してくれるスグレモノです。

`<input type="text" class="todo_input" v-model="todo" @keyup.enter="addTodo" placeholder="TODOを入力" />` 

新規TODOを入力するテキストボックスです。
Enterキーで`addTodo`を呼び出してLocalStorageにTODOテキストを保存しています。

`<div class="todo_list" v-for="todo in todoList" :key="todo.id">`

TODOリストを作成しているところです。
LocalStorageにあるtodoListを1件ずつ配列で取得しています。
`v-for`をつかうことでループ処理を実現しています。

`<input type="checkbox" class="check" @change="changeCheck(todo.id)" :checked="todo.checked" />`

チェックボックスです。
チェックすると`changeCheck`を呼び出してチェック処理します。

`<input v-show="!todo.checked" type="text" class="task" @keyup.enter="editTodo(todo.id)" @blur="editTodo(todo.id)" v-model="todo.task" />`

TODOリストのテキスト部です。
テキストを変更すると`editTodo`を呼び出して編集しています。フォーカスアウトのときも同じ動作します。

最後に、これらの挙動を制御するスクリプト部です。
#### useTodoList.ts
```typescript:useTodoList.ts
import { ref } from 'vue'

export const useTodoList = () => {
  const todoList = ref<{ id: number; task: string; checked: boolean }[]>([])
  const ls = localStorage.todoList

  // ローカルストレージにtodoListが存在していればparseし、
  // なければ空配列をセット
  todoList.value = ls ? JSON.parse(ls) : []

  const findById = (_id: number) => {
    return todoList.value.find((todo) => todo.id === _id)
  }

  const findIndexById = (_id: number) => {
    return todoList.value.findIndex((todo) => todo.id === _id)
  }

  const add = (_task: string) => {
    const id = new Date().getTime()
    todoList.value.push({ id: id, task: _task, checked: false })
    localStorage.todoList = JSON.stringify(todoList.value)
  }

  const edit = (_id: number) => {
    const todo = findById(_id)
    const idx = findIndexById(_id)
    if (!todo) return
    if (todo?.task === '') {
      del(_id)
    } else {
      todoList.value.splice(idx, 1, todo) // splice関数で_idxを元にTODOを置換
      localStorage.todoList = JSON.stringify(todoList.value)
    }
  }

  const del = (_id: number) => {
    const todo = findById(_id)
    if (todo) {
      const idx = findIndexById(_id)
      todoList.value.splice(idx, 1)
      localStorage.todoList = JSON.stringify(todoList.value)
    }
  }

  const check = (_id: number) => {
    const todo = findById(_id)
    const idx = findIndexById(_id)

    if (todo) {
      todo.checked = !todo.checked //反転
      todoList.value.splice(idx, 1, todo)
      localStorage.todoList = JSON.stringify(todoList.value)
    }
  }

  return { todoList, add, edit, check }
}
```

`return { todoList, add, edit, check }`で作成した関数を戻しています。
MainTodo.vueのスクリプト部で`const { todoList, add, edit, check } = useTodoList()`としてキャッチしています。

## さいごに
初めてVueに触れましたが、すごく進化していて楽しかったです。
業務でWeb開発（JSP+Java）していたのが2010年頃なので、まったく違う体験をしました。「今ってこうなってるのーー！」という衝撃を受けました。

どこまで書いていいのか、どう書いていいのか、初めてのことばかりで文章が雑だと感じています。
内容の推敲は今後時間があるときに行っていきたいと思います。

書籍「Vue.js 超入門3.4+」を片手（iPad）に開発をしてきましたが、
本書の手順に忠実に従うことで、スムーズにアプリを完成させることができました。
説明が適切なタイミングで、適切な分量で出てくるので、読み進めやすかったです。
Vue初学者の方はぜひともご一読してみてはいかがでしょうか。

https://techbookfest.org/product/6jP3qbQeZRAAJhZzTHNTX7?productVariantID=5w1NkwGYW9SmGLDj4PVfyC

[^1]:https://ja.vuejs.org/guide/essentials/application#the-application-instance